<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>The Tiles Blog</title>
    <link>https://www.tiles.run/blog</link>
    <description>Open source privacy technology for personalized software experiences</description>
    <language>en-us</language>
    <lastBuildDate>Fri, 10 Apr 2026 03:38:27 GMT</lastBuildDate>
    <atom:link href="https://www.tiles.run/api/rss" rel="self" type="application/rss+xml"/>
        <item>
      <title><![CDATA[Ship it up]]></title>
      <link>https://www.tiles.run/blog/ship-it-up</link>
      <description><![CDATA[How we package and ship Tiles]]></description>
      <content:encoded><![CDATA[<p>We ship Tiles as a <code>tar.gz</code> tarball and as a native macOS <code>.pkg</code>. Either format follows the same broad steps.</p>

<ul>
<li><p>Assemble the deliverable:</p><ul><li><p>Package the Tiles binary and the Python <a href="https://www.tiles.run/blog/move-along-python" target="_blank" rel="noopener noreferrer">venvstack</a> artifacts into one installer payload.</p></li></ul></li>
<li><p>Run the installer:</p><ul><li><p>Copy the Tiles binary and Python artifacts into the correct locations on disk.</p></li></ul></li>
<li><p>Run postinstall scripts.</p></li>
</ul>

<p>In early releases, the tarball was the only install path. It served us well, but it had clear limits.</p>

<ul>
<li><p>Because the release artifact is a gzip tarball, we cannot codesign, notarize, and staple the archive itself the way we can a full installer. We run those steps on the Tiles binary only, not on the entire <code>.gz</code> file.</p></li>
<li><p>Installation relies on a curl-based script. That is workable for developers and rougher for everyone else.</p></li>
<li><p>The flow leaves little room to tailor copy, branding, or steps in the installer UI.</p></li>
</ul>

<p>We wanted a path that fits Apple's signing and notarization story and feels native on macOS. Installers ship as <code>.dmg</code> or <code>.pkg</code> bundles; we chose <code>.pkg</code> because it can run scripts, uses a familiar installer UI, and supports customization with simple HTML. What follows is how we build that <code>.pkg</code> for Tiles.</p>

<h2>Install file structure</h2>

<img src="https://www.tiles.run/blog-ship-it-up-pkgroot-structure.jpg" alt="" style="width: 100%; height: auto; margin: 2rem 0;" />

<p>The layout matches the directory tree above. When we build the package, we point the tooling at this <code>pkgroot</code> directory as the install root. The installer copies that tree onto the destination volume and creates intermediate directories as needed.</p>

<p>For each release, we build a fresh Tiles binary into <code>pkgroot/usr/local/bin</code> and place the remaining updated artifacts under <code>pkgroot/usr/local/share/tiles/</code>.</p>

<p>The <a href="https://github.com/tilesprivacy/tiles/blob/main/pkg/build.sh" target="_blank" rel="noopener noreferrer">build script</a> in the repo has the full sequence.</p>

<h2>Code signing the Tiles binary</h2>

<p>Code signing gives us:</p>

<ul>
<li><p>A signed Tiles binary that cannot be altered without invalidating the signature.</p></li>
<li><p>A signature from a developer certificate that Apple trusts.</p></li>
</ul>

<p>You need an Apple Developer account and two certificate types: <strong>DEVELOPER ID APPLICATION</strong> for the binary and <strong>DEVELOPER ID INSTALLER</strong> for the <code>.pkg</code>. The finished installer wraps the signed binary and the other artifacts in one compressed package.</p>

<p>For creating and exporting those certificates, the CodeVamping <a href="https://www.codevamping.com/2023/11/macos-pkg-installer/" target="_blank" rel="noopener noreferrer">macOS pkg installer article</a> covers the details.</p>

<pre><code># Signing the Tiles binary
codesign --force \
  --sign "$DEVELOPER_ID_APPLICATION"\
  --options runtime \
  --timestamp \
  --strict \
  "${CLI_BIN_PATH}/tiles"
</code></pre>

<p>The snippet signs the Tiles CLI. <code>$DEVELOPER_ID_APPLICATION</code> is an environment variable that holds the common name of the Developer ID Application certificate.</p>

<h2>Scripts</h2>

<p>Bash scripts can run before and after the payload is laid down. We keep <strong>preinstall</strong> and <strong>postinstall</strong> scripts in a directory and pass that path into <code>pkgbuild</code>.</p>

<p>The Tiles repo has <a href="https://github.com/tilesprivacy/tiles/tree/main/pkg/scripts" target="_blank" rel="noopener noreferrer">those scripts</a>; they handle cleanup and internal setup.</p>

<h2>Building the Tiles package</h2>

<p>With the signed Tiles binary and the rest of the install tree under <code>pkgroot</code>, we run:</p>

<pre><code>pkgbuild --root pkgroot --scripts \
 pkg/scripts --identifier com.tilesprivacy.tiles --version "$VERSION" \
 pkg/tiles-unsigned.pkg
</code></pre>

<p><code>--root</code> is <code>pkgroot</code>; <code>--scripts</code> points at the directory from the previous section.</p>

<h2>Customizing the installer</h2>

<img src="https://www.tiles.run/blog-ship-it-up-installer-custom-1.jpg" alt="" style="width: 100%; height: auto; margin: 2rem 0;" />

<p>Opening the unsigned package from the previous step shows Apple's default, minimal flow. We layer on welcome and conclusion screens, a logo, and other panels with an Apple <a href="https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Introduction.html#//apple_ref/doc/uid/TP40005370-CH1-SW1" target="_blank" rel="noopener noreferrer" style="font-weight:700">distribution</a> definition in XML. That file references HTML for the welcome, conclusion, and other supported steps. Our <a href="https://github.com/tilesprivacy/tiles/blob/main/pkg/distribution_network.xml" target="_blank" rel="noopener noreferrer">distribution_network.xml</a> is the working example. Elements such as <code>&lt;welcome/&gt;</code> and <code>&lt;conclusion/&gt;</code> point at resources we keep alongside the definition and pass into the final <code>productbuild</code> invocation.</p>

<pre><code>productbuild \
  --distribution pkg/distribution_network.xml \
  --resources pkg/resources \
  --package-path pkg/  \
  pkg/tiles-dist-unsigned.pkg
</code></pre>

<p><code>productbuild</code> reads the unsigned package and writes a distributable installer with those customizations baked in.</p>

<img src="https://www.tiles.run/blog-ship-it-up-installer-custom-2.jpg" alt="" style="width: 100%; height: auto; margin: 2rem 0;" />

<img src="https://www.tiles.run/blog-ship-it-up-installer-custom-3.jpg" alt="" style="width: 100%; height: auto; margin: 2rem 0;" />

<h2>Code signing the complete installer</h2>

<pre><code>productsign \
  --sign "$DEVELOPER_ID_INSTALLER" \
  pkg/tiles-dist-unsigned.pkg \
  pkg/tiles.pkg
</code></pre>

<p>That signs the <code>.pkg</code> itself. The Tiles binary is already signed; signing a macOS binary and signing an installer package are different operations.</p>

<p>The main difference is <code>productsign</code> instead of <code>codesign</code>, and the certificate we use is <code>DEVELOPER ID INSTALLER</code> instead of <code style="white-space: pre-wrap">DEVELOPER  ID APPLICATION</code>.</p>

<h2>Notarizing the installer</h2>

<p>Notarization lets Apple scan the payload for malware. We upload the installer; when processing finishes, the service returns acceptance or rejection.</p>

<p>Submission looks like this:</p>

<pre><code>xcrun notarytool submit pkg/tiles.pkg \
  --keychain-profile "tiles-notary-profile" \
  --wait
</code></pre>

<p><code>tiles-notary-profile</code> is the keychain entry name so we do not pass Apple ID details on every run. Store the profile once with:</p>

<pre><code>xcrun notarytool store-credentials \
  "tiles-notary-profile" \
  --apple-id "john.doe@gmail.com" \
  --team-id "X********4" \
  --password "****-****-****-****"</code></pre>

<h2>Stapling the installer</h2>

<p>Stapling embeds the notarization ticket in the installer file so Gatekeeper needs fewer round trips to Apple when a user opens the package.</p>

<pre><code>xcrun stapler staple pkg/tiles.pkg
</code></pre>

<h2>What's next</h2>

<p>We also ship a fully offline installer that bundles a gpt-oss model, so Tiles can be installed without an internet connection. We are working toward making Tiles itself portable, allowing users to carry their model and data and run it from a flash drive on any compatible system.</p>

<p>The installer requires Rosetta; on Apple Silicon Macs, it is not included by default and must be installed separately. Rosetta translates Intel binaries to run on Apple Silicon. There is a workaround for this, described in the <a href="https://github.com/tilesprivacy/tiles/issues/105" target="_blank" rel="noopener noreferrer">linked GitHub issue</a>.</p>]]></content:encoded>
      <pubDate>Sun, 05 Apr 2026 00:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://www.tiles.run/blog/ship-it-up</guid>
    </item>
    <item>
      <title><![CDATA[Introducing Tiles Public Alpha]]></title>
      <link>https://www.tiles.run/blog/introducing-tiles-public-alpha</link>
      <description><![CDATA[Announcing Tiles Public Alpha: our first release of a privacy-first AI assistant with local models, a CLI app for Apple Silicon, and a Tilekit SDK for developers.]]></description>
      <content:encoded><![CDATA[<img src="https://www.tiles.run/kingston.webp" alt="Cover image for Introducing Tiles Public Alpha" style="width: 100%; height: auto; margin-bottom: 2rem;" />

<p>We're thrilled to launch Tiles Public Alpha, our first public release of a privacy-first AI assistant. Tiles brings together local-first models, personalized experiences, and verifiable privacy, all running on your device, with your data staying yours. We believe identity and memory are two sides of the same coin, and Tiles makes that coin yours: your user-agent.</p>

<p>Our first alpha is a CLI assistant app for Apple Silicon devices, and a Tilekit SDK that lets developers customize local models and agent experiences within Tiles. We aim to evolve Modelfile in collaboration with the community, establishing it as the standard for model customization.</p>

<h2>Philosophy</h2>

<p>The project is defined by four interdependent design choices<a href="https://www.tiles.run/blog/introducing-tiles-public-alpha#ref-2">²</a>:</p>

<ol>
  <li>
    <strong>Device-anchored identity with keyless ops:</strong> Clients are provisioned through the device keychain and cannot access the registry by identity alone<a href="https://www.tiles.run/blog/introducing-tiles-public-alpha#ref-3">³</a>. Keyless operations are only enabled after an identity is verified and linked to the device key, allowing third-party agent access under user-defined policies<a href="https://www.tiles.run/blog/introducing-tiles-public-alpha#ref-4">⁴</a>.
  </li>
  <li>
    <strong>Immutable model builds:</strong> Every build is version-locked and reproducible, ensuring consistency and reliability across updates and platforms.
  </li>
  <li>
    <strong>Content-hashed model layers:</strong> Models are stored and referenced by cryptographic hashes of their layers, guaranteeing integrity and enabling efficient deduplication and sharing.
  </li>
  <li>
    <strong>Verifiable transparency and attestations:</strong> Every signing and build event is logged in an append-only transparency log, producing cryptographic attestations that can be independently verified. This ensures accountability, prevents hidden modifications, and provides an auditable history of model provenance across devices and registries.
  </li>
</ol>

<h2>Implementation</h2>

<img src="https://www.tiles.run/tilescli.png" alt="Tiles CLI" style="width: 100%; height: auto; margin: 2rem 0;" />

<p>Tiles bundles a fine-tuned model to manage context and memories locally on-device with hyperlinked markdown files. Currently, we use <a href="https://huggingface.co/driaforall/mem-agent" target="_blank" rel="noopener noreferrer">mem-agent</a> model (from <a href="https://dria.co/" target="_blank" rel="noopener noreferrer">Dria</a>, based on <code>qwen3-4B-thinking-2507</code>), and are in the process of training our initial in-house memory models.</p>

<p>These models utilize a human-readable external memory stored as markdown, and learned policies (trained via reinforcement learning on synthetically generated data) to decide when to call Python functions that retrieve, update, or clarify memory, allowing the assistant to maintain and refine persistent knowledge across sessions.</p>

<h2>Looking forward</h2>

<p>We're building the next layer of private personalization: customizable memory, private sync, verifiable identity, and a more open model ecosystem.</p>

<ul>
  <li>
    <strong>Memory extensions:</strong> Add support for LoRA-based memory extensions so individuals and organizations can bring their own data and shape the assistant's behavior and tone on top of the base memory model.
  </li>
  <li>
    <strong>Sync:</strong> Build a reliable, peer-to-peer sync layer using <a href="https://www.iroh.computer/" target="_blank" rel="noopener noreferrer">Iroh</a> for private, device-to-device state sharing.
  </li>
  <li>
    <strong>Identity:</strong> Ship a portable identity system using <a href="https://atproto.com/specs/did" target="_blank" rel="noopener noreferrer">AT Protocol DIDs</a>, designed for device-anchored trust.
  </li>
  <li>
    <strong>MIR in Modelfile:</strong> Work with the <a href="https://darkshapes.org/" target="_blank" rel="noopener noreferrer">Darkshapes</a> team to support the <a href="https://huggingface.co/darkshapes/MIR_" target="_blank" rel="noopener noreferrer">MIR</a> (Machine Intelligence Resource) naming scheme in our Modelfile implementation.
  </li>
  <li>
    <strong>Registry:</strong> Continue supporting Hugging Face, while designing a decentralized registry for versioned, composable model layers using the open-source <a href="https://github.com/huggingface/xet-core" target="_blank" rel="noopener noreferrer">xet-core</a> client tech.
  </li>
  <li>
    <strong>Research roadmap:</strong> As part of our research on private software personalization infrastructure, we are investigating sparse memory finetuning, text diffusion models, Trusted Execution Environments (TEEs), and Per-Layer Embeddings (PLE) with offloading to flash storage.
  </li>
</ul>

<p>We are seeking design partners for training workloads that align with our goal of ensuring a verifiable privacy perimeter. If you're interested, please reach out to us at <a href="mailto:hello@tiles.run">hello@tiles.run</a>.</p>

<h2>References</h2>

<ol>
  <li id="ref-1">
    <a href="https://docs.ollama.com/modelfile" target="_blank" rel="noopener noreferrer">Ollama Modelfile</a> is the blueprint to create and share customized models using Ollama.
  </li>
  <li id="ref-2">
    <a href="https://newsletter.squishy.computer/p/decentralizability" target="_blank" rel="noopener noreferrer">Decentralizability</a> (Gordon Brander): Immutable data, universal IDs, user-controlled keys… and just using HTTP. I think this is probably minimum viable decentralizability.
  </li>
  <li id="ref-3">
    <a href="https://keybase.io/blog/keybase-new-key-model" target="_blank" rel="noopener noreferrer">Keybase's New Key Model</a>
  </li>
  <li id="ref-4">
    <a href="https://www.sigstore.dev/how-it-works" target="_blank" rel="noopener noreferrer">Sigstore: How It Works</a>
  </li>
</ol>]]></content:encoded>
      <pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://www.tiles.run/blog/introducing-tiles-public-alpha</guid>
    </item>
    <item>
      <title><![CDATA[Move Along, Python]]></title>
      <link>https://www.tiles.run/blog/move-along-python</link>
      <description><![CDATA[Deterministic, portable Python runtimes for Tiles using layered venvstacks.]]></description>
      <content:encoded><![CDATA[<p>We have been working on <a href="https://www.tiles.run/" target="_blank" rel="noopener noreferrer">Tiles</a>, a private and secure AI assistant for everyday use.</p>

<h2>The Python Problem</h2>

<p>Right now, we have a polyglot architecture where the control pane and CLI are written in Rust, while local model inference runs through a Python server as a daemon. Ideally, when we ship Tiles, we should also ship the required artifacts needed to run Python on the user’s system.</p>

<p>Since Python servers cannot be compiled into a single standalone binary, the user’s system must have a Python runtime available. More importantly, it must be a deterministic Python runtime so that the server runs exactly on the version developers expect.</p>

<p>In earlier releases of Tiles (before 0.4.0), we packed the server files into the final release tarball. During installation, we extracted them to the user’s system, downloaded <code>uv</code> (a Python package manager), installed Python 3.13 if it was not already present, and then ran the server as a daemon.</p>

<p>This approach had several issues:</p>

<ul>
  <li>Downloading development-related tools such as <code>uv</code> onto the user’s system</li>
  <li>Relying on <code>uv</code> at install time to manage dependencies and run the server</li>
  <li>Increased chances of failures due to dependency or runtime non-determinism</li>
  <li>Requiring internet access to download all of the above tools</li>
  <li>Lack of a fully deterministic runtime across operating systems</li>
</ul>

<p>One of the long-term goals of Tiles is complete portability. The previous approach did not align with that vision.</p>

<h2>Portable Runtimes</h2>

<p>To address these issues, we decided to ship the runtime along with the release tarball. We are now using <a href="https://lmstudio.ai/blog/venvstacks" target="_blank" rel="noopener noreferrer">venvstacks</a> by LM Studio to achieve this.</p>

<p>Venvstacks allows us to build a layered Python environment with three layers:</p>

<ul>
  <li><strong>Runtimes</strong><br />
  Defines the exact Python runtime version we need.</li>
  <li><strong>Frameworks</strong><br />
  Specifies shared Python frameworks such as NumPy, MLX, and others.</li>
  <li><strong>Applications</strong><br />
  Defines the actual server application and its specific dependencies.</li>
</ul>

<p>Similar to Docker, each layer depends on the layer beneath it. A change in any layer requires rebuilding that layer and the ones above it.</p>

<p>All components within a layer share the layers beneath them. For example, every framework uses the same Python runtime defined in the <code>runtimes</code> layer. Likewise, if we have multiple servers in the <code>applications</code> layer and both depend on MLX, they will share the exact deterministic MLX dependency defined in <code>frameworks</code>, as well as the same Python runtime defined in <code>runtimes</code>.</p>

<p>We define everything inside a <code>venvstacks.toml</code> file. Here is the <a href="https://github.com/tilesprivacy/tiles/blob/main/server/stack/venvstacks.toml" target="_blank" rel="noopener noreferrer">venvstacks.toml</a> used in Tiles.</p>

<p>Because we pin dependency versions in the TOML file, we eliminate non-determinism.</p>

<p>Internally, venvstacks uses <code>uv</code> to manage dependencies. Once the TOML file is defined, we run:</p>

<pre><code>venvstacks lock venvstacks.toml</code></pre>

<p>This resolves dependencies and creates the necessary folders, lock files, and metadata for each layer.</p>

<p>Next:</p>

<pre><code>venvstacks build venvstacks.toml</code></pre>

<p>This builds the Python runtime and environments based on the lock files.</p>

<p>Finally:</p>

<pre><code>venvstacks publish venvstacks.toml</code></pre>

<p>This produces reproducible tarballs for each layer. These tarballs can be unpacked on external systems and run directly.</p>

<p>We bundle the venvstack runtime artifacts into the final installer using this <a href="https://github.com/tilesprivacy/tiles/blob/main/scripts/bundler.sh" target="_blank" rel="noopener noreferrer">bundler script</a>. During installation, this <a href="https://github.com/tilesprivacy/tiles/blob/main/scripts/install.sh" target="_blank" rel="noopener noreferrer">installer script</a> extracts the venvstack tarballs into a deterministic directory.</p>

<p>Our Rust CLI can then predictably start the Python server using:</p>

<pre><code>stack_export_prod/app-server/bin/python -m server.main
</code></pre>

<h2>What’s Next</h2>

<p>We tested version 0.4.0 on clean macOS virtual machines to verify portability, and the approach worked well.</p>

<p>For now, we are focusing only on macOS. When we expand support to other operating systems, we will revisit this setup and adapt it as needed.</p>

<p>Packaging the runtime and dependencies increases the size of the final installer. We are exploring ways to reduce that footprint.</p>

<p>We also observed that changes in lock files can produce redundant application tarballs when running the <code>publish</code> command. More details are tracked in this <a href="https://github.com/tilesprivacy/tiles/issues/84" target="_blank" rel="noopener noreferrer">issue</a>.</p>

<p>Overall, we are satisfied with this approach for now.</p>

<p>Till then, keep on tiling.</p>]]></content:encoded>
      <pubDate>Tue, 17 Feb 2026 00:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://www.tiles.run/blog/move-along-python</guid>
    </item>
  </channel>
</rss>