> ## Documentation Index
> Fetch the complete documentation index at: https://evalgate.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Asset Portability

> Move versioned evaluation assets between EvalGate organizations with canonical hashes, dependency ordering, policy checks, redaction evidence, signatures, and dry-run-first imports.

Asset Portability moves institutional evaluation knowledge without flattening it into an unversioned file. An EvalGate portability bundle retains each asset's canonical identity, exact content hash, dependencies, provenance, required scopes, calibration interpretation, and historical evidence.

Use this control plane for governed organization transfers, environment promotion, backup and restore, or merger and reorganization work. The import never mutates destination state until an owner commits the exact plan returned by a persisted dry run.

## Supported assets

| Kind                    | Versioned source                                  | Evidence retained                                                                 |
| ----------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------- |
| `prompt`                | Prompt release artifact and exact release version | Review, use, result, and release history                                          |
| `dataset`               | Dataset and exact dataset version                 | Row hashes, snapshots, source and quality summaries, and run references           |
| `calibration`           | Calibration set version                           | Anchor provenance, rubric and score-scale versions, mappings, and reviews         |
| `scorer`                | Scorer release artifact and exact release version | Review, use, result, and release history                                          |
| `failure_memory`        | Failure topic and exact topic version             | Decisions, cohort definition, snapshots, and severity evidence                    |
| `approval`              | Prompt or scorer approval record                  | Approved version, environment, approver, rationale, and required asset dependency |
| `institutional_context` | Knowledge-base document                           | Document hash, source, metadata, tags, and versioned content                      |

Exports resolve these records from their native product stores. Imported assets remain visible in the portability workspace with both source and destination references; they do not replace the native source record that produced the bundle.

## Permissions

| Operation                                                                                              | Required access                                    |
| ------------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
| Open the workspace, inspect inventory and history, download a bundle, export, or run an import preview | Organization administrator with `exports:download` |
| Commit an import or trust a signer                                                                     | Organization owner with `admin:org`                |

The importer also compares every bundle and asset `requiredScopes` value with the actor's current scopes. An import cannot grant or preserve a capability the committing actor does not hold.

## Bundle identity and compatibility

Every new bundle declares:

```json theme={null}
{
  "schema": "evalgate.portability.bundle",
  "bundleVersion": "1.0.0",
  "compatibilityVersion": 1,
  "bundleId": "7c48681e-2b9f-4a3f-80db-f4efcba7eabe",
  "dependencyOrder": ["dataset:support-gold@4", "prompt:support-answer@9"],
  "redactionManifest": [],
  "bundleHash": "3bb4b24b0dcdcb211f06f81c67f1f12a0f69b1dd4f383b03ec68ee82879e850a"
}
```

The bundle hash is the SHA-256 digest of canonical JSON with `bundleHash` and `signature` removed. Each asset has a separate SHA-256 `contentHash` over canonical asset content. EvalGate verifies both layers before planning an import.

`dependencyOrder` is a topological order, not a display hint. The importer rejects a missing required dependency, an expected dependency hash mismatch, or a cycle. Compatibility version `0` bundles are migrated to the current shape during validation. A bundle from a future unsupported compatibility version is rejected before any destination mutation.

## Define the source-organization policy

The export policy travels inside the bundle hash and, when enabled, its signature:

```json theme={null}
{
  "classification": "confidential",
  "allowCrossOrganization": true,
  "allowedDestinationOrganizationIds": [
    "7693dc4c-4556-47ff-b309-4e90ecb40b1c"
  ],
  "requiresSignature": true,
  "requiredScopes": ["eval:read", "exports:download"]
}
```

* `classification` is one of `public`, `internal`, `confidential`, or `restricted`.
* `allowCrossOrganization` must be true when source and destination organizations differ.
* `allowedDestinationOrganizationIds` narrows cross-organization import to explicit UUIDs. An empty list allows any destination only when cross-organization import is enabled.
* `requiresSignature` makes an unsigned, invalidly signed, or untrusted bundle incompatible.
* `requiredScopes` are checked in addition to each asset's scopes.

Changing policy JSON after export changes the canonical bundle hash and invalidates the bundle.

## Redact sensitive fields

Redactions use a canonical asset reference plus a JSON Pointer into that asset's `content` value:

```json theme={null}
{
  "canonicalRef": "dataset:support-gold@4",
  "jsonPointer": "/rows/0/snapshotPayload/customer_email",
  "action": "hash",
  "reason": "Remove direct identifiers before transfer"
}
```

Available actions are:

| Action   | Exported value                                            |
| -------- | --------------------------------------------------------- |
| `remove` | The property or array item is omitted                     |
| `mask`   | The value becomes `[REDACTED]`                            |
| `hash`   | The value becomes a deterministic `sha256:<digest>` token |

Each applied rule adds a redaction-manifest entry containing the asset reference, pointer, action, reason, and SHA-256 hash of the original value. The original value is not stored in the manifest. Asset and bundle hashes are computed after redaction, so an importer verifies the transferred representation exactly.

## Configure signing keys

Export signing uses Ed25519 PEM keys configured on the EvalGate server. `PORTABILITY_SIGNING_KEYS` is a JSON array:

```bash theme={null}
PORTABILITY_SIGNING_KEYS='[{"keyId":"transfer-2026-q3","privateKey":"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----","publicKey":"-----BEGIN PUBLIC KEY-----\\n...\\n-----END PUBLIC KEY-----"}]'
```

If `publicKey` is omitted, EvalGate derives it from the private key. Keep private keys in the deployment secret manager and rotate `keyId` values deliberately; the bundle embeds only the public key, key ID, algorithm, and signature.

Before importing a signature-required bundle, a destination owner trusts the exact key:

```bash theme={null}
curl -X POST "$EVALGATE_URL/api/portability/trusted-signers" \
  -H "Authorization: Bearer $EVALGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "keyId": "transfer-2026-q3",
    "displayName": "Source production transfer key",
    "publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
  }'
```

Trust is organization-scoped. Matching only the key ID is insufficient: the public key and cryptographic signature must also match.

## Export a governed bundle

Selectors identify native assets. Omit `version` to resolve the latest immutable version.

```bash theme={null}
curl -X POST "$EVALGATE_URL/api/portability/exports" \
  -H "Authorization: Bearer $EVALGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "selectors": [
      {
        "kind": "dataset",
        "id": "support-gold",
        "version": "4",
        "includeHistoricalEvidence": true
      },
      {
        "kind": "prompt",
        "id": "support-answer",
        "version": "9",
        "includeHistoricalEvidence": true
      }
    ],
    "policy": {
      "classification": "confidential",
      "allowCrossOrganization": true,
      "allowedDestinationOrganizationIds": [
        "7693dc4c-4556-47ff-b309-4e90ecb40b1c"
      ],
      "requiresSignature": true,
      "requiredScopes": ["eval:read"]
    },
    "redactions": [],
    "signingKeyId": "transfer-2026-q3"
  }'
```

The response contains the stored export record and its complete `bundle`. Save the JSON without rewriting it. You can later retrieve the exact stored payload:

```bash theme={null}
curl "$EVALGATE_URL/api/portability/bundles/$BUNDLE_ID" \
  -H "Authorization: Bearer $EVALGATE_API_KEY" \
  --output evalgate-portability-bundle.json
```

The download response includes `X-EvalGate-Bundle-Hash`. Compare it with the JSON `bundleHash` when moving the file through another system.

## Preview an import

Every import begins with a dry run. Choose both a conflict policy and a reference policy explicitly:

| Policy                           | Behavior                                                                            |
| -------------------------------- | ----------------------------------------------------------------------------------- |
| `conflictPolicy: reject`         | Mark an existing destination reference and version incompatible                     |
| `conflictPolicy: skip`           | Retain the destination record and append a skip event                               |
| `conflictPolicy: replace`        | Atomically replace the matching reference and version                               |
| `conflictPolicy: create_version` | Allocate the next deterministic version for that target reference                   |
| `referencePolicy: preserve`      | Keep every canonical reference from the source bundle                               |
| `referencePolicy: remap`         | Require an explicit source-to-target entry for every asset and dependency reference |

```bash theme={null}
curl -X POST "$EVALGATE_URL/api/portability/imports/dry-run" \
  -H "Authorization: Bearer $EVALGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "bundle": { "schema": "evalgate.portability.bundle", "...": "complete bundle" },
  "idempotencyKey": "acquisition-482-support-assets",
  "conflictPolicy": "create_version",
  "referencePolicy": "remap",
  "referenceRemap": {
    "dataset:support-gold@4": "dataset:shared-support-gold@1",
    "prompt:support-answer@9": "prompt:shared-support-answer@1"
  },
  "allowPartial": false
}
JSON
```

The response persists an immutable plan with:

* `dryRunToken`, bound to the complete request and destination state;
* global `compatible` and `partial` results;
* dependency order and a per-action count;
* one planned target reference, target version, action, and compatibility result per asset;
* structured issue codes such as `SIGNER_UNTRUSTED`, `DEPENDENCY_MISSING`, `PRIVILEGE_WIDENING`, `REFERENCE_REMAP_MISSING`, or `CONFLICT`.

Set `allowPartial` only when it is acceptable to commit compatible assets and retain explicit reject events for incompatible assets. With the default `false`, any incompatible asset blocks the commit.

## Commit the exact plan

Add the returned token to the otherwise unchanged request:

```json theme={null}
{
  "bundle": { "...": "the same bundle" },
  "idempotencyKey": "acquisition-482-support-assets",
  "conflictPolicy": "create_version",
  "referencePolicy": "remap",
  "referenceRemap": {
    "dataset:support-gold@4": "dataset:shared-support-gold@1",
    "prompt:support-answer@9": "prompt:shared-support-answer@1"
  },
  "allowPartial": false,
  "dryRunToken": "201a4816-1561-4b98-a029-6ff66b515037"
}
```

```bash theme={null}
curl -X POST "$EVALGATE_URL/api/portability/imports/commit" \
  -H "Authorization: Bearer $EVALGATE_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @commit-request.json
```

EvalGate acquires an organization-scoped import lock and recomputes the plan inside the commit transaction. A changed bundle, policy, conflict choice, remap, partial setting, actor scope, signature trust state, or destination record makes the dry run stale and prevents mutation.

The bundle record, committed import, portable assets, per-asset events, commit event, and audit log are written in one database transaction. If any write fails, none of them remain. Repeating a completed request with the same idempotency key returns the original import; reusing that key for a different request is rejected.

## Verify imported evidence

List the organization workspace:

```bash theme={null}
curl "$EVALGATE_URL/api/portability" \
  -H "Authorization: Bearer $EVALGATE_API_KEY"
```

For each imported asset, verify:

* `canonicalRef` and `sourceCanonicalRef` reflect the selected preserve or remap policy;
* `version` is the planned destination version;
* `contentHash` equals the exported asset hash;
* `dependencies` point to the planned destination references;
* `provenance` retains native source identifiers and the original native hash;
* `requiredScopes` have not widened;
* calibration assets retain `rubricVersion`, `scoreScaleVersion`, and mapping interpretation;
* `historicalEvidence` contains the selected native reviews, decisions, runs, or snapshots.

Retrieve a specific import and its ordered event history with:

```bash theme={null}
curl "$EVALGATE_URL/api/portability/imports/$IMPORT_ID" \
  -H "Authorization: Bearer $EVALGATE_API_KEY"
```

## Failure behavior

| Condition                                                         | Result before mutation                             |
| ----------------------------------------------------------------- | -------------------------------------------------- |
| Bundle or asset hash mismatch                                     | Import rejected                                    |
| Missing dependency, dependency hash mismatch, or cycle            | Import rejected or explicitly partial when allowed |
| Signature required but absent, invalid, or untrusted              | Import rejected                                    |
| Future compatibility version                                      | Import rejected                                    |
| Cross-organization transfer not allowed or destination not listed | Import rejected                                    |
| Actor lacks a policy or asset scope                               | `PRIVILEGE_WIDENING`; import blocked               |
| Remap omits an asset or dependency                                | `REFERENCE_REMAP_MISSING`; import blocked          |
| Destination changed after preview                                 | Dry-run token reported stale; rerun preview        |
| Persistence failure during commit                                 | Entire transaction rolled back                     |

Do not edit a bundle to work around an issue. Correct the source policy, redaction, dependency, destination conflict, signer trust, or actor authorization, create a new bundle when its contents change, and run a new dry run.
