Ledger model v0¶
The ledger model is the hub every reader maps into and every writer maps out of. It is defined as Pydantic models in src/marshal_ledger/model.py. The JSON Schema in schema/marshal-v0.schema.json is generated from them.
Records¶
| Kind | Meaning |
|---|---|
Agent |
The logical identity, such as cfn/hr-resources/HRAgent |
AgentVersion |
One content-addressed version of an agent, read from one source artifact |
Deployment |
A version placed in an environment, as a proposed or shipped event |
Entity |
A shared tool, knowledge source, model, store or other dependency that many agents can reference |
Values and provenance¶
Every scored field is a Value:
scope:
value: write
provenance: derived # declared | attested | annotation | derived | unknown
basis: 'widest action in the IAM role HRAgentActionGroupLambdaRole of executor Lambda BedrockHRFunction: s3:PutObject'
declared: stated in the source artifact.attested: stated by a person in a hand-written definition (Marshal file), not read from the platform.basisnames the file and, where given, who attested it and when.annotation: from a hint attached to the source, such as MCP tool annotations.derived: computed by a documented rule from declared values.basisnames the rule.unknown: not available. The ledger stores no value rather than guess.
What a spec holds¶
| Field | Contents |
|---|---|
display_name, description |
Human-readable identity |
model |
requested model, provider, resolved model (where a gateway is connected), declared generation parameters |
instructions |
Hash of the instruction text, and the text unless hash-only mode is on |
tools[] |
Entity ref, name, description, input and output JSON Schemas, scope, reversible, requires_confirmation |
knowledge[] |
Entity ref, name, description, the source's own type, scope |
permissions |
identity kind, principal, grants held by the agent's identity, access rules for who may use the agent |
extensions |
Platform- or format-specific fields with no portable equivalent, kept intact per platform |
Spec and bindings¶
spec is the environment-independent definition: name, description, model, instructions, tools, knowledge, permissions and platform extensions. bindings holds environment-bound values: target name, workspace host, warehouse IDs, deployed names.
Only spec is hashed, so the same definition in dev and prod is the same version.
Version hash¶
version is sha256: over a canonical JSON form of spec (sorted keys, no whitespace, UTF-8). The hash excludes:
provenanceandbasismetadata. Moving a file, or improving a reader's classification, doesn't create a new version.- Instruction text. Instructions are represented by their own hash, so hash-only mode gives the same version as full mode.
- Absent values (
null, empty lists, empty mappings). Adding an optional field to the model doesn't change existing versions.
Tools, knowledge, grants and access rules are sorted before hashing, so reordering them doesn't create a new version. Lists inside extensions keep their order.
Version store¶
The store keeps each (agent, version hash) once and numbers versions per agent in the order it first saw them: v1, v2, and so on. Sequence numbers are local to a store; the hash is the portable identity.
Every collection adds an event, proposed or shipped, for an environment. Bindings, commit and pipeline run are stored on the event, not on the version. Which version runs where is the most recent shipped event per environment, so re-shipping an older version (a rollback) is recorded as such.
Commands and the store API select versions with:
| Selector | Meaning |
|---|---|
v3 |
Sequence number |
sha256:7c41a9e or 7c41a9e |
Version hash or a unique prefix of at least 7 hex digits |
latest |
The version in the agent's most recent event |
shipped, shipped:prod |
The version most recently shipped, anywhere or to one environment |
proposed, proposed:prod |
The version most recently proposed |
observed, observed:prod |
The version most recently observed live |
marshal diff AGENT compares the latest version with the last shipped one. Diffs are computed on the ledger model, so they read the same on every platform. Changes are grouped by category, permissions and tools first.
Channels, drift and freshness¶
Every event records its channel, which says how far to trust it:
| Channel | Source | Set by |
|---|---|---|
ci |
An authenticated pipeline | The ingest API, for proposed and shipped events |
scheduled |
A scheduled job that exported the platform's live definition | The ingest API, for observed events; marshal collect --channel scheduled locally |
manual |
A person ran a collection or imported a file | marshal collect (the default) |
Clients can't choose ci: the ingest API sets the channel from the authenticated request.
An observed event records what is live on the platform. When it differs from the version last shipped to that environment, the agent has drifted: something changed outside the pipeline, for example an edit in a platform's web UI. Drift clears when the agent is shipped again.
Freshness is the age of the latest shipped or observed event, judged by its channel:
| Channel | Stale after (default) |
|---|---|
manual |
30 days |
scheduled |
7 days (the scheduled export has stopped running) |
ci |
Never by age alone; drift detection covers changes made outside the pipeline |
marshal ls shows freshness per environment, and marshal status AGENT shows the shipped and observed versions. marshal status --check exits 1 on drift or staleness, for scheduled checks. Freshness is reported beside risk, not folded into it: how risky a change is and how far to trust the record are separate questions.
The store also indexes the tools, knowledge and grant targets of each version, which backs marshal ls --tool and --knowledge and dependency queries.
Entity references¶
References use the form <kind>/<platform>.<type>.<identifier>, for example:
tool/powerplatform.shared_office365users.Manager_V2
knowledge/databricks.uc.table.samples.nyctaxi.trips
model/databricks.serving_endpoint.databricks-claude-sonnet-5-5
An agent that another agent calls is referenced by its agent id: agent/<agent id>. Functions defined in code are scoped to their project (tool/python.<project>/<module>:<function>), so same-named functions in different projects are different entities.
marshal deps PATTERN lists the agents whose latest or shipped versions reference a matching tool, knowledge source or grant target, and where those versions are shipped. marshal entities lists shared entities with the number of agents that use them.
Resolving refs across platforms¶
A ref names its platform, so an API called from two platforms has two refs. Marshal derives a target from what each ref states, and marshal deps also lists refs on any platform that share one with a match:
| Target | From | Match |
|---|---|---|
http:GET https://api.acme.example/invoices/status |
HTTP calls: Agent Spec remote tools and Copilot Studio HTTP request steps | same HTTP operation |
mcp:https://api.githubcopilot.com/mcp, mcp:npm:@notionhq/notion-mcp-server |
MCP servers, by URL or by the package an npx or uvx command runs |
same MCP server |
openapi:<document>#<operationId> |
ADL tools annotated with an OpenAPI operation | same OpenAPI operation |
url:https://contoso.sharepoint.com/sites/hr |
Knowledge sources at a URL | same knowledge source |
host:api.acme.example |
The host of any URL above, and network grants | same API host |
URLs are compared after lowercasing the host, dropping default ports, queries and trailing slashes, and writing every path parameter ({id}, {{id}}, :id) as {}. A same-host match is weaker: it says two agents reach the same system, not that they do the same thing, so it is listed after the others. Nothing is matched on a name alone, nothing is fetched, and a URL with a placeholder or loopback host has no target. --exact turns resolution off, and PATTERN can name a target directly, such as marshal deps "host:api.acme.example".
In the demo, a proposed Copilot Studio topic calls the host that the public ADL examples' network boundaries allow:
AGENT VERSION WHERE RELATION REF
test_allthethings v2 latest tool tool/http.GET.https://api.acme.example/invoices/status
Reaching the same thing, on any platform:
AGENT VERSION WHERE MATCH VIA REF
https://agents.acme.example/invoice-processor v1 prod (latest) same API host host:api.acme.example other/network.api.acme.example
https://agents.acme.example/legacy-analyzer v1 prod (latest) same API host host:api.acme.example other/network.api.acme.example
Tools whose definitions name only a platform object, such as a Power Platform connector operation, an Apex class, a Unity Catalog function or a Bedrock action group's Lambda, match exactly within their platform and aren't resolved further yet.
Completeness¶
quality.completeness is the weighted share of scored fields that are known. quality.unknown_fields lists the rest. Weights favor what matters for governance:
| Field | Weight |
|---|---|
permissions.identity |
3 |
tools[].scope |
3 |
permissions.principal |
2 |
tools[].reversible |
2 |
model.requested |
2 |
instructions |
2 |
knowledge[].scope |
1 |
description, display_name |
0.5 each |
Risk ranking¶
Every change in a diff gets a severity, and reports list the highest first. An unknown scope ranks as a write, because the ledger doesn't assume an undeclared tool is safe.
| Severity | Changes |
|---|---|
| HIGH | Identity changed; grant added with write or admin scope; tool added with write, admin or unknown scope, or known to be irreversible; tool scope widened; tool became irreversible; tool no longer requires confirmation; model provider changed; instructions changed and a removed line held a guardrail word; knowledge added with write scope |
| MED | Principal changed; grant added with read or execute scope; access rule added; tool added with execute scope; tool reversibility became unknown; requested or resolved model changed within a provider, or model parameters changed; instructions changed; knowledge removed, added with unknown scope, or widened |
| LOW | Removals of tools, grants and access; read-only additions; metadata and other extension changes |
Instructions often carry an agent's guardrails, such as "never approve payment without AP approval," so a removed guardrail can widen what the agent does more than a new tool. When both versions' instruction text is stored, Marshal looks in the removed lines for words that usually state a limit: never, must not, do not, cannot, not allowed, forbidden, prohibited, approve or approval, confirm or confirmation, limit, maximum, at most, no more than, only if and unless. A word that also appears in an added line counts as reworded, not removed. The diff names the words it found. The public ADL invoice processor, with "and never approve payment without explicit AP approval" taken out of its instructions:
https://agents.acme.example/invoice-processor (adl) v1 → v2
HIGH ~ instructions: +1 -1 lines; removed a line with 'never', 'approve', 'approval'
Completeness 100%
This is a heuristic: it can't tell a guardrail from an ordinary sentence that happens to use one of these words, and in hash-only mode, with no text, a change to instructions ranks MED.
A different model from the same provider, such as a routine upgrade, ranks MED; a different provider ranks HIGH, because another party then sees the agent's data. Checking models against an approved list belongs to policy checks, a planned milestone.
Platform-specific fields that control identity or access rank by their key wherever they appear in an extension: connection and credential modes, authentication mode, access policy, agent user, user API scopes, authorizers and attached managed policies are HIGH; guardrails, security settings and permission blocks are MED.
The rules are in src/marshal_ledger/risk.py.