AID-H-024.001: Fully Qualified Tool ID & Version Pinning
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.
AID-H-024.001-G001
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
- python-semver
- node-semver
- python-jsonschema
- Open Policy Agent (OPA)
- OpenTelemetry
Commercial / hosted tools
- Kong Gateway Enterprise (custom fail-closed resolver plugin required)
- Kong Konnect (custom fail-closed resolver plugin required)
- Apigee API Management (custom fail-closed resolver policy/extension required)
Threat mappings
MITRE ATLAS
- AML.T0010 AI Supply Chain Compromise
- AML.T0011 User Execution
- AML.T0115 Publish Poisoned AI Artifacts
- AML.T0010.001 AI Supply Chain Compromise: AI Software
- AML.T0074 Masquerading
- AML.T0115.002 Publish Poisoned AI Artifacts: AI Agent Tools
- AML.T0011.002 User Execution: Poisoned AI Agent Tool
- AML.T0060 Publish Hallucinated Entities (version pinning prevents resolution of hallucination-squatted tools)
- AML.T0010.005 AI Supply Chain Compromise: AI Agent Tool
- AML.T0109 AI Supply Chain Rug Pull
MAESTRO
- Compromised Agent Registry (L7)
- Agent Impersonation (L7)
- Supply Chain Attacks (Cross-Layer)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection
OWASP ML Top 10 2023
- ML06:2023 AI Supply Chain Attacks (tool version pinning is part of supply chain integrity)
OWASP Top 10 for Agentic Applications 2026
- ASI04:2026 Agentic Supply Chain Vulnerabilities
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.1.4 Tool Shadowing
- AISubtech-4.3.2 Namespace Collision
- AISubtech-9.3.1 Malicious Package / Tool Injection
- AISubtech-9.3.2 Dependency Name Squatting (Tools / Servers)
- AISubtech-9.3.3 Dependency Replacement / Rug Pull
- AITech-12.1 Tool Exploitation
- AITech-4.3 Protocol Manipulation
- AITech-9.3 Dependency / Plugin Compromise
Google Secure AI Framework 2.0 - Risks
- IIC: Insecure Integrated Component (version pinning prevents binding to wrong tool implementation)
Databricks AI Security Framework 3.0
- Agents - Tools MCP Server 13.18: Tool Poisoning
- Agents - Tools MCP Server 13.21: Supply Chain Attacks