AID-H-024.001: Fully Qualified Tool ID & Version Pinning

Version 1.20261015 · Actionable sub-technique

Require fully qualified references (namespace/tool@version) for all tool calls; reject bare names and floating aliases to prevent ambiguous or hijacked resolution.

Pillars: app

Lifecycle: building, operation

Scope boundary

Owns canonical fully qualified tool identifiers and immutable version selection at resolution time. It rejects bare, floating, or ambiguous references but does not verify descriptor content or contract meaning.

AID-H-024.002: AID-H-024.001 pins the tool namespace and version; AID-H-024.002 binds the descriptor bytes returned for that identity. A fully qualified name can still serve drifted descriptor content.

Implementation guidance

AID-H-024.001-G001

Fail-closed resolver entry point: only accept namespace/tool@x.y.z; never resolve bare names or floating tags.

Use this resolver for registry-backed or configured agent tools before descriptor loading or invocation. The caller must supply a VerifiedAllowlist produced by a signed-artifact loader such as AID-H-021.002; that dependency must bind the exact allowlist bytes, signature, policy version, and digest. A dependency PASS only makes the allowlist eligible for this local resolution decision.

Implement an exact, typed resolver
# File: tool_resolution/fqid_resolver.py
from __future__ import annotations

import re
from dataclasses import dataclass

import semver

IDENTIFIER = r"[a-z0-9](?:[a-z0-9._-]{0,61}[a-z0-9])?"
FQID = re.compile(
    rf"^(?P<namespace>{IDENTIFIER})/(?P<name>{IDENTIFIER})@(?P<version>[^@/]+)$"
)
SHA256 = re.compile(r"^[0-9a-f]{64}$")


class ResolutionDenied(RuntimeError):
    pass


@dataclass(frozen=True)
class ToolRecord:
    tool_id: str
    endpoint: str
    descriptor_sha256: str
    descriptor_media_type: str


@dataclass(frozen=True)
class VerifiedAllowlist:
    records: dict[str, ToolRecord]
    policy_version: str
    policy_sha256: str
    signature_sha256: str


def canonical_fqid(tool_ref: str) -> str:
    if not isinstance(tool_ref, str) or not 5 <= len(tool_ref) <= 190:
        raise ResolutionDenied("invalid_tool_reference_length")
    match = FQID.fullmatch(tool_ref)
    if match is None:
        raise ResolutionDenied("fully_qualified_tool_id_required")
    raw_version = match.group("version")
    try:
        parsed = semver.Version.parse(raw_version)
    except ValueError as exc:
        raise ResolutionDenied("invalid_semver") from exc
    if str(parsed) != raw_version:
        raise ResolutionDenied("noncanonical_semver")
    return tool_ref


def resolve(tool_ref: str, allowlist: VerifiedAllowlist) -> tuple[ToolRecord, dict]:
    requested = canonical_fqid(tool_ref)
    if not isinstance(allowlist, VerifiedAllowlist):
        raise ResolutionDenied("verified_allowlist_required")
    if not allowlist.policy_version or not SHA256.fullmatch(allowlist.policy_sha256):
        raise ResolutionDenied("invalid_allowlist_receipt")
    if not SHA256.fullmatch(allowlist.signature_sha256):
        raise ResolutionDenied("invalid_allowlist_signature_receipt")

    record = allowlist.records.get(requested)
    if record is None:
        raise ResolutionDenied("tool_not_allowlisted")
    if not isinstance(record, ToolRecord) or record.tool_id != requested:
        raise ResolutionDenied("allowlist_record_identity_mismatch")
    if not SHA256.fullmatch(record.descriptor_sha256):
        raise ResolutionDenied("invalid_descriptor_digest")
    if not isinstance(record.endpoint, str) or not record.endpoint:
        raise ResolutionDenied("invalid_endpoint")
    if any(ord(character) < 0x20 for character in record.endpoint):
        raise ResolutionDenied("endpoint_contains_control_character")
    if record.descriptor_media_type != "application/json":
        raise ResolutionDenied("unsupported_descriptor_media_type")

    receipt = {
        "schema_version": "aidefend.h024001-resolution.v1",
        "status": "PASS",
        "requested_tool_id": requested,
        "resolved_tool_id": record.tool_id,
        "endpoint": record.endpoint,
        "descriptor_sha256": record.descriptor_sha256,
        "allowlist_policy_version": allowlist.policy_version,
        "allowlist_policy_sha256": allowlist.policy_sha256,
        "allowlist_signature_sha256": allowlist.signature_sha256,
    }
    return record, receipt
Enforce and independently verify

The dispatcher calls this function with the exact requested reference; it must not retry with a bare name, strip the version, follow latest, or substitute a near match. Store both denied and allowed receipts outside the resolver identity. A read-only verifier reloads the same signed allowlist release and replays every recorded request, then confirms the descriptor loader consumed the exact resolved ID and descriptor digest. Endpoint transport identity is enforced separately by AID-H-028.001 or the applicable service-authentication control.

Action: Replace all aliases and best-match fallback paths with this exact resolver. Release a tool only when the signed allowlist dependency is valid and the local requested-ID, record-ID, version, media type, and descriptor-digest checks all succeed.

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-024.001: Fully Qualified Tool ID & Version Pinning | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version