Package specification¶
Every cartridge is cleanly specified: it carries a signed, closed package spec with exactly eight normative artifact roles and versioned schema ids — MCP-style self-describing — and pins a fixed LanceDB schema for the optional vector memory payload.
This page covers SPEC §7.7. The reference logic lives in src/packagespec.mjs (build, emit, validate); the JSON Schemas are schemas/package-spec.schema.json and schemas/lance-memory.schema.json.
Why packages are cleanly specified¶
A .acx file is one SQLite database holding many kinds of content: identity metadata, a memory table, skills in sqlar, capability rows, a harness manifest, a loop policy, attestations. Without a manifest of what is in here and what format each thing claims to be, a consumer has to guess — probe tables, sniff paths, hope the versions line up.
The package spec removes the guessing. It borrows the pattern MCP uses for tool declarations: every artifact self-describes with a stable role and a versioned schema id, so a consumer can
- enumerate what the package contains,
- check every
requiredartifact is actually present, and - validate each artifact against the exact schema version it declares —
all before activating anything. Because the spec lives in the ROM zone (rom/package-spec.json), it is covered by the ROM manifest and DSSE signature: a publisher cannot claim one set of artifacts and ship another without the cartridge verifying as tampered.
Where the spec lives
rom/package-spec.json— the package spec,schemaVersion: "acx.package-spec/1", ROM-signed.rom/schema/lance-memory.json— the pinned LanceDB memory descriptor,schemaVersion: "acx.lance-memory/1", ROM-signed.- Meta keys
acx.package_specandacx.lance_schemapoint at both paths.
Both files are emitted automatically on export (emitPackageSpec in src/packagespec.mjs) — authors never write them by hand.
Inspecting a package: acx spec¶
The spec command prints the package spec and runs full validation: the closed eight-role profile and
cartridge metadata agree exactly, every stored archive path is portable and traversal-safe, required
artifacts exist, memory and capability records conform, and the LanceDB descriptor matches the
cartridge's embedding engine.
$ node --experimental-sqlite src/cli.mjs spec examples/research-designer.acx
package-spec: acx.package-spec/1 engine={"id":"local-hash-128","dim":128}
artifacts:
* identity acx.cartridge-meta/1 meta
* memory-baseline acx.memory-record.v1 table (1)
memory-vectors acx.lance-memory/1 lance
skills acx.skill/1 sqlar (1)
capabilities acx.capability/1 table (2)
* harness acx.harness.v1 sqlar
* loop-context acx.loop-context-policy/1.1 sqlar
level acx.level-credential.v1 attestation (0)
validation: CLEAN ✓
A * marks a required artifact; counts in parentheses are live row/file counts. A cartridge that fails any check is not clean, and downstream tooling treats it accordingly — for example, the git registry's index builder (tools/build-registry-index.mjs) rejects any pushed cartridge whose package spec fails validation, alongside signature and tamper checks.
The raw rom/package-spec.json behind that output
{
"schemaVersion": "acx.package-spec/1",
"cartridgeId": "io.github.agentibus/scenario-research-designer@3bb15e93-4ab3-437a-9de0-8e4282547cb6",
"specVersion": "0.1",
"embeddingEngine": { "id": "local-hash-128", "dim": 128 },
"artifacts": [
{ "role": "identity", "kind": "meta", "schema": "acx.cartridge-meta/1", "required": true },
{ "role": "memory-baseline", "kind": "table", "table": "memory",
"mediaType": "application/json", "schema": "acx.memory-record.v1",
"required": true, "count": 1 },
{ "role": "memory-vectors", "kind": "lance", "path": "vectors/memories.lance",
"schema": "acx.lance-memory/1", "required": false, "signed": false,
"reindexOnImport": true, "descriptor": "rom/schema/lance-memory.json" },
{ "role": "skills", "kind": "sqlar", "path": "rom/skills/",
"schema": "acx.skill/1", "required": false, "count": 1 },
{ "role": "capabilities", "kind": "table", "table": "capabilities",
"schema": "acx.capability/1", "required": false, "count": 2 },
{ "role": "harness", "kind": "sqlar", "path": "rom/manifest/harness-requirements.json",
"schema": "acx.harness.v1", "required": true },
{ "role": "loop-context", "kind": "sqlar", "path": "rom/policy/loop-context-policy.json",
"schema": "acx.loop-context-policy/1.1", "required": true },
{ "role": "level", "kind": "attestation",
"schema": "acx.level-credential.v1", "required": false, "count": 0 }
]
}
Publisher ids like io.github.agentibus are illustrative handles, not real organizations.
Artifact roles and schema ids¶
Each artifact entry is {role, kind, schema, required, …}. The role is stable, the kind says where
in the container the artifact lives, and schema is one of the registered, versioned ids:
| Role | Schema id | Kind | Required | Format page |
|---|---|---|---|---|
identity |
acx.cartridge-meta/1 |
meta | yes | Container |
memory-baseline |
acx.memory-record.v1 |
table (memory) |
yes | Memory |
memory-vectors |
acx.lance-memory/1 |
lance (vectors/memories.lance) |
no | this page, below |
skills |
acx.skill/1 |
sqlar (rom/skills/) |
no | Skills |
capabilities |
acx.capability/1 |
table (capabilities) |
no | Capabilities |
harness |
acx.harness.v1 |
sqlar (rom/manifest/harness-requirements.json) |
yes | Harness requirements |
loop-context |
acx.loop-context-policy/1.1 |
sqlar (rom/policy/loop-context-policy.json) |
yes | Loop + context policy |
level |
acx.level-credential.v1 |
attestation | no | Leveling |
acx.package-spec/1 contains one and only one of every row in this table. Each role has a fixed set of
fields and fixed values for its kind, schema, required flag, locator, and import policy. Unknown top-level
or role fields, unknown roles, duplicates, omissions, or a changed role profile make the package unclean.
The manifest's cartridgeId, specVersion, and closed {id, dim} embeddingEngine must exactly match
the signed cartridge metadata. “Required: no” means that role's payload may be absent; its role declaration
still appears in the eight-entry manifest with a zero count or the fixed re-index policy.
The full schema index — $ids, media types, and the files under schemas/ — is on the JSON schemas & media types reference page.
The fixed LanceDB memory schema (acx.lance-memory/1)¶
The optional vector payload has exactly one allowed shape. It is pinned in rom/schema/lance-memory.json (ROM-signed, pointed to by acx.lance_schema) and validated by schemas/lance-memory.schema.json:
- Table:
memories - Partitioned by:
zone(rom|save— the two container zones) - Distance metric:
cosine - Embedding engine:
{id, dim}recorded in the descriptor; thevectorcolumn dimension comes fromdim
A conforming .lance payload MUST use exactly these columns:
| Column | Type | Nullable | Notes |
|---|---|---|---|
id |
utf8 |
no | |
zone |
utf8 |
no | enum rom | save |
portable |
bool |
no | the tier selector |
artifact_fingerprint |
utf8 |
no | |
codebase_fingerprint |
utf8 |
yes | null for portable records |
title |
utf8 |
no | |
summary |
utf8 |
no | |
source_type |
utf8 |
no | |
tags |
list<utf8> |
no | |
impact |
utf8 |
no | enum positive | neutral | negative |
xp_awarded |
int32 |
no | |
timestamp |
utf8 |
no | |
text |
utf8 |
no | the embedded document: title + "\n\n" + summary |
vector |
fixed_size_list<float32, dim> |
no | engine(text); dim from the embedding engine |
The columns map 1:1 to acx.memory-record.v1 (snake_case in the table, camelCase in JSON), so the derivation is deterministic:
Baseline is authoritative; vectors are derived¶
The JSON baseline wins — always
The JSON memory baseline (acx.memory-record.v1, SPEC §7.6) is authoritative. The LanceDB table is a derived, re-indexable projection that is never signed and MUST be rebuilt on import against the consumer's own embedding engine (reindexOnImport: true in the package spec, signed: false because the ROM manifest cannot cover bytes that legitimately differ per engine). If the baseline and the vectors ever disagree, the vectors are wrong by definition — drop and re-index.
Because the projection is deterministic, the .lance payload can be materialized from the baseline at any time, and a registry can ship the .lance file alongside the JSON baseline without ambiguity about which is the source of truth.
Materialize a real LanceDB file: acx lance
# one-time: the optional materializer (the project's single dependency, isolated here)
uv venv tools/lance/.venv --python 3.12
uv pip install --python tools/lance/.venv pylance pyarrow numpy
node --experimental-sqlite src/cli.mjs lance my-agent.acx
# materialized a real LanceDB dataset (acx.lance-memory/1): N rows, fixed_size_list<item: float>[128]
# embedded in cartridge → save/vectors/memories.lance (SAVE zone, unsigned)
# standalone dataset → my-agent.memories.lance
acx lance computes the local-hash-128 vectors in JS (src/embed.mjs, acx.embed/local-hash-128/1),
writes a genuine LanceDB dataset with the exact acx.lance-memory/1 schema via pylance, embeds it
in the cartridge SAVE zone (so it never affects the ROM signature — verified by the test suite), and
leaves a standalone <file>.memories.lance/ dataset that any LanceDB runtime opens directly.
What the zero-dependency core ships
The CLI core runs on Node's builtin node:sqlite with no runtime dependencies: it ships the JSON
baseline, the pinned acx.lance-memory/1 descriptor, and the validator (validatePackageSpec checks the
descriptor's dim against the cartridge's engine and its column set against the normative list). The
real .lance bytes are produced by the optional acx lance tool above (one dependency, pylance,
kept out of the core) or by any LanceDB-capable runtime such as the AGENTIBUS studio — all building the
identical table from the same descriptor + embedding definition.
Validation rules¶
acx spec (and validatePackageSpec in src/packagespec.mjs) reports a cartridge as CLEAN only when all of the following hold:
rom/package-spec.jsonexists, declaresschemaVersion: "acx.package-spec/1", contains only the five defined top-level fields, and exactly matches cartridge id, spec version, and embedding-engine metadata.artifactscontains exactly the eight role profiles above — no unknown, duplicate, missing, extended, or altered entries.- Every stored
sqlarname begins withrom/orsave/and uses only non-empty portable ASCII segments matching[A-Za-z0-9][A-Za-z0-9._-]{0,127}. Absolute paths,.,.., empty segments, backslashes, NUL, and other traversal/platform-specific forms are rejected before extraction. - Every record in the
memorytable structurally conforms toacx.memory-record.v1— all mandatory fields present,portableboolean, validimpact/zoneenums, and the tier invariant:portable: true⇒codebaseFingerprintisnull,portable: false⇒ a fingerprint is present. - Capability records conform and their locally resolvable evidence references exist.
rom/schema/lance-memory.jsonexists, its embedding engine matches the cartridge's engine, and its column count matches the normative 14-column list.- Every
requiredSQLAR artifact path in the spec resolves to a real file.
Anything less is reported as a list of issues and a non-clean verdict.
Related pages¶
- Container — the SQLite file format, zones, and the
sqlarlayout the spec paths refer to. - Memory — the two-tier memory model behind
portable,zone, and the fingerprints. - Signing & trust — how the ROM manifest covers
rom/package-spec.jsonandrom/schema/lance-memory.json. - JSON schemas & media types — the consolidated schema index.