kortecxdocs
Apps

Sharing an App

Export a Kortecx App to a single portable .kxapp file, import it under your own account, or clone it locally — and understand exactly what travels and what never does.

An App is the saved, re-runnable unit of work in the agentic runtime — see Apps. Sharing one means writing it to a single file and handing that file over: attach it to an email, drop it in a shared folder, commit it to a repository.

The file is called a bundle, and it ends in .kxapp.

Two things are worth knowing before you send one.

A bundle carries no authority. No secret, no password, no API key, and no permission travels inside it. When someone imports your App, it runs under their account with their permissions — never yours.

A bundle is not signed. There is nothing in the file that proves who made it. Read what is inside before you run it.

Why the plain envelope is not enough

An App stores its text — prompts, rules, context, skill instructions — by reference. The saved App holds a pointer to each piece of content, not the content itself. That is efficient locally, and useless on someone else's machine, where those pointers resolve to nothing.

kx app export apps/local/echo-demo --output echo.app.json

That writes the envelope: the App's structure, by reference. It round-trips on your own machine. It is not self-contained.

A bundle fixes that. It is a kortecx.appbundle/v1 archive: the App's envelope plus every blob of content it points at, packed into one file.

Export an App

Find the App's handle

kx app list

A handle looks like apps/local/echo-demo. That is what every command below takes.

Write the bundle

kx app export apps/local/echo-demo --bundle echo.kxapp
import kortecx as kx

a = (
    kx.app("echo-demo")
    .blueprint(kx.flow().agent("Echo the user's input."))
    .rule("no-pii", body="Never reveal personal data.")
)
a.export("echo.kxapp", bundle=True, client=kx.default_client())

bundle=True needs a client, because collecting the content closure means talking to the running server. It saves the App first, then writes the file.

import { app, flow } from "@kortecx/sdk";

await app("echo-demo")
  .blueprint(flow().agent("Echo the user's input."))
  .rule("no-pii", { body: "Never reveal personal data." })
  .export("echo.kxapp", { bundle: true });

bundle: true needs a client, because collecting the content closure means talking to the running server. The Node entry point (@kortecx/sdk) supplies a default one; pass { bundle: true, client } to use your own. It saves the App first, then writes the file.

The command prints how many blobs it packed and how many bytes they came to.

Include corpora, if the App grounds on one

By default the bundle skips dataset payloads, because a corpus can be far larger than the App around it. Add --with-data to pack the documents in too:

kx app export apps/local/analyst --bundle analyst.kxapp --with-data

Now the App carries its own reading material. Whether it grounds on that reading material on the other side depends on the receiving runtime:

  • Retrieval requires a gateway built with the hnsw feature. Without it, the run honest-degrades to an ungrounded one rather than failing.
  • Materializing the carried corpus on the first run also needs a served embedding model, and the corpus must fit under the self-ingest ceilings — at most 4096 documents and 64 MiB. Over either ceiling, the corpus is skipped and the App grounds on an existing dataset of that name, or on nothing. kx app import reports this at import time, while you can still split the corpus or ingest it by hand.

See Datasets.

Export refuses to ship something that looks like a secret

Blob bodies travel verbatim, so before writing a bundle the exporter scans them for things that look like credentials — a PEM private-key block, an AWS-style access key id. If it finds one it prints the offending blob's content address and refuses. Read the content, remove the secret, and re-export. If you are certain it is a false positive, --force proceeds.

This is a high-signal check, not a guarantee. It cannot recognise every shape a secret can take. You are still the one responsible for what is in your prompts.

What travels, and what does not

Connections and secrets never travel

An App that reaches out to an external service carries only the name of the credential it wants — never the credential. There is no field in the format that can hold one.

The person importing the App re-registers that connection themselves, under their own account, with their own credentials:

kx connections add --provider gmail

Until they do, the App fails closed at run with a message naming what is missing:

missing integration: KX_GMAIL_CREDENTIAL (register it with `kx connections add`)

That is the designed behaviour, not a bug. See Connections.

Travels in the bundle:

  • the App's structure — its blueprint, steering settings, and budgets
  • the text it carries: prompts, rules, memory artifacts, skill instructions
  • context blobs it references
  • dataset payloads, but only with --with-data

Never travels:

  • secrets, API keys, tokens, passwords — any credential value
  • registered connections (only the credential name the App asks for)
  • your grants, permissions, or policy
  • run history, results, and cost records — the format has no field for any of them
  • any file on your host filesystem

Import a bundle

Importing reconciles the file under your own account.

Import an unfamiliar bundle from the CLI

The console has an Import button in the Apps section header, and it imports the file you pick straight away. Only the CLI shows you the instruction text the bundle carries before writing anything. If you did not author the bundle, import it with kx app import and read the review.

Run the import

kx app import echo.kxapp

Read the review

Before anything is written, the importer prints the App's name, its description, and the full text of every prompt, rule, and skill instruction the bundle carries — not a list of names. It then lists the connections and tools the App is asking for.

Read it. Imported instructions steer a model that runs with your tools. The text is the thing worth reviewing; a name-only summary would tell you nothing.

Then it asks:

Proceed with import? [y/N]

Confirm

Answer y. In a script, where there is no terminal to ask, the import refuses unless you pass --yes — so nobody can slip an import into a pipeline without a human having looked once:

kx app import echo.kxapp --yes

Register what did not travel

The import ends by listing the integrations you still need locally — connection credentials, tools, datasets that need re-ingesting. Work through them, then check your standing with:

kx app manifest apps/local/echo-demo

That diffs what the App wants against what you actually have, marking each capability satisfied, missing, or inherited. It is read-only and gates nothing; the runtime enforces the same intersection at run. See Apps.

An import will not quietly overwrite an App you already have at the same handle. It stops and tells you; --force overwrites deliberately.

The checks an import runs

  • Schema. A bundle tagged with a version this build does not understand is rejected outright.
  • Shape. Every digest and content reference must be 64 characters of lowercase hex; every blob must be valid base64.
  • Digest match. The importer re-derives the App's digest from the envelope and compares it to the one the bundle declares. A mismatch means the file was corrupted or altered, and the import stops.
  • Size ceilings. At most 4096 blobs, and at most 512 MiB of content across the whole bundle, checked before a single byte is uploaded. Separately, the server caps any individual blob — 32 MiB by default.
  • Content integrity. Each blob is uploaded and the server re-derives its address from the bytes themselves. If the address it computes differs from the one the bundle claimed, the import stops. Blobs you already hold are recognised and not stored twice.
  • Envelope validation. The server re-validates the envelope on save, exactly as it would for an App you authored yourself.

`source_digest` is a lineage hint, not a signature

A freshly exported bundle carries no lineage field. It is the importing side that stamps one: when you import a bundle, the App saved under your account records a source_digest — the digest of the App it came from, taken from the envelope the importer just verified. A clone records the same thing, using the source App's digest. It is useful for answering "where did this copy come from" when you already trust the source.

It is not a signature. It is a hash of an App's own structure, and nothing binds it to a person. Kortecx has no signing, no publisher identity, and no notion of a trusted source in this release. Provenance across parties you do not already trust is unavailable — treat a bundle from a stranger exactly as you would treat a script from a stranger: read it first.

Importing can never widen what you are allowed to do

This is the guarantee worth stating on its own.

An App carries a request for capability — the tools it wants, the connections it wants, the model it wants. The runtime grants only the overlap between that request and what you are already permitted to do. A request is never authority.

So an imported App:

  • runs under your account and your permissions
  • resolves every tool grant against your policy, at run time, server-side
  • resolves each connection against your registered credentials, by name
  • fails closed if it names a model this instance does not serve, rather than quietly running on a different one

Nothing in the file can change any of that. There is no field for a permission, no field for a grant, no field for a secret. Sharing an App is sharing a plan, never a key.

Sharing between people, not between machines

This is single-system sharing: you hand someone a file, they import it into their own runtime. There is no registry and no link to send. Multi-party sharing on one instance, and provenance you can verify between parties, do not exist in this release.

Clone an App locally

To make a copy of one of your own Apps under a new name — a safe place to experiment, or a starting point for a variant — clone it. Nothing is exported and nothing transfers, because the content is already on the machine:

kx app clone apps/local/echo-demo my-copy

That writes a new App at apps/local/my-copy, records the original's digest as its lineage, and leaves the original untouched. If an App already exists at the new name, the clone stops and asks you to pick another.

The console calls this Duplicate, in each App's overflow menu.

The file itself

A .kxapp is plain text — canonical JSON with sorted keys, and blob bodies base64-encoded. You can open it in an editor and read it.

Because the format is canonical, the same App exported from the CLI, from Python, or from TypeScript produces byte-identical files. That is pinned by a shared golden corpus the three implementations are tested against, so the surfaces cannot drift apart.

The top-level fields:

FieldWhat it is
schemakortecx.appbundle/v1 — the version gate; a reader fails closed on anything else
app_digestthe App's own identity, derived from its envelope
envelopethe App's structure, verbatim
blobsthe content closure — each blob's address mapped to its bytes
source_digestoptional lineage hint; omitted when there is none, as on a fresh export

An App with no carried content and no lineage omits blobs and source_digest entirely.

Next