Skip to content

Commit 6525400

Browse files
committed
Add standards alignment track
1 parent 358de02 commit 6525400

6 files changed

Lines changed: 449 additions & 1 deletion

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ For MCP gateway integration, see [`docs/mcp-gateway-integration.md`](docs/mcp-ga
8181
For provider-side MCP authorization, see [`docs/provider-mcp-authorization.md`](docs/provider-mcp-authorization.md).
8282
For provider MCP contract CI checks, see [`docs/provider-mcp-ci.md`](docs/provider-mcp-ci.md).
8383
For provider MCP positioning and adoption ideas, see [`docs/provider-mcp-positioning.md`](docs/provider-mcp-positioning.md).
84+
For DID, Verifiable Credential, OAuth/OIDC, MCP, A2A, AGNTCY, and NIST alignment, see [`docs/standards-alignment.md`](docs/standards-alignment.md).
8485
For the API-to-MCP article, see [`docs/turn-your-api-into-mcp-safely.md`](docs/turn-your-api-into-mcp-safely.md).
8586
For the provider MCP authorization demo, see [`docs/provider-mcp-demo.md`](docs/provider-mcp-demo.md).
8687
For job-to-be-done boundaries, see [`docs/job-boundaries.md`](docs/job-boundaries.md).
@@ -116,6 +117,13 @@ section maps customer identity-provider claims to AgentID concepts such as
116117
tenant, user, and agent, then declares the scopes required to authorize tool
117118
calls, read policies, or issue JIT grants.
118119

120+
AgentID can also carry optional distributed identity metadata. A manifest may
121+
bind an agent to a DID, declare trusted issuers, and include VC-style
122+
attestations for security review, provider approval, compliance status, or
123+
operational readiness. These fields are evidence inputs for runtime policy; they
124+
do not replace AgentID's action-level authorization decision. See
125+
[`docs/standards-alignment.md`](docs/standards-alignment.md).
126+
119127
## Positioning in one minute
120128

121129
AgentID sits between agent identity and tool execution.
@@ -350,6 +358,9 @@ For MCP server calls, including internal and provider-hosted servers, see
350358
For the provider side of that boundary, including authorization receipts and
351359
provider execution receipts, see
352360
[`docs/provider-mcp-authorization.md`](docs/provider-mcp-authorization.md).
361+
For standards alignment and contribution targets around DID, Verifiable
362+
Credentials, OAuth/OIDC, MCP, A2A, AGNTCY, and NIST, see
363+
[`docs/standards-alignment.md`](docs/standards-alignment.md).
353364
For a reference adapter, see [`mcp-gateway-adapter/`](mcp-gateway-adapter/).
354365
For provider-side Express receipt verification middleware, see
355366
[`packages/provider-express/`](packages/provider-express/).

agentid/explain.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ def explain_manifest(manifest: dict[str, Any]) -> str:
1515
audit = manifest.get("audit", {})
1616
kill_switch = manifest.get("kill_switch", {})
1717
oidc = manifest.get("oidc", {})
18+
trusted_issuers = manifest.get("trusted_issuers", [])
19+
attestations = manifest.get("attestations", [])
1820

1921
lines: list[str] = []
2022

2123
lines.append(f"Agent: {agent.get('name', 'Unnamed agent')} ({agent.get('id', 'missing-id')})")
2224
lines.append(f"Owner: {agent.get('owner', 'missing-owner')}")
2325
lines.append(f"Environment: {agent.get('environment', 'unspecified')}")
2426
lines.append(f"Purpose: {agent.get('purpose', 'unspecified')}")
27+
if agent.get("did"):
28+
lines.append(f"DID: {agent['did']}")
2529
if agent.get("expires_at"):
2630
lines.append(f"Authority expires: {agent['expires_at']}")
2731

@@ -54,6 +58,27 @@ def explain_manifest(manifest: dict[str, Any]) -> str:
5458
+ ", ".join(f"{name}={scope}" for name, scope in scopes.items())
5559
)
5660

61+
lines.append("")
62+
lines.append("Trust and attestations:")
63+
if trusted_issuers:
64+
lines.append("- Trusted issuers: " + ", ".join(trusted_issuers))
65+
else:
66+
lines.append("- Trusted issuers: none declared")
67+
if not attestations:
68+
lines.append("- No attestations declared.")
69+
else:
70+
for attestation in attestations:
71+
summary = (
72+
f"- {attestation.get('type', 'unnamed-attestation')}: "
73+
f"issuer={attestation.get('issuer', 'missing-issuer')}, "
74+
f"result={attestation.get('result', 'unknown')}"
75+
)
76+
if attestation.get("standard"):
77+
summary += f", standard={attestation['standard']}"
78+
if attestation.get("expires_at"):
79+
summary += f", expires={attestation['expires_at']}"
80+
lines.append(summary)
81+
5782
lines.append("")
5883
lines.append("Just-in-time authorization:")
5984
lines.append(f"- Enabled: {bool(jit.get('enabled'))}")
@@ -94,6 +119,9 @@ def explain_manifest(manifest: dict[str, Any]) -> str:
94119
lines.append(f"- Enforce manifest: {bool(runtime.get('enforce_manifest'))}")
95120
lines.append(f"- Detect tool drift: {bool(runtime.get('detect_tool_drift'))}")
96121
lines.append(f"- Detect new destinations: {bool(runtime.get('detect_new_destinations'))}")
122+
lines.append(f"- Require valid attestations: {bool(runtime.get('require_valid_attestations'))}")
123+
lines.append(f"- Deny if attestation expired: {bool(runtime.get('deny_if_attestation_expired'))}")
124+
lines.append(f"- Deny if credential revoked: {bool(runtime.get('deny_if_credential_revoked'))}")
97125

98126
lines.append("")
99127
lines.append("Audit:")

agentid/manifest.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class ValidationResult:
2424
VALID_APPROVAL = {"none", "notify", "required", "human_confirm", "step_up", "manager", "block"}
2525
VALID_AUTH_MODE = {"delegated", "service", "just_in_time"}
2626
VALID_OIDC_TOKEN_MODES = {"jwks", "demo_hs256"}
27+
VALID_ATTESTATION_RESULTS = {"pass", "fail", "partial", "unknown"}
2728

2829

2930
def load_manifest(path: str | Path) -> dict[str, Any]:
@@ -56,6 +57,9 @@ def validate_manifest(manifest: dict[str, Any]) -> ValidationResult:
5657
if not agent.get(field):
5758
errors.append(f"Missing required field: agent.{field}")
5859

60+
_validate_agent_identity(manifest, errors, warnings)
61+
_validate_trusted_issuers(manifest, errors, warnings)
62+
_validate_attestations(manifest, errors, warnings)
5963
_validate_jit_authorization(manifest, errors, warnings)
6064
_validate_oidc(manifest, errors, warnings)
6165
_validate_tools(manifest, errors, warnings)
@@ -76,6 +80,87 @@ def validate_manifest(manifest: dict[str, Any]) -> ValidationResult:
7680
return ValidationResult(ok=not errors, errors=errors, warnings=warnings)
7781

7882

83+
def _validate_agent_identity(manifest: dict[str, Any], errors: list[str], warnings: list[str]) -> None:
84+
agent = manifest.get("agent", {})
85+
if not isinstance(agent, dict):
86+
return
87+
88+
did = agent.get("did")
89+
if did is None:
90+
return
91+
if not isinstance(did, str) or not did:
92+
errors.append("agent.did must be a non-empty string if provided.")
93+
elif not did.startswith("did:"):
94+
warnings.append("agent.did does not look like a decentralized identifier.")
95+
96+
97+
def _validate_trusted_issuers(manifest: dict[str, Any], errors: list[str], warnings: list[str]) -> None:
98+
issuers = manifest.get("trusted_issuers")
99+
attestations = manifest.get("attestations", [])
100+
101+
if issuers is None:
102+
if attestations:
103+
warnings.append("trusted_issuers is not set. Attestation signatures may lack an explicit trust policy.")
104+
return
105+
106+
if not isinstance(issuers, list):
107+
errors.append("trusted_issuers must be a list.")
108+
return
109+
110+
for idx, issuer in enumerate(issuers):
111+
if not isinstance(issuer, str) or not issuer:
112+
errors.append(f"trusted_issuers[{idx}] must be a non-empty string.")
113+
114+
115+
def _validate_attestations(manifest: dict[str, Any], errors: list[str], warnings: list[str]) -> None:
116+
attestations = manifest.get("attestations")
117+
if attestations is None:
118+
runtime = manifest.get("runtime", {})
119+
if isinstance(runtime, dict) and runtime.get("require_valid_attestations"):
120+
errors.append("attestations is required when runtime.require_valid_attestations is true.")
121+
return
122+
123+
if not isinstance(attestations, list):
124+
errors.append("attestations must be a list.")
125+
return
126+
127+
agent_did = manifest.get("agent", {}).get("did") if isinstance(manifest.get("agent"), dict) else None
128+
trusted_issuers = set(manifest.get("trusted_issuers", []) or [])
129+
130+
for idx, attestation in enumerate(attestations):
131+
prefix = f"attestations[{idx}]"
132+
if not isinstance(attestation, dict):
133+
errors.append(f"{prefix} must be an object.")
134+
continue
135+
136+
for field in ["type", "issuer", "result"]:
137+
if not attestation.get(field):
138+
errors.append(f"{prefix}.{field} is required.")
139+
140+
issuer = attestation.get("issuer")
141+
if isinstance(issuer, str) and trusted_issuers and issuer not in trusted_issuers:
142+
warnings.append(f"{prefix}.issuer is not listed in trusted_issuers: {issuer}.")
143+
144+
subject = attestation.get("subject")
145+
if subject is not None and (not isinstance(subject, str) or not subject):
146+
errors.append(f"{prefix}.subject must be a non-empty string if provided.")
147+
elif agent_did and subject and subject != agent_did:
148+
warnings.append(f"{prefix}.subject does not match agent.did.")
149+
150+
result = attestation.get("result")
151+
if result and result not in VALID_ATTESTATION_RESULTS:
152+
errors.append(f"{prefix}.result must be one of: {', '.join(sorted(VALID_ATTESTATION_RESULTS))}.")
153+
154+
expires_at = attestation.get("expires_at")
155+
if expires_at:
156+
_validate_date_field(f"{prefix}.expires_at", expires_at, errors, warnings)
157+
else:
158+
warnings.append(f"{prefix}.expires_at is not set.")
159+
160+
if not attestation.get("credential_status"):
161+
warnings.append(f"{prefix}.credential_status is not set. Revocation checks may not be possible.")
162+
163+
79164
def _validate_oidc(manifest: dict[str, Any], errors: list[str], warnings: list[str]) -> None:
80165
oidc = manifest.get("oidc")
81166
if oidc is None:
@@ -331,6 +416,18 @@ def _validate_runtime(manifest: dict[str, Any], errors: list[str], warnings: lis
331416
if not runtime.get(field):
332417
warnings.append(f"runtime.{field} is not true.")
333418

419+
if runtime.get("require_valid_attestations"):
420+
if not manifest.get("attestations"):
421+
errors.append("runtime.require_valid_attestations is true but attestations is empty.")
422+
if not manifest.get("trusted_issuers"):
423+
errors.append("runtime.require_valid_attestations is true but trusted_issuers is empty.")
424+
425+
if runtime.get("require_valid_attestations") and not runtime.get("deny_if_attestation_expired"):
426+
warnings.append("runtime.deny_if_attestation_expired is not true while valid attestations are required.")
427+
428+
if runtime.get("require_valid_attestations") and not runtime.get("deny_if_credential_revoked"):
429+
warnings.append("runtime.deny_if_credential_revoked is not true while valid attestations are required.")
430+
334431

335432
def _validate_audit(manifest: dict[str, Any], errors: list[str], warnings: list[str]) -> None:
336433
audit = manifest.get("audit", {})
@@ -365,3 +462,17 @@ def _validate_expiry(value: Any, warnings: list[str], errors: list[str]) -> None
365462

366463
if expiry < date.today():
367464
warnings.append("agent.expires_at is in the past.")
465+
466+
467+
def _validate_date_field(field: str, value: Any, errors: list[str], warnings: list[str]) -> None:
468+
try:
469+
if isinstance(value, date):
470+
parsed = value
471+
else:
472+
parsed = datetime.strptime(str(value), "%Y-%m-%d").date()
473+
except ValueError:
474+
errors.append(f"{field} must be YYYY-MM-DD.")
475+
return
476+
477+
if parsed < date.today():
478+
warnings.append(f"{field} is in the past.")

0 commit comments

Comments
 (0)