diff --git a/proposals/asbom/README.md b/proposals/asbom/README.md new file mode 100644 index 00000000..a6638b94 --- /dev/null +++ b/proposals/asbom/README.md @@ -0,0 +1,74 @@ +# Proposal: Agentic-System Bill of Materials (ASBOM) and `principal` field + +**Status:** Draft RFC for community feedback +**Discussion:** [#940](https://github.com/CycloneDX/specification/discussions/940) +**Process step:** [Standardization process](https://cyclonedx.org/participate/standardization-process/) — "Pull requests can be initially created in 'draft' to enable early community feedback." +**Author:** Markus Hupfauer (@mhupfauer) +**Date:** 2026-05-30 + +## What this PR is (and is not) + +This PR surfaces the proposal from discussion [#940](https://github.com/CycloneDX/specification/discussions/940) in **draft PR form** for early community feedback, per the CycloneDX standardization process. It deliberately does **not** patch the released v1.7 schema or pre-empt the in-flight modular v2.0 architecture in PR [#930](https://github.com/CycloneDX/specification/pull/930). The intent is to give the Core Working Group a reviewable surface (file diff, line comments, suggested edits) for a proposal that has not received engagement on the discussion thread. + +If the Working Group accepts the proposal, the natural follow-up work is to: + +- realise the `principal` field as a modular model under [`schema/2.0/model/`](../../schema/2.0/model/) once that structure lands via [#930](https://github.com/CycloneDX/specification/pull/930) — likely as `cyclonedx-principal-2.0.schema.json`, referenced from `cyclonedx-service-2.0.schema.json` and `cyclonedx-component-2.0.schema.json`; +- align ASBOM's `bomType` discriminator with whatever convention v2.0 adopts (the current draft uses `metadata.properties.cdx:bomType` for compatibility with 1.7 consumers); +- compose with the [party model](https://github.com/CycloneDX/specification/pull/930) where appropriate — the `valid-party-system-service-account-2.0.json` and `valid-party-ai-agent-2.0.json` fixtures in #930 cover concepts adjacent to `principal.attributionMode`, and the two models should be reconciled rather than overlap. + +## Contents + +- **[`profile.md`](./profile.md)** — ASBOM v0.1 specification. Conformance rules, component taxonomy mapping agentic-system parts to existing CycloneDX `component`/`service` types, composition pattern (`compositions`, `formulation.runtimeTopology`), attestation via `declarations`, signing requirements, property-bag fallback for consumers without the `principal` field. +- **[`principal.md`](./principal.md)** — Schema proposal for the one new field ASBOM requires. Decomposes credential modelling into four orthogonal axes (attribution mode, credential storage, credential lifetime, consent binding), frames the gap around non-repudiation, ships a JSON Schema fragment and worked diff examples (OBO MCP server vs. PAT shim). Generally useful beyond ASBOM (CI pipelines, serverless chains, multi-tenant SaaS). +- **[`example-claude-code-session.cdx.json`](./example-claude-code-session.cdx.json)** — Valid CycloneDX 1.7 ASBOM applied to a realistic scenario: Claude Code in Windows 11 Agent Workspace, one skill installed via internal registry, one MCP server using delegated-OBO with DPoP-bound OAuth tokens in OS keychain, plus signed fleet-admin and registry-operator attestations. + +## Why a new BOM type, not just a profile + +The agentic-AI ecosystem has rapidly moved to a place where capabilities (SKILL.md packages, MCP servers, plugins) are installable artifacts, hosts (Claude Code, Cursor, Gemini CLI, GitHub CLI) load them dynamically, and operating systems (Windows 11 Agent Workspace) now expose dedicated agent principals. The *running composition* of these parts — which model, in which harness, with which skills, in which runtime, under which credentials — is what determines an agentic system's effective authority and blast radius. It is the unit operators must audit, sign, and respond to in an incident. + +| BOM type | Inventories | +|---|---| +| SBOM | Software components | +| HBOM | Hardware components | +| ML-BOM | Models, training datasets, training methodology | +| SaaSBOM | Service surfaces, endpoints, data flows | +| CBOM | Cryptographic assets | +| MBOM | Manufactured items | +| OBOM | Operations / runtime configuration | +| **ASBOM** | **Runtime composition of an agentic system + identities under which each part executes** | + +Where ML-BOM answers "what is this model and how was it trained" and SaaSBOM answers "what services and data flows exist," ASBOM answers "what composition is executing *right now*, under whose authority, with what reversibility, and with what non-repudiation properties." None of the existing types capture the *running stack of identities* — and that is the unit operators must audit, sign, and respond to in an agentic-AI incident. + +ASBOM is expressible entirely in CycloneDX 1.7 primitives (`compositions`, `formulation.runtimeTopology`, `declarations`, `services.trustZone`, `components.type=machine-learning-model`/`platform`/`application`) with one schema addition — the `principal` block proposed in [`principal.md`](./principal.md). + +## The four orthogonal axes of `principal` (summary) + +Today `services.authenticated: bool` + `services.trustZone: string` collapses four orthogonal axes into a single boolean. The proposed `principal` block separates them: + +1. **Attribution mode** — `none`, `delegated-ambient` (skill uses host's `gh` auth), `delegated-obo` (RFC 8693 token exchange), `pat` (long-lived bearer), `service-principal`, `agent-account-os` (e.g., Win11 Agent Workspace). Determines who the downstream audit log records as actor. +2. **Credential storage** — `none` / `in-memory` / `env-var` / `file` / `os-keychain` / `hardware-bound` / `broker`. Determines exfiltration and off-device replay risk. +3. **Credential lifetime** — `session` / `short-lived-oauth` / `long-lived-pat` / `perpetual`. Determines duration of compromise. +4. **Consent binding** — `none` / `per-grant` / `per-session` / `per-action`. Determines whether a prompt-injected agent action is also a consented action. + +Real systems mix these axes — e.g., an MCP server can run as a service principal (axis 1) holding short-lived OAuth tokens (axis 3) stored in OS keychain (axis 2) with per-action consent (axis 4). Conflating them loses the information operators need. + +See [`principal.md`](./principal.md) for the full enumeration, JSON Schema fragment, backwards-compatibility analysis, and worked diff examples that show two MCP servers identical under today's schema (`authenticated: true, trustZone: internal`) but with wildly different governance postures once `principal` is populated. + +## Open questions for the Working Group + +The same four questions as the discussion thread, restated for PR review: + +1. **Framing.** Is ASBOM the right framing, or should this land as a *profile* on top of existing BOM types? My read: new BOM type, because the domain is distinct enough that operators need a recognisable category — but happy to be argued out of it. The party model in [#930](https://github.com/CycloneDX/specification/pull/930) creates an alternative path where ASBOM could land as a coordinated party + composition + principal pattern rather than a top-level type. +2. **Axes.** Is the four-axis decomposition for `principal` the right grain, or are some of these axes better collapsed / expanded? Of the four, attribution-mode is the load-bearing one; consent-binding is the most novel. +3. **Attachment.** Where should `principal` attach — `component` only, `service` only, both, or via `definitions` for reuse? The draft attaches to both; in v2.0 modular form it would naturally become a referenceable model. +4. **Home.** If accepted, should the ASBOM specification live in this repo (as a capability page / authoritative guide) or as a downstream maintained spec? + +I am happy to split this into smaller PRs (`principal` field alone, ASBOM capability brand alone, axes as separate enums) if that is easier to land incrementally. + +## Related + +- Discussion: [CycloneDX/specification#940](https://github.com/CycloneDX/specification/discussions/940) +- Adjacent in-flight work: [CycloneDX/specification#930](https://github.com/CycloneDX/specification/pull/930) (party model / modular v2.0) +- Blog post motivating the broader argument: [The registry is the control plane](https://hupfauer.one/posts/the-registry-is-the-control-plane/) +- Working drafts and reference repo: [mhupfauer/asbom](https://github.com/mhupfauer/asbom) +- Reference implementation seed for policy-aware skill installation (the natural ASBOM producer): [vercel-labs/skills#1254](https://github.com/vercel-labs/skills/pull/1254) diff --git a/proposals/asbom/example-claude-code-session.cdx.json b/proposals/asbom/example-claude-code-session.cdx.json new file mode 100644 index 00000000..d2eabb12 --- /dev/null +++ b/proposals/asbom/example-claude-code-session.cdx.json @@ -0,0 +1,368 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.7", + "serialNumber": "urn:uuid:6f1b9c08-7e9a-4f7a-9b6e-3ad7d6a4f1d2", + "version": 1, + "metadata": { + "timestamp": "2026-05-25T19:42:11Z", + "lifecycles": [{ "phase": "operations" }], + "tools": { + "components": [ + { + "type": "application", + "name": "npx-skills", + "version": "0.3.0+policy-rfc", + "supplier": { "name": "vercel-labs (fork: mhupfauer)" } + } + ] + }, + "manufacturer": { + "name": "Example Corp", + "url": ["https://example.corp"] + }, + "properties": [ + { "name": "cdx:bomType", "value": "asbom" }, + { "name": "cdx:asbom:version", "value": "0.1" } + ] + }, + + "components": [ + { + "type": "machine-learning-model", + "bom-ref": "comp:model:claude-opus-4-7", + "name": "claude-opus-4-7", + "version": "claude-opus-4-7-2026-04-30", + "supplier": { "name": "Anthropic" }, + "modelCard": { + "bom-ref": "modelcard:claude-opus-4-7", + "modelParameters": { + "task": "text-generation", + "architectureFamily": "transformer" + }, + "considerations": { + "useCases": ["general-purpose assistant", "code generation", "agentic tool use"], + "ethicalConsiderations": [ + { "name": "Usage policy", "description": "Subject to Anthropic Acceptable Use Policy." } + ] + } + } + }, + + { + "type": "application", + "bom-ref": "comp:harness:claude-code-1.2.0", + "name": "claude-code", + "version": "1.2.0", + "supplier": { "name": "Anthropic" }, + "hashes": [ + { "alg": "SHA-256", "content": "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678901234567890abcdef012345" } + ], + "properties": [ + { "name": "cdx:asbom:role", "value": "harness" }, + { "name": "cdx:asbom:harness:vendor", "value": "anthropic" } + ], + "principal": { + "bom-ref": "principal:harness", + "mode": "agent-account-os", + "subject": { + "kind": "agent-os-account", + "identifier": "WIN-EXAMPLE\\agent_claude_code", + "ref": "comp:runtime:win11-agent-workspace" + }, + "provider": { "name": "local-os" }, + "scopes": [], + "credential": { + "storage": "none", + "lifetime": "session", + "replayProtection": "none", + "rotation": "none" + }, + "consent": { "binding": "per-session", "mechanism": "agent-workspace-enable" }, + "attestation": { "level": "os-attested", "notes": "Intune device-compliance posture available." } + } + }, + + { + "type": "application", + "bom-ref": "comp:skill:salesforce-export-1.4.2", + "name": "vendor.salesforce-export", + "version": "1.4.2", + "supplier": { "name": "vendor.example" }, + "hashes": [ + { "alg": "SHA-256", "content": "7b4f8e2c0a1d9b6e3f5c8a2d4e6f8013579bdf2468ace0123456789abcdef0123" } + ], + "externalReferences": [ + { "type": "vcs", "url": "https://github.com/vendor-example/salesforce-export-skill" } + ], + "properties": [ + { "name": "cdx:asbom:role", "value": "skill" }, + { "name": "cdx:asbom:skill:format", "value": "agentskills.io/0.1" }, + { "name": "cdx:asbom:skill:source", "value": "https://skills.internal.example.corp/vendor.salesforce-export@1.4.2" }, + { "name": "cdx:asbom:skill:digest", "value": "sha256:7b4f8e2c0a1d9b6e3f5c8a2d4e6f8013579bdf2468ace0123456789abcdef0123" } + ], + "principal": { + "bom-ref": "principal:skill", + "mode": "delegated-ambient", + "subject": { + "kind": "agent-os-account", + "identifier": "WIN-EXAMPLE\\agent_claude_code", + "ref": "comp:harness:claude-code-1.2.0" + }, + "provider": { "name": "local-os" }, + "scopes": [], + "credential": { "storage": "none", "lifetime": "session" }, + "consent": { "binding": "none" }, + "attestation": { "level": "os-attested", "notes": "Inherits harness OS attribution; no separate identity." } + } + }, + + { + "type": "platform", + "bom-ref": "comp:runtime:win11-agent-workspace", + "name": "Windows 11 Agent Workspace", + "version": "10.0.26100.7344", + "supplier": { "name": "Microsoft" }, + "properties": [ + { "name": "cdx:asbom:role", "value": "runtime" }, + { "name": "cdx:asbom:runtime:kind", "value": "agent-os-account" }, + { "name": "cdx:asbom:runtime:isolation", "value": "os-account+acl+app-container" }, + { "name": "cdx:asbom:runtime:hostFolders", "value": "Documents Downloads Desktop Music Pictures Videos" } + ], + "principal": { + "bom-ref": "principal:runtime", + "mode": "agent-account-os", + "subject": { + "kind": "agent-os-account", + "identifier": "WIN-EXAMPLE\\agent_claude_code" + }, + "provider": { "name": "local-os" }, + "scopes": ["files:six-known-folders:per-prompt"], + "credential": { "storage": "none", "lifetime": "perpetual" }, + "consent": { "binding": "per-action", "mechanism": "windows-file-access-consent" }, + "attestation": { "level": "os-attested" } + } + } + ], + + "services": [ + { + "bom-ref": "svc:api:anthropic", + "name": "anthropic-messages-api", + "version": "2024-10-22", + "provider": { "name": "Anthropic" }, + "endpoints": ["https://api.anthropic.com/v1/messages"], + "authenticated": true, + "x-trust-boundary": true, + "trustZone": "vendor-saas", + "data": [ + { "flow": "bi-directional", "classification": "confidential" } + ], + "properties": [ + { "name": "cdx:asbom:role", "value": "external-api" } + ] + }, + + { + "bom-ref": "svc:mcp:salesforce-mcp-0.9.1", + "name": "salesforce-mcp", + "version": "0.9.1", + "provider": { "name": "vendor.example" }, + "endpoints": ["https://salesforce-mcp.internal.example.corp/mcp"], + "authenticated": true, + "x-trust-boundary": true, + "trustZone": "internal", + "data": [ + { "flow": "bi-directional", "classification": "confidential" } + ], + "properties": [ + { "name": "cdx:asbom:role", "value": "mcp-server" }, + { "name": "cdx:asbom:mcp:transport", "value": "streamable-http" } + ], + "principal": { + "bom-ref": "principal:mcp", + "mode": "delegated-obo", + "subject": { + "kind": "compound-user-and-client", + "identifier": "alice@example.corp + client:salesforce-mcp" + }, + "provider": { + "name": "salesforce", + "issuer": "https://login.salesforce.com" + }, + "scopes": ["api", "refresh_token", "id"], + "credential": { + "storage": "os-keychain", + "lifetime": "short-lived-oauth", + "replayProtection": "dpop-bound", + "rotation": "automatic" + }, + "consent": { "binding": "per-grant", "mechanism": "oauth-authorize" }, + "attestation": { "level": "idp-attested" } + } + }, + + { + "bom-ref": "svc:api:salesforce", + "name": "salesforce-api", + "endpoints": ["https://example.my.salesforce.com/services/data"], + "authenticated": true, + "x-trust-boundary": true, + "trustZone": "vendor-saas", + "data": [ + { "flow": "bi-directional", "classification": "confidential" } + ], + "properties": [ + { "name": "cdx:asbom:role", "value": "external-api" } + ] + } + ], + + "dependencies": [ + { "ref": "comp:harness:claude-code-1.2.0", "dependsOn": ["comp:model:claude-opus-4-7", "comp:runtime:win11-agent-workspace", "svc:api:anthropic"] }, + { "ref": "comp:skill:salesforce-export-1.4.2", "dependsOn": ["comp:harness:claude-code-1.2.0", "svc:mcp:salesforce-mcp-0.9.1"] }, + { "ref": "svc:mcp:salesforce-mcp-0.9.1", "dependsOn": ["svc:api:salesforce"] } + ], + + "compositions": [ + { + "bom-ref": "comp:asbom:claude-code-with-salesforce-skill", + "aggregate": "complete", + "assemblies": [ + "comp:model:claude-opus-4-7", + "comp:harness:claude-code-1.2.0", + "comp:skill:salesforce-export-1.4.2", + "comp:runtime:win11-agent-workspace", + "svc:api:anthropic", + "svc:mcp:salesforce-mcp-0.9.1", + "svc:api:salesforce" + ], + "signature": { + "algorithm": "ES256", + "publicKey": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }, + "value": "" + } + } + ], + + "formulation": [ + { + "bom-ref": "formula:claude-code-session-2026-05-25", + "workflows": [ + { + "bom-ref": "wf:session-bootstrap", + "uid": "session-2026-05-25-9b6e3ad7", + "name": "Agent session bootstrap and binding", + "taskTypes": ["build", "deploy"], + "tasks": [ + { + "bom-ref": "task:install-skill", + "uid": "task:install-skill", + "name": "Install vendor.salesforce-export@1.4.2 from internal registry", + "resourceReferences": [ + { "ref": "comp:skill:salesforce-export-1.4.2" } + ] + }, + { + "bom-ref": "task:bind-mcp", + "uid": "task:bind-mcp", + "name": "Bind salesforce-mcp with delegated-obo flow", + "resourceReferences": [ + { "ref": "svc:mcp:salesforce-mcp-0.9.1" }, + { "ref": "principal:mcp" } + ] + }, + { + "bom-ref": "task:start-session", + "uid": "task:start-session", + "name": "Start agent session in Agent Workspace", + "resourceReferences": [ + { "ref": "comp:harness:claude-code-1.2.0" }, + { "ref": "comp:runtime:win11-agent-workspace" } + ] + } + ], + "runtimeTopology": [ + { "ref": "comp:harness:claude-code-1.2.0", "dependsOn": ["comp:runtime:win11-agent-workspace"] }, + { "ref": "comp:skill:salesforce-export-1.4.2", "dependsOn": ["comp:harness:claude-code-1.2.0"] }, + { "ref": "svc:mcp:salesforce-mcp-0.9.1", "dependsOn": ["comp:harness:claude-code-1.2.0"] } + ] + } + ] + } + ], + + "declarations": { + "assessors": [ + { + "bom-ref": "assessor:fleet-admin", + "thirdParty": true, + "organization": { "name": "Example Corp Security Engineering" } + }, + { + "bom-ref": "assessor:registry-operator", + "thirdParty": true, + "organization": { "name": "Example Corp Skills Registry" } + } + ], + "attestations": [ + { + "summary": "Composition conforms to Example Corp Agent Skills Policy v3.", + "assessor": "assessor:fleet-admin", + "map": [ + { + "requirement": "policy:no-pat-credentials-in-env-or-file", + "claims": [ + { + "subjects": ["comp:asbom:claude-code-with-salesforce-skill"], + "predicate": "no component or service in the composition has principal.mode=pat with credential.storage in (env-var, file)" + } + ], + "conformance": { "score": 1.0 } + }, + { + "requirement": "policy:obo-required-for-mcp", + "claims": [ + { + "subjects": ["svc:mcp:salesforce-mcp-0.9.1"], + "predicate": "principal.mode == delegated-obo" + } + ], + "conformance": { "score": 1.0 } + } + ], + "signature": { + "algorithm": "ES256", + "publicKey": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }, + "value": "" + } + }, + { + "summary": "Skill artifact was installed via the internal policy-aware registry; source and digest verified.", + "assessor": "assessor:registry-operator", + "map": [ + { + "requirement": "policy:internal-mirror-only", + "claims": [ + { + "subjects": ["comp:skill:salesforce-export-1.4.2"], + "predicate": "skill source = https://skills.internal.example.corp/* AND digest matches mirror record" + } + ], + "conformance": { "score": 1.0 } + } + ], + "signature": { + "algorithm": "ES256", + "publicKey": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }, + "value": "" + } + } + ] + }, + + "signature": { + "algorithm": "ES256", + "publicKey": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }, + "value": "" + } +} diff --git a/proposals/asbom/principal.md b/proposals/asbom/principal.md new file mode 100644 index 00000000..95b7142d --- /dev/null +++ b/proposals/asbom/principal.md @@ -0,0 +1,312 @@ +# Proposal: First-class `principal` block for components and services + +**Status:** Draft v0.1 +**Target:** CycloneDX 1.8 (or 1.7.x point release) +**Applies to:** `components`, `services` +**Author:** Markus Hupfauer +**Date:** 2026-05-25 + +## Summary + +Add an optional `principal` object to `components` and `services` that describes the runtime identity under which the component executes, the credential mechanism that backs that identity, and the non-repudiation properties of the resulting audit trail. + +The field is the schema enabler for a proposed new BOM type — **[Agentic-System Bill of Materials (ASBOM)](./profile.md)** — but is itself generally useful for any composed system where components execute under distinct identities (CI pipelines with secrets, serverless function chains, multi-tenant SaaS components, etc.). The motivating domain is agentic AI, where the gap is most acute and the operator pain is most concrete; the primitive is not AI-specific. + +Today CycloneDX models *what a service exposes* (`services.endpoints`, `services.authenticated: bool`, `services.trustZone: string`) but not *what identity the service executes under* or *what credential it presents to downstream systems*. For agentic systems — where components are increasingly distinguished by *whose authority they act with* rather than by what they technically do — this is the load-bearing missing field. Two skills that ostensibly "use GitHub" have radically different blast radii depending on whether they hold a long-lived PAT, an ambient CLI session, an OAuth on-behalf-of token, or a service-principal identity. The schema must say which. + +## The gap, concretely + +`services.authenticated: true` collapses every meaningful distinction into one boolean. In practice the distinctions that matter for governance, incident response, and non-repudiation are not whether *some* authentication happens, but **who the audit-log actor is**, **what kind of credential is being presented**, **where that credential is stored**, and **whether a human consent event is bound to its use**. These are four orthogonal axes. No existing CycloneDX field carries any of them. + +## The four orthogonal axes + +Most schemas (and most thinking) collapse these into "auth method." They are not the same thing and must be modeled separately, because real systems mix them: e.g., an MCP server can run as a service principal (axis 1) holding short-lived OAuth tokens (axis 3) stored in OS keychain (axis 2) with per-action consent (axis 4). Conflating them loses information operators need. + +### Axis 1 — Attribution mode (who the audit log records as the actor) + +| Value | Meaning | Example | +|---|---|---| +| `none` | Component takes no credentials, calls no authenticated downstream. | Pure-compute skill that only manipulates in-context text. | +| `delegated-ambient` | Component uses whatever credentials are already loaded in the host environment. The downstream system attributes the action to the human user. No per-component consent event ties this specific component's use to the user. | A Claude Code skill that shells out to `gh pr create` and uses the existing `gh` auth. A skill that reads `OPENAI_API_KEY` from env. A skill that reads `~/.aws/credentials`. | +| `delegated-obo` | Component has its own registered client identity at the identity provider. The host or a broker exchanges the user's token for a downstream token bound to *both* the user and the component's client identity (RFC 8693 / Microsoft Graph on-behalf-of). Downstream audit logs show both principals. | A well-behaved MCP server that registers as an OAuth client and uses token exchange to call Microsoft Graph on behalf of the signed-in user. | +| `pat` | Component holds a long-lived bearer token (personal access token, API key) attributed to a human's account but used by the component. The downstream system attributes actions to the token's owning human; nothing in the audit log distinguishes a human-typed call from a component-issued one. | A skill given a GitHub PAT via `GH_TOKEN`. A skill given a Salesforce session token. | +| `service-principal` | Component has its own dedicated machine identity, distinct from any human user. Downstream attributes actions to the machine identity directly. | An MCP server using an Azure managed identity. A GitHub App installation. An AWS IAM role assumed by an agent. | +| `agent-account-os` | Component runs under a separate OS user account whose actions are OS-attestable as belonging to the agent, not the human user. Composes with the above: an OS agent account may *itself* hold delegated-obo or service-principal credentials at the application layer. | A skill running inside Windows 11 Agent Workspace, attributed at the OS layer to the agent's Windows account. | + +The crucial property carried by this axis is **non-repudiation between the user and the component**. `delegated-ambient` and `pat` provide *none* — every downstream record is indistinguishable from the user acting directly. `delegated-obo`, `service-principal`, and `agent-account-os` provide it in different ways and at different layers (IdP-attested, IdP-attested, OS-attested respectively). + +### Axis 2 — Credential storage (where the secret lives) + +| Value | Meaning | +|---|---| +| `none` | No credential held by the component. | +| `in-memory` | Credential lives only in the component's process memory for the duration of a session; never written to disk. | +| `env-var` | Credential is exposed to the component via a process environment variable. Inherits the host's process-environment exposure. | +| `file` | Credential is read from a plaintext file at a known path (e.g., `~/.aws/credentials`, `~/.config/gh/hosts.yml`). | +| `os-keychain` | Credential is retrieved from an OS-mediated secret store: macOS Keychain, Windows Credential Manager / DPAPI, Linux libsecret/Secret Service. Retrieval is gated by the OS and is locally auditable. | +| `hardware-bound` | Credential is sealed to hardware (TPM, Secure Enclave, HSM, FIDO2 authenticator) and cannot be extracted; downstream proofs may be unforgeable off-device. | +| `broker` | Credential is held by a separate broker process (e.g., systemd-credentials, browser credential manager, cloud workload identity sidecar) and presented to the component on demand. | + +The crucial property carried by this axis is **exfiltration and replay risk**. A `pat` stored in `env-var` is one shell-injection away from being a permanent compromise; the same `pat` stored in `hardware-bound` form cannot leave the device. The attribution mode (axis 1) tells you *who gets blamed*; the storage axis tells you *how long a stolen credential keeps working from somewhere else*. + +### Axis 3 — Credential lifetime + +| Value | Meaning | +|---|---| +| `none` | No credential. | +| `session` | Credential is bound to a single host session (e.g., a fresh OAuth grant per CLI session). | +| `short-lived-oauth` | OAuth access token with TTL on the order of minutes to an hour, typically with refresh. | +| `long-lived-pat` | Bearer token with TTL on the order of months or longer, often non-rotating. | +| `perpetual` | No effective expiry until explicit revocation (machine key, classic API key). | + +The crucial property carried by this axis is **the duration of compromise from a single exfiltration**. Short-lived tokens limit the blast radius even if storage is weak; long-lived PATs make storage decisions load-bearing. + +### Axis 4 — Consent binding + +| Value | Meaning | +|---|---| +| `none` | No human consent event is bound to the component's credential use. | +| `per-grant` | A single human consent event at credential issuance authorizes all future use until revocation (the standard OAuth grant model). | +| `per-session` | Human consent is required at the start of each host session in which the component runs. | +| `per-action` | An explicit human consent prompt gates every privileged use (UAC, GitHub fine-grained interactive elevation, "approve this action" patterns in Claude Code and Cursor). | + +The crucial property carried by this axis is **whether a prompt-injected agent action is also a consented action**. `none` and `per-grant` mean a single past consent licenses arbitrary future use; `per-action` means each high-stakes call requires a fresh human signal that the agent cannot synthesize. + +## Why all four axes are needed + +A few worked examples to motivate keeping them separate: + +- **Skill A**: uses `delegated-ambient` GitHub CLI auth, credential stored in `file` (`~/.config/gh/hosts.yml`), `long-lived-pat` underlying, `consent: none`. → Indistinguishable from the user at GitHub. Stolen file = persistent compromise. +- **Skill B**: same downstream effect, but uses `delegated-obo` with a registered OAuth client, `short-lived-oauth` in `os-keychain`, `consent: per-grant`. → GitHub audit log shows both Skill B and the user. Stolen credential expires in 1h. +- **Skill C**: holds an Azure `service-principal`, `hardware-bound` to TPM, `perpetual` until rotated, `consent: none`. → Azure logs attribute everything to the agent's machine identity directly. Credential cannot be exfiltrated off the device. +- **Skill D**: runs as an `agent-account-os` user under Windows Agent Workspace, but the *application-layer* credential it presents to a SaaS API is a `pat` stored in `env-var`. → OS audit logs distinguish agent from user; the SaaS API does not. + +All four skills would have `services.authenticated: true` today. Operators cannot tell them apart from a BOM. Under this proposal each has a distinct `principal` object, and an incident-response query like "did any agent-installed component on this host hold a long-lived bearer token in plaintext that could have been exfiltrated by the skill installed at T-30 days" becomes a structured query rather than a forensic dig. + +## Proposed schema fragment + +Attached to both `component` and `service` definitions (and reusable elsewhere via `$ref`): + +```jsonc +{ + "principal": { + "$ref": "#/definitions/principal" + } +} +``` + +```jsonc +"principal": { + "type": "object", + "title": "Principal", + "description": "Describes the runtime identity, credential mechanism, and non-repudiation properties under which a component or service executes.", + "additionalProperties": false, + "properties": { + "bom-ref": { + "$ref": "#/definitions/refType", + "description": "Identifier so this principal can be referenced from attestations or compositions." + }, + "mode": { + "type": "string", + "enum": [ + "none", + "delegated-ambient", + "delegated-obo", + "pat", + "service-principal", + "agent-account-os" + ], + "description": "Attribution mode. Determines who the downstream audit log records as the actor and whether the component is distinguishable from the human user at the identity provider." + }, + "subject": { + "type": "object", + "description": "The identity actions are attributed to at the downstream system.", + "properties": { + "kind": { + "type": "string", + "enum": ["user", "agent-os-account", "service-principal", "token-owner", "compound-user-and-client"] + }, + "identifier": { + "type": "string", + "description": "Human-readable identifier of the subject (e.g., user email, service-principal name, OS account name). Should not include secrets." + }, + "ref": { + "$ref": "#/definitions/refLinkType", + "description": "Optional reference to a component, service, or organizationalContact representing the subject." + } + } + }, + "provider": { + "type": "object", + "description": "The identity provider or authority that issues and attests credentials for this principal.", + "properties": { + "name": { "type": "string", "description": "e.g., 'github.com', 'entra-id', 'okta', 'aws-iam', 'local-os', 'salesforce'." }, + "uri": { "type": "string", "format": "iri-reference" }, + "issuer": { "type": "string", "description": "OAuth/OIDC issuer URL if applicable." } + } + }, + "scopes": { + "type": "array", + "items": { "type": "string" }, + "description": "Scope strings as the provider expresses them. Free-form to accommodate provider-specific conventions (OAuth scopes, IAM action names, GitHub fine-grained permissions, etc.)." + }, + "credential": { + "type": "object", + "description": "Properties of the credential material itself, independent of the attribution mode.", + "properties": { + "storage": { + "type": "string", + "enum": ["none", "in-memory", "env-var", "file", "os-keychain", "hardware-bound", "broker"], + "description": "Where the credential lives at rest." + }, + "lifetime": { + "type": "string", + "enum": ["none", "session", "short-lived-oauth", "long-lived-pat", "perpetual"], + "description": "Effective duration of validity from issuance." + }, + "replayProtection": { + "type": "string", + "enum": ["none", "single-use", "dpop-bound", "mtls-bound", "hardware-bound"], + "description": "Whether a stolen credential is replayable off the originating device or session." + }, + "rotation": { + "type": "string", + "enum": ["none", "manual", "automatic"], + "description": "Operational rotation cadence, if applicable." + } + } + }, + "consent": { + "type": "object", + "description": "Whether and how a human consent event is bound to credential use.", + "properties": { + "binding": { + "type": "string", + "enum": ["none", "per-grant", "per-session", "per-action"] + }, + "mechanism": { + "type": "string", + "description": "Free-text identifier of the consent surface, e.g., 'oauth-authorize', 'uac-prompt', 'claude-code-approval', 'github-fine-grained-elevation'." + } + } + }, + "attestation": { + "type": "object", + "description": "Where, if anywhere, the principal's actions are independently attestable.", + "properties": { + "level": { + "type": "string", + "enum": ["none", "partial", "idp-attested", "os-attested", "idp-and-os-attested"] + }, + "notes": { "type": "string" } + } + }, + "properties": { + "$ref": "#/definitions/properties" + } + }, + "required": ["mode"] +} +``` + +## Backwards compatibility + +Pure addition. `principal` is optional on both `component` and `service`. Existing `services.authenticated` and `services.trustZone` remain. Tooling that does not understand `principal` continues to function; tooling that does can lift more precise governance signals from BOMs that include it. + +The proposal deliberately does *not* deprecate `services.authenticated` — they cover slightly different intents (the boolean is a property of the service surface; the principal is a property of the executing identity). A consumer SHOULD treat `principal.mode != "none"` as implying `services.authenticated: true` for the service it is attached to. + +## Why now + +Three converging vectors: + +1. **Agent skills as a distribution format.** SKILL.md ([agentskills.io](https://agentskills.io)) and `npx skills` (vercel-labs/skills) made agent capabilities installable artifacts. CycloneDX is the natural inventory; today it cannot say what identity an installed skill executes under. +2. **MCP as the protocol for cross-process agent tools.** MCP servers vary widely in their identity discipline — some implement RFC 8693 token exchange, some forward bearer tokens unchanged, some hold long-lived service tokens. The `principal` block lets a BOM distinguish them. +3. **OS-level agent identity (Windows 11 Agent Workspace).** Microsoft is the first major platform to provide an OS-attested agent principal distinct from the user. Without a schema field for `agent-account-os`, BOMs cannot represent the security property that the OS just made available. + +## Worked-diff example (today vs. proposed) + +Today, an MCP server appears in a BOM roughly as: + +```jsonc +{ + "type": "service", + "bom-ref": "svc:salesforce-mcp", + "name": "salesforce-mcp", + "version": "0.9.1", + "endpoints": ["https://salesforce-mcp.internal/mcp"], + "authenticated": true, + "trustZone": "internal" +} +``` + +Under this proposal, two MCP servers that today look identical become distinguishable: + +```jsonc +{ + "type": "service", + "bom-ref": "svc:salesforce-mcp-good", + "name": "salesforce-mcp", + "version": "0.9.1", + "endpoints": ["https://salesforce-mcp.internal/mcp"], + "authenticated": true, + "trustZone": "internal", + "principal": { + "mode": "delegated-obo", + "subject": { "kind": "compound-user-and-client", "identifier": "alice@example.com + client:salesforce-mcp" }, + "provider": { "name": "salesforce", "issuer": "https://login.salesforce.com" }, + "scopes": ["api", "refresh_token", "id"], + "credential": { + "storage": "os-keychain", + "lifetime": "short-lived-oauth", + "replayProtection": "dpop-bound", + "rotation": "automatic" + }, + "consent": { "binding": "per-grant", "mechanism": "oauth-authorize" }, + "attestation": { "level": "idp-attested" } + } +} +``` + +```jsonc +{ + "type": "service", + "bom-ref": "svc:salesforce-mcp-bad", + "name": "salesforce-mcp-shim", + "version": "0.1.0", + "endpoints": ["https://salesforce-mcp-shim.internal/mcp"], + "authenticated": true, + "trustZone": "internal", + "principal": { + "mode": "pat", + "subject": { "kind": "token-owner", "identifier": "alice@example.com" }, + "provider": { "name": "salesforce" }, + "scopes": ["full"], + "credential": { + "storage": "env-var", + "lifetime": "perpetual", + "replayProtection": "none", + "rotation": "manual" + }, + "consent": { "binding": "none" }, + "attestation": { "level": "none" } + } +} +``` + +Same protocol, same target system, same trust zone — wildly different governance posture, now machine-readable. + +## Open questions + +1. Should `principal` be reusable via the `definitions` collection (so multiple components can reference one principal) or always inlined? Suggest: support both via `bom-ref` + `refLinkType`. +2. Are `consent.binding` and `credential.lifetime` better expressed as enums (as proposed) or as durations and free-text? Suggest: enums for interoperability; properties bag for refinements. +3. Where does `principal` attach for *workflows*? Probably on the `task` (which already has `runtimeTopology`) rather than on the `formula`. To be clarified once the profile draft validates against worked examples. + +## Prior art + +- OAuth 2.0 Token Exchange (RFC 8693) +- Microsoft on-behalf-of flow +- SPIFFE / SPIRE workload identity +- in-toto attestation predicates (the `principal` block is conceptually a predicate) +- Sigstore Fulcio identity bindings + +None of these standards is in conflict with this proposal; the `principal` block is the BOM-side surface that lets a consumer reason about which of them is actually in use. diff --git a/proposals/asbom/profile.md b/proposals/asbom/profile.md new file mode 100644 index 00000000..8bc4b5f8 --- /dev/null +++ b/proposals/asbom/profile.md @@ -0,0 +1,295 @@ +# Agentic-System Bill of Materials (ASBOM) + +**Status:** Draft v0.1 +**Type:** Proposed new CycloneDX BOM type, alongside SBOM, ML-BOM, SaaSBOM, CBOM, HBOM, OBOM, MBOM +**Target spec:** CycloneDX 1.7 (with proposed [`principal`](./principal.md) field addition) +**Author:** Markus Hupfauer +**Date:** 2026-05-25 + +## 1. Scope + +This document specifies **Agentic-System Bill of Materials (ASBOM)** — a CycloneDX BOM type for describing the runtime composition of an *agentic system*: a bounded set of one or more AI models, a host application ("harness") that drives them, zero or more installed skills, zero or more invoked MCP servers, a runtime/sandbox environment, and the identities under which each of those components executes. + +ASBOM stands alongside the existing CycloneDX BOM types — SBOM (software), ML-BOM (model + training), SaaSBOM (service surfaces), CBOM (cryptographic assets) — and inventories the unit that none of them currently capture: the *running composition*. Where ML-BOM answers "what is this model and how was it trained," and SaaSBOM answers "what services and data flows exist," ASBOM answers "what composition is executing *right now*, under whose authority, with what reversibility, and with what non-repudiation properties." + +ASBOM is expressible entirely in CycloneDX 1.7 primitives with one schema addition (the [`principal`](./principal.md) block). It defines: + +- a component taxonomy mapping agentic-system parts to existing CycloneDX `component` and `service` types, +- a composition pattern using `compositions` and `formulation`, +- attestation rules using `declarations`, +- signing requirements, +- and naming conventions for the `properties` bag. + +Consumers that have not adopted the `principal` proposal can substitute a `properties` namespace as described in §5.4. + +## 2. Motivation + +CycloneDX ML-BOM describes the *model*. CycloneDX SaaSBOM describes a *service surface*. Neither describes the **running composition** that determines what an agentic system can actually do: which model, loaded in which harness, with which skills installed, executing in which runtime, under which credentials. + +That composition is the unit operators must audit, sign, attest, and respond to in an incident. Today it has no home in the schema. ASBOM gives it one without inventing a new format — reusing CycloneDX 1.7 primitives plus one schema addition (`principal`) for runtime identity. + +## 3. Conformance + +A document conforms to ASBOM v0.1 if: + +1. It is a valid CycloneDX 1.7 BOM. +2. Its `metadata.properties` contains `cdx:bomType = "asbom"` and `cdx:asbom:version = "0.1"`. +3. It contains at least one `composition` of `aggregate: complete` (or `complete_with_exceptions`) that enumerates the agentic system's runtime parts. +4. Each component or service that *executes code under some identity* carries either a `principal` block (preferred) or a `cdx:asbom:principal:*` properties namespace (fallback; see §5.4). + +A document MAY include additional CycloneDX content unrelated to the agentic system; only the conforming composition needs to satisfy ASBOM. + +## 4. Component taxonomy + +The profile maps agentic-system parts to existing CycloneDX 1.7 types as follows. Where multiple types would be defensible, the profile picks one to keep producers and consumers aligned. + +### 4.1 Model + +CycloneDX `component.type = "machine-learning-model"` with a `modelCard`. No new convention. The model is named by its vendor and version. If the model is consumed via API rather than executed locally, the `component` represents the model itself and a *separate* `service` represents the API endpoint (see §4.5). + +### 4.2 Harness (agent host application) + +The CLI, IDE, or desktop application that loads the model, loads skills, mediates tool calls, and presents the UI to the user. Modeled as `component.type = "application"` with: + +- `properties."cdx:asbom:role" = "harness"` +- `properties."cdx:asbom:harness:vendor"` (e.g., `anthropic`, `cursor`, `github`) + +Examples: Claude Code CLI, Cursor, Gemini CLI, the GitHub CLI in `gh copilot` mode. + +The harness MUST declare a `principal` describing the OS-level identity it runs under. + +### 4.3 Skill + +An installed agent capability — a SKILL.md package, an Anthropic plugin, a Cursor extension, etc. Modeled as `component.type = "application"` with: + +- `properties."cdx:asbom:role" = "skill"` +- `properties."cdx:asbom:skill:format"` (e.g., `agentskills.io/0.1`, `anthropic-plugin/1.0`) +- `properties."cdx:asbom:skill:source"` (URL or registry URI the skill was installed from) +- `properties."cdx:asbom:skill:digest"` (full content hash; SHOULD use `hashes` array as well) + +A skill MAY declare a `principal`. Skills that execute under the harness's ambient identity SHOULD set `principal.mode = "delegated-ambient"` and `principal.subject.ref` to the harness's `bom-ref` — making the inheritance explicit rather than implicit. + +### 4.4 MCP server + +An MCP server invoked by the harness or by a skill. Modeled as `service`, not `component`, even when colocated with the harness, because MCP is fundamentally a protocol-mediated service surface. Properties: + +- `properties."cdx:asbom:role" = "mcp-server"` +- `properties."cdx:asbom:mcp:transport"` (e.g., `stdio`, `http+sse`, `streamable-http`) +- `endpoints` MUST list every endpoint the server exposes; if `stdio`, use a synthetic `stdio://` URI naming the binary. + +An MCP server MUST declare a `principal`. This is the field that distinguishes a token-passthrough shim from a properly OBO-flow server (see [the principal proposal §3](./principal.md#axis-1--attribution-mode-who-the-audit-log-records-as-the-actor)). + +### 4.5 External AI / data services + +Any external service the model, harness, skill, or MCP server calls — including the model's own API endpoint when the model is hosted. Modeled as `service` with: + +- `properties."cdx:asbom:role" = "external-api"` +- `services.endpoints` listing the calling endpoint(s) +- `services.data` describing classification and direction +- `services.trustZone` set to a meaningful name (e.g., `public-internet`, `vendor-saas`, `internal`) + +These services SHOULD NOT carry a `principal` block — the principal lives on whichever component or service *calls* them. (Modeling it on both ends is allowed but creates ambiguity; prefer the caller.) + +### 4.6 Runtime environment + +The sandbox, user account, or container under which the harness executes. Modeled as `component.type = "platform"` (1.7 explicitly clarifies that `platform` covers interpreters and runtime hosts). Properties: + +- `properties."cdx:asbom:role" = "runtime"` +- `properties."cdx:asbom:runtime:kind"` — one of: + - `host-user-session` — running in the interactive user's OS session under the user's account + - `agent-os-account` — running under a dedicated OS account (e.g., Windows 11 Agent Workspace) + - `container` — running in a container with declared isolation + - `vm` — running in a VM + - `cloud-managed` — running in a vendor-managed agent runtime (Anthropic Managed Agents, etc.) +- `properties."cdx:asbom:runtime:isolation"` — free text, e.g., `none`, `os-account+acl`, `app-container`, `oci-container:user-namespace`, `hyperv-vm`, `vendor-attested`. + +The runtime component MUST carry a `principal` whose `mode` is one of `agent-account-os`, `service-principal`, or `none`. The `mode = none` value is appropriate for `host-user-session` runtimes whose attribution is the user's existing OS identity rather than a distinct agent principal. + +### 4.7 Credentials and identities + +Credentials are *not* modeled as standalone components. They are modeled as properties of a `principal` block attached to whichever component or service holds them (§4.2–§4.4). This avoids the trap of inventorying secret material directly and keeps the schema's grain at the principal rather than at the credential. + +When the same credential is held by multiple components (e.g., a shared OAuth refresh token), each component's `principal` block describes its own access path to that credential; the credential itself is not a referenceable BOM object. + +## 5. Composition pattern + +### 5.1 The agentic-system composition + +Every conforming BOM MUST contain exactly one `composition` representing the agentic system as a whole: + +```jsonc +{ + "compositions": [ + { + "bom-ref": "comp:asbom:claude-code-with-salesforce-skill", + "aggregate": "complete", + "assemblies": [ + "comp:model:claude-opus-4-7", + "comp:harness:claude-code-1.2.0", + "comp:skill:salesforce-export-1.4.2", + "comp:runtime:macos-user-session", + "svc:mcp:salesforce-mcp-0.9.1" + ] + } + ] +} +``` + +The `aggregate` value MUST be `complete` if the producer asserts the listed assemblies fully describe the runtime composition. Use `complete_with_exceptions` when transitive dependencies of the listed parts are not enumerated; use `incomplete_first_party_only` when only the producer's own components are listed. + +The composition's `signature` (JSF) is what a fleet admin or registry attests when approving the composition. + +### 5.2 Dependencies + +`dependencies` SHOULD record the call relationships among agentic-system parts: + +- harness `depends-on` model and skills +- skills `depends-on` MCP servers they invoke +- MCP servers `depends-on` external APIs they call + +This produces a directed acyclic graph operators can walk for blast-radius queries. + +### 5.3 Formulation: how this composition was assembled + +The agentic system's `formulation` captures the assembly process. The 1.7 release extended formulation to apply to "any referencable object within the BOM, including … the BOM itself," which is exactly the level ASBOM uses. Recommended structure: + +```jsonc +{ + "formulation": [{ + "bom-ref": "formula:claude-code-session-2026-05-25", + "workflows": [{ + "bom-ref": "wf:session-bootstrap", + "uid": "session-2026-05-25-...", + "name": "Agent session bootstrap", + "taskTypes": ["build", "deploy"], + "tasks": [ + { + "uid": "task:load-model", + "name": "Load model via vendor API", + "resourceReferences": [{ "ref": "comp:model:claude-opus-4-7" }, { "ref": "svc:api:anthropic" }] + }, + { + "uid": "task:install-skill", + "name": "Install skill from registry", + "resourceReferences": [{ "ref": "comp:skill:salesforce-export-1.4.2" }], + "inputs": [{ "source": { "ref": "svc:registry:internal-skills" } }] + }, + { + "uid": "task:bind-mcp", + "name": "Bind MCP server with OBO identity", + "resourceReferences": [{ "ref": "svc:mcp:salesforce-mcp-0.9.1" }] + } + ], + "runtimeTopology": [ + { "ref": "comp:harness:claude-code-1.2.0", "dependsOn": ["comp:runtime:macos-user-session"] }, + { "ref": "comp:skill:salesforce-export-1.4.2", "dependsOn": ["comp:harness:claude-code-1.2.0"] }, + { "ref": "svc:mcp:salesforce-mcp-0.9.1", "dependsOn": ["comp:harness:claude-code-1.2.0"] } + ] + }] + }] +} +``` + +The `runtimeTopology` field is the load-bearing one: it lets a consumer reconstruct what was *executing under what* at the moment the BOM was produced, which is precisely the information that disappears between an SBOM's "what is installed" and a SaaSBOM's "what services exist." + +### 5.4 Property-bag fallback for `principal` + +For BOMs targeting consumers that have not adopted the [`principal` proposal](./principal.md), the same fields MAY be expressed under the `cdx:asbom:principal:*` property namespace: + +```jsonc +"properties": [ + { "name": "cdx:asbom:principal:mode", "value": "delegated-obo" }, + { "name": "cdx:asbom:principal:subject:kind", "value": "compound-user-and-client" }, + { "name": "cdx:asbom:principal:subject:identifier", "value": "alice@example.com + client:salesforce-mcp" }, + { "name": "cdx:asbom:principal:provider:name", "value": "salesforce" }, + { "name": "cdx:asbom:principal:provider:issuer", "value": "https://login.salesforce.com" }, + { "name": "cdx:asbom:principal:scopes", "value": "api refresh_token id" }, + { "name": "cdx:asbom:principal:credential:storage", "value": "os-keychain" }, + { "name": "cdx:asbom:principal:credential:lifetime", "value": "short-lived-oauth" }, + { "name": "cdx:asbom:principal:credential:replayProtection", "value": "dpop-bound" }, + { "name": "cdx:asbom:principal:consent:binding", "value": "per-grant" }, + { "name": "cdx:asbom:principal:attestation:level", "value": "idp-attested" } +] +``` + +Producers SHOULD prefer the structured `principal` block once the proposal lands. Consumers SHOULD accept both for the transition period. + +## 6. Attestation via `declarations` + +ASBOM uses CycloneDX 1.7 `declarations` for the third-party-assertion layer. Trust in an ASBOM derives from who signed which assertion about it, not from the artifact's self-description. + +Minimum recommended declarations for a fleet-approved composition: + +| Claim | Asserted by | Evidence | +|---|---|---| +| Composition matches the org's approved-skill policy | Fleet admin (third-party assessor) | Signature over the `composition.bom-ref` and the resolved skill digests | +| Every skill in the composition was installed via the org's policy-aware registry | Registry operator | Registry-side install log referenced by URI | +| MCP servers in the composition use OBO flow, not token passthrough | MCP server vendor | OAuth client registration evidence | +| The runtime principal is OS-attested | OS / endpoint mgmt | Intune/MDM device-compliance posture, EDR runtime trace, etc. | + +Each `declaration` carries: + +- `assessors[].thirdParty: true` for assertions made by parties other than the BOM producer +- `attestations[].map` linking the claim to the relevant `bom-ref` (component, service, or composition) +- A JSF signature over the attestation + +The point is: **a BOM is governance only to the extent that its claims are signed by parties whose authority over the claim is operationally meaningful**. A `principal.mode: delegated-obo` assertion in a self-signed BOM tells the consumer the producer *says* the MCP server does OBO; an attestation signed by the registry operator after running an actual OAuth-flow probe tells the consumer the registry *verified* it. + +## 7. Signing + +Every conforming BOM MUST be signed (JSF, enveloped, at the top level). The signer is the BOM producer. + +A fleet-approved composition SHOULD additionally carry: + +- a signed `composition` (JSF over the composition object) by the fleet admin's signing key +- signed `declarations.attestations[]` for at least the policy-conformance and registry-provenance claims above + +Verification flow at runtime (e.g., in `npx skills`, a managed harness, or a gateway): + +1. Verify the envelope signature against the producer's key. +2. Verify the composition signature against the fleet admin's key from the org's trust policy. +3. For each declared attestation, verify the assessor's signature against the assessor's published key. +4. Refuse the composition if any required attestation is missing or fails. + +## 8. Worked example + +See [`example-claude-code-session.cdx.json`](./example-claude-code-session.cdx.json) for a complete BOM modeling a Claude Code session with one skill, one MCP server, and a Windows 11 Agent Workspace runtime, including the `principal` block on every runtime actor and a fleet-admin attestation over the composition. + +## 9. Implementation guidance + +### 9.1 Producers + +- A policy-aware registry CLI (e.g., `npx skills` with [vercel-labs/skills#1254](https://github.com/vercel-labs/skills/pull/1254)) SHOULD emit a conforming BOM at install time, attaching it to the install log and (optionally) writing it to a stable on-disk path for fleet tooling to read. +- A harness SHOULD emit a conforming BOM at session start, capturing the current runtime composition and any user-consent events that bound the session's `principal` blocks. + +### 9.2 Consumers + +- A fleet-management agent (Intune script, JAMF script, osquery extension) SHOULD verify the BOM signature chain and refuse compositions missing required attestations. +- An MCP gateway SHOULD ingest the conforming BOM and use the declared `principal` to decide whether to require additional consent, route through a token-exchange broker, or refuse the call entirely. +- An incident-response query SHOULD be able to answer, against a corpus of conforming BOMs, "which compositions on which hosts hold a `credential.lifetime: long-lived-pat` with `credential.storage: env-var` or `file`." + +### 9.3 Where this profile does not help yet + +- Runtime data-flow / value provenance (the "no egress after sensitive read" policy from the source blog post) is *not* modeled here. That belongs in the gateway/runtime, not the BOM. The BOM names the principals and trust zones the gateway then enforces against. +- The transitive permission graph beyond the second hop (skill → MCP server → external API → further APIs) is modeled only to the depth the producer can observe. Deep transitive analysis remains an out-of-band concern. + +## 10. Open questions + +1. Should the profile mandate a specific `bom-ref` naming convention (e.g., `comp:role:name:version`)? Suggest: SHOULD, not MUST, with the convention above as the recommended form. +2. How should multi-model compositions be modeled (a session that uses both Opus and Haiku)? Probably as two `machine-learning-model` components both listed in the composition's `assemblies`, with `formulation.workflow.tasks` indicating which task used which model. Worked example to follow. +3. How should ephemeral compositions be expressed — e.g., a tool call that briefly invokes a sub-agent? Probably a nested `workflow` within the same `formulation`. Worked example to follow. + +## Appendix A: Mapping to existing standards + +| Concept | This profile | SBOM (CycloneDX core) | ML-BOM | SaaSBOM | SLSA | in-toto | +|---|---|---|---|---|---|---| +| Inventory of software parts | components | ✓ | ✓ | ✗ | ✗ | ✗ | +| Model card | model component | ✗ | ✓ | ✗ | ✗ | ✗ | +| Service surface | mcp-server / external-api | ✗ | ✗ | ✓ | ✗ | ✗ | +| Runtime composition (what's running together) | composition + formulation.runtimeTopology | partial | ✗ | ✗ | ✗ | partial | +| Build provenance | declarations + formulation | partial | ✗ | ✗ | ✓ | ✓ | +| Runtime identity / principal | `principal` (proposed) | ✗ | ✗ | partial | ✗ | ✓ (predicate) | +| Third-party attestation | declarations | ✓ | ✓ | ✓ | ✓ | ✓ | +| Non-repudiation properties | `principal.attestation` (proposed) | ✗ | ✗ | ✗ | ✗ | partial | + +ASBOM is best understood as the CycloneDX-side wiring that lets an agentic system carry the equivalent of an in-toto principal predicate as an inline schema-validated block, signed alongside everything else in the BOM.