Skip to content

Commit c622e42

Browse files
feat(oci): add OCI manifest adapter for AI Card spec alignment (#1366) (#1456)
Implement OciManifestAdapter for converting AGT agent manifests to/from OCI manifest format with AI Card compatibility. Enables registry-based discovery and verification of agent metadata. Features: - AGT identity to AI Card conversion (bidirectional) - OCI manifest packaging with typed layers (AI Card + policy) - Layer digest verification (SHA-256) - Policy rule packaging in OCI layers - Standard OCI annotations (title, vendor, description) - AI Card metadata preservation (DID, delegation, expiry) Media types: - application/vnd.ai-card.agent.v1+json (agent metadata) - application/vnd.agt.policy.v1+json (governance policy) New types: OciManifest, OciDescriptor, AICard, AICardSkill, AICardCapabilities, AICardInvocation, OciPackageResult 20 tests covering conversion, packaging, unpacking, round-trips, and integrity verification. Closes #1366 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7f8baaf commit c622e42

4 files changed

Lines changed: 483 additions & 0 deletions

File tree

agent-governance-typescript/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export { GovernanceVerifier } from './verify';
2828
export { SurfaceParityChecker } from './surface-parity';
2929
export { CascadeContainmentManager } from './cascade-containment';
3030
export { ContextPoisoningDetector } from './context-poisoning';
31+
export { OciManifestAdapter } from './oci-manifest';
3132

3233
// E2E Encryption (AgentMesh Wire Protocol v1.0)
3334
export {
@@ -92,6 +93,13 @@ export type {
9293
PoisoningFinding,
9394
ContextPoisoningScanResult,
9495
ContextIsolationViolation,
96+
OciManifest,
97+
OciDescriptor,
98+
AICard,
99+
AICardSkill,
100+
AICardCapabilities,
101+
AICardInvocation,
102+
OciPackageResult,
95103
} from './types';
96104
export type { PromptDefenseConfig, PromptDefenseFinding, PromptDefenseReport } from './prompt-defense';
97105
export type {
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { createHash } from 'crypto';
5+
import type {
6+
AgentIdentityJSON,
7+
AICard,
8+
AICardCapabilities,
9+
AICardSkill,
10+
OciDescriptor,
11+
OciManifest,
12+
OciPackageResult,
13+
PolicyRule,
14+
} from './types';
15+
16+
const AI_CARD_MEDIA_TYPE = 'application/vnd.ai-card.agent.v1+json';
17+
const POLICY_MEDIA_TYPE = 'application/vnd.agt.policy.v1+json';
18+
const OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.v2+json';
19+
const OCI_CONFIG_MEDIA_TYPE = 'application/vnd.oci.image.config.v1+json';
20+
21+
/**
22+
* Converts AGT agent manifests to/from OCI manifest format with AI Card
23+
* compatibility. Enables registry-based discovery and verification of
24+
* agent metadata.
25+
*/
26+
export class OciManifestAdapter {
27+
/**
28+
* Convert an AGT agent identity to an AI Card.
29+
*/
30+
identityToAICard(identity: AgentIdentityJSON, extra?: Partial<AICard>): AICard {
31+
const card: AICard = {
32+
name: identity.name ?? identity.did,
33+
version: '1.0.0',
34+
description: identity.description,
35+
author: identity.organization ?? identity.sponsor,
36+
capabilities: this.capabilitiesToAICard(identity.capabilities),
37+
metadata: {
38+
did: identity.did,
39+
status: identity.status ?? 'active',
40+
createdAt: identity.createdAt,
41+
expiresAt: identity.expiresAt,
42+
parentDid: identity.parentDid,
43+
delegationDepth: identity.delegationDepth,
44+
},
45+
...extra,
46+
};
47+
48+
return card;
49+
}
50+
51+
/**
52+
* Convert an AI Card back to an AGT agent identity.
53+
*/
54+
aiCardToIdentity(card: AICard, publicKey: string): AgentIdentityJSON {
55+
const metadata = (card.metadata ?? {}) as Record<string, unknown>;
56+
return {
57+
did: (metadata.did as string) ?? `did:agent:${card.name}`,
58+
publicKey,
59+
capabilities: this.aiCardToCapabilities(card.capabilities),
60+
name: card.name,
61+
description: card.description,
62+
organization: card.author,
63+
status: (metadata.status as AgentIdentityJSON['status']) ?? 'active',
64+
createdAt: metadata.createdAt as string | undefined,
65+
expiresAt: metadata.expiresAt as string | undefined,
66+
parentDid: metadata.parentDid as string | undefined,
67+
delegationDepth: metadata.delegationDepth as number | undefined,
68+
};
69+
}
70+
71+
/**
72+
* Package an agent identity and optional policy rules into an OCI manifest.
73+
*/
74+
package(
75+
identity: AgentIdentityJSON,
76+
policyRules?: PolicyRule[],
77+
extra?: Partial<AICard>,
78+
): OciPackageResult {
79+
const aiCard = this.identityToAICard(identity, extra);
80+
const aiCardJson = JSON.stringify(aiCard, null, 2);
81+
const aiCardDescriptor = this.createDescriptor(aiCardJson, AI_CARD_MEDIA_TYPE, {
82+
'org.opencontainers.image.title': 'ai-card.json',
83+
});
84+
85+
const layers: Array<{ descriptor: OciDescriptor; content: string }> = [
86+
{ descriptor: aiCardDescriptor, content: aiCardJson },
87+
];
88+
89+
if (policyRules && policyRules.length > 0) {
90+
const policyJson = JSON.stringify({ rules: policyRules }, null, 2);
91+
const policyDescriptor = this.createDescriptor(policyJson, POLICY_MEDIA_TYPE, {
92+
'org.opencontainers.image.title': 'policy.json',
93+
});
94+
layers.push({ descriptor: policyDescriptor, content: policyJson });
95+
}
96+
97+
const configContent = JSON.stringify({
98+
created: new Date().toISOString(),
99+
author: identity.organization ?? identity.sponsor ?? 'unknown',
100+
agent: { did: identity.did, name: identity.name },
101+
});
102+
const configDescriptor = this.createDescriptor(configContent, OCI_CONFIG_MEDIA_TYPE);
103+
104+
const manifest: OciManifest = {
105+
schemaVersion: 2,
106+
mediaType: OCI_MANIFEST_MEDIA_TYPE,
107+
config: configDescriptor,
108+
layers: layers.map((l) => l.descriptor),
109+
annotations: {
110+
'org.opencontainers.image.title': identity.name ?? identity.did,
111+
'org.opencontainers.image.description': identity.description ?? '',
112+
'org.opencontainers.image.vendor': identity.organization ?? '',
113+
'dev.ai-card.version': aiCard.version,
114+
'dev.agt.did': identity.did,
115+
},
116+
};
117+
118+
return { manifest, aiCard, configBlob: configContent, layers };
119+
}
120+
121+
/**
122+
* Extract an AI Card from an OCI manifest's layers.
123+
*/
124+
unpackAICard(layers: Array<{ descriptor: OciDescriptor; content: string }>): AICard | null {
125+
const aiCardLayer = layers.find((l) => l.descriptor.mediaType === AI_CARD_MEDIA_TYPE);
126+
if (!aiCardLayer) return null;
127+
128+
try {
129+
return JSON.parse(aiCardLayer.content) as AICard;
130+
} catch {
131+
return null;
132+
}
133+
}
134+
135+
/**
136+
* Extract policy rules from an OCI manifest's layers.
137+
*/
138+
unpackPolicies(layers: Array<{ descriptor: OciDescriptor; content: string }>): PolicyRule[] {
139+
const policyLayer = layers.find((l) => l.descriptor.mediaType === POLICY_MEDIA_TYPE);
140+
if (!policyLayer) return [];
141+
142+
try {
143+
const parsed = JSON.parse(policyLayer.content) as { rules: PolicyRule[] };
144+
return parsed.rules ?? [];
145+
} catch {
146+
return [];
147+
}
148+
}
149+
150+
/**
151+
* Verify the integrity of an OCI manifest by checking layer digests.
152+
*/
153+
verifyManifest(
154+
manifest: OciManifest,
155+
layers: Array<{ descriptor: OciDescriptor; content: string }>,
156+
configContent?: string,
157+
): { valid: boolean; errors: string[] } {
158+
const errors: string[] = [];
159+
160+
if (manifest.schemaVersion !== 2) {
161+
errors.push(`Unsupported schema version: ${manifest.schemaVersion}`);
162+
}
163+
164+
// Verify config digest
165+
if (configContent) {
166+
const expectedDigest = this.sha256Digest(configContent);
167+
if (manifest.config.digest !== expectedDigest) {
168+
errors.push(`Config digest mismatch: expected ${expectedDigest}, got ${manifest.config.digest}`);
169+
}
170+
}
171+
172+
// Verify layer digests
173+
for (const layer of layers) {
174+
const expectedDigest = this.sha256Digest(layer.content);
175+
if (layer.descriptor.digest !== expectedDigest) {
176+
errors.push(
177+
`Layer digest mismatch for ${layer.descriptor.annotations?.['org.opencontainers.image.title'] ?? 'unknown'}: expected ${expectedDigest}, got ${layer.descriptor.digest}`,
178+
);
179+
}
180+
181+
const expectedSize = Buffer.byteLength(layer.content, 'utf-8');
182+
if (layer.descriptor.size !== expectedSize) {
183+
errors.push(
184+
`Layer size mismatch: expected ${expectedSize}, got ${layer.descriptor.size}`,
185+
);
186+
}
187+
}
188+
189+
return { valid: errors.length === 0, errors };
190+
}
191+
192+
// ── Internal helpers ──
193+
194+
private createDescriptor(
195+
content: string,
196+
mediaType: string,
197+
annotations?: Record<string, string>,
198+
): OciDescriptor {
199+
return {
200+
mediaType,
201+
digest: this.sha256Digest(content),
202+
size: Buffer.byteLength(content, 'utf-8'),
203+
annotations,
204+
};
205+
}
206+
207+
private sha256Digest(content: string): string {
208+
const hash = createHash('sha256').update(content, 'utf-8').digest('hex');
209+
return `sha256:${hash}`;
210+
}
211+
212+
private capabilitiesToAICard(capabilities: string[]): AICardCapabilities {
213+
return {
214+
domains: capabilities,
215+
input_modes: ['text'],
216+
output_modes: ['text', 'json'],
217+
};
218+
}
219+
220+
private aiCardToCapabilities(capabilities?: AICardCapabilities): string[] {
221+
if (!capabilities) return [];
222+
return capabilities.domains ?? [];
223+
}
224+
}

agent-governance-typescript/src/types.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,72 @@ export interface ContextIsolationViolation {
394394
description: string;
395395
}
396396

397+
// ΓöÇΓöÇ OCI Manifest Adapter ΓöÇΓöÇ
398+
399+
/** OCI manifest format (v2 schema). */
400+
export interface OciManifest {
401+
schemaVersion: 2;
402+
mediaType: string;
403+
config: OciDescriptor;
404+
layers: OciDescriptor[];
405+
annotations?: Record<string, string>;
406+
}
407+
408+
/** OCI content descriptor. */
409+
export interface OciDescriptor {
410+
mediaType: string;
411+
digest: string;
412+
size: number;
413+
annotations?: Record<string, string>;
414+
}
415+
416+
/** AI Card agent metadata (framework-neutral). */
417+
export interface AICard {
418+
name: string;
419+
version: string;
420+
description?: string;
421+
author?: string;
422+
license?: string;
423+
homepage?: string;
424+
repository?: string;
425+
skills?: AICardSkill[];
426+
capabilities?: AICardCapabilities;
427+
invocation?: AICardInvocation;
428+
metadata?: Record<string, unknown>;
429+
}
430+
431+
export interface AICardSkill {
432+
name: string;
433+
description?: string;
434+
parameters?: Record<string, string>;
435+
returns?: string;
436+
}
437+
438+
export interface AICardCapabilities {
439+
languages?: string[];
440+
domains?: string[];
441+
input_modes?: string[];
442+
output_modes?: string[];
443+
streaming?: boolean;
444+
async?: boolean;
445+
}
446+
447+
export interface AICardInvocation {
448+
protocol?: string;
449+
endpoint?: string;
450+
authentication?: { type: string; required: boolean };
451+
method?: string;
452+
format?: string;
453+
}
454+
455+
/** Result of converting AGT identity to OCI manifest. */
456+
export interface OciPackageResult {
457+
manifest: OciManifest;
458+
aiCard: AICard;
459+
configBlob: string;
460+
layers: Array<{ descriptor: OciDescriptor; content: string }>;
461+
}
462+
397463
// ΓöÇΓöÇ Audit ΓöÇΓöÇ
398464

399465
export interface AuditConfig {

0 commit comments

Comments
 (0)