Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ packages/ai/src/providers/data
packages/coding-agent/binaries
*.log
*.tsbuildinfo

.env
.env.*
**/.env
**/.env.*
**/__pycache__
85 changes: 41 additions & 44 deletions .github/workflows/runtime-image.yml
Original file line number Diff line number Diff line change
@@ -1,67 +1,64 @@
name: Runtime Image

on:
push:
branches: [main]
paths:
- '.github/workflows/runtime-image.yml'
- '.dockerignore'
- 'Dockerfile.runtime'
- 'package.json'
- 'package-lock.json'
- 'packages/**'
- 'scripts/**'
pull_request:
branches: [main]
paths:
- '.github/workflows/runtime-image.yml'
- '.dockerignore'
- 'Dockerfile.runtime'
- 'package.json'
- 'package-lock.json'
- 'packages/**'
- 'scripts/**'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: runtime-image-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: false

jobs:
build:
# Publication is paused until the runtime image and deployment consumers are validated.
if: ${{ false }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
validate:
runs-on: ubuntu-24.04
timeout-minutes: 25
steps:
- name: Checkout
- name: Checkout candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

with:
persist-credentials: false
- name: Resolve candidate identity
id: source
run: echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0

- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and publish immutable runtime image
- name: Prepare private evidence directory
run: mkdir -p .artifacts/runtime
- name: Build Linux amd64 candidate locally
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: Dockerfile.runtime
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ghcr.io/diffexai/diffex-runtime:sha-${{ github.sha }}
load: true
push: false
tags: diffex-runtime:candidate
build-args: SOURCE_COMMIT=${{ steps.source.outputs.commit }}
cache-from: type=gha,scope=diffex-runtime
cache-to: type=gha,mode=max,scope=diffex-runtime
provenance: mode=max
sbom: true
# Docker's local exporter cannot retain attestations.
# GHCR publication requires a separate reviewed OCI attestation gate.
provenance: false
sbom: false
- name: Validate hosted process without external networking
run: |
docker run --rm --network none \
--mount "type=bind,src=$GITHUB_WORKSPACE/scripts/fixtures,dst=/acceptance,readonly" \
--entrypoint node diffex-runtime:candidate /acceptance/runtime-smoke.mjs \
> .artifacts/runtime/smoke.json
- name: Inspect every candidate image layer
run: |
docker save --output .artifacts/runtime/runtime.tar diffex-runtime:candidate
python3 scripts/check-runtime-image.py .artifacts/runtime/runtime.tar > .artifacts/runtime/payload.json
docker inspect diffex-runtime:candidate > .artifacts/runtime/image.json
sha256sum .artifacts/runtime/runtime.tar > .artifacts/runtime/SHA256SUMS
- name: Retain tested image and private evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: runtime-candidate-${{ steps.source.outputs.commit }}
path: .artifacts/runtime/
if-no-files-found: error
retention-days: 14
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,9 @@ If the user's instructions conflict with any rule in this document, ask for expl
- 2026-09-13: Node rejects package import targets containing node_modules -> Preserve existing package import targets and validate staged tarballs through installed CLI startup before publication.

- 2026-09-13: npm accepted publication before registry metadata propagated -> Preserve the uploaded artifact identity and resume verification without republishing or rebuilding.

- 2026-09-13: Guessed release helper paths and reused npm caches obscured validation -> Discover paths before reading and use a fresh cache when proving that an update downloads artifact bytes.

- 2026-09-13: Assumed interrupted npm downloads preserve the prefix -> Test real npm processes, retain user data, and recover ENOTEMPTY by matching the affected installation or backup against actual isolated directories instead of assuming every failure is a backup rename.

- 2026-09-13: Runtime staging copied a redundant project NOTICE after attribution was consolidated -> Generate only the consolidated attribution file and reject separate NOTICE payloads.
28 changes: 21 additions & 7 deletions Dockerfile.runtime
Original file line number Diff line number Diff line change
@@ -1,28 +1,42 @@
FROM node:24-bookworm-slim AS build
FROM node:24.16.0-bookworm-slim@sha256:2c87ef9bd3c6a3bd4b472b4bec2ce9d16354b0c574f736c476489d09f560a203 AS build

WORKDIR /opt/diffex

COPY package.json package-lock.json ./
COPY tsconfig.base.json ./
ARG SOURCE_COMMIT
ENV DIFFEX_SOURCE_COMMIT=$SOURCE_COMMIT

COPY package.json package-lock.json tsconfig.base.json ./
COPY LICENSE ./
COPY legal ./legal
COPY docs/release/distribution-policy.json ./docs/release/distribution-policy.json
COPY packages ./packages
COPY scripts ./scripts

RUN npm ci --ignore-scripts
RUN npm run build
RUN npm run hydrate:model-data && npm run build:offline
RUN npm prune --omit=dev --ignore-scripts
RUN node scripts/stage-runtime-distribution.mjs /opt/runtime

FROM node:24-bookworm-slim AS runtime
FROM node:24.16.0-bookworm-slim@sha256:2c87ef9bd3c6a3bd4b472b4bec2ce9d16354b0c574f736c476489d09f560a203 AS runtime

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& sed -i "s|http://deb.debian.org|https://deb.debian.org|g" /etc/apt/sources.list.d/debian.sources \
&& apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates fd-find git openssh-client ripgrep \
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
&& rm -rf /var/lib/apt/lists/*

COPY --from=build /opt/diffex /opt/diffex
COPY --from=build /opt/runtime /opt/diffex

RUN ln -s /opt/diffex/packages/coding-agent-server/dist/cli.js /usr/local/bin/diffex-runtime-server

WORKDIR /workspace

ARG SOURCE_COMMIT
LABEL org.opencontainers.image.source="https://github.com/diffexai/diffex"
LABEL org.opencontainers.image.description="Pi-based hosted Diffex coding-agent runtime"
LABEL org.opencontainers.image.revision=$SOURCE_COMMIT
LABEL org.opencontainers.image.licenses="Apache-2.0 AND MIT"
LABEL org.opencontainers.image.description="Hosted Diffex coding-agent runtime"

CMD ["diffex-runtime-server"]
9 changes: 5 additions & 4 deletions docs/cloud-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ Only Daytona's opaque placeholder enters the sandbox environment, and the real p

## Runtime Image

`Dockerfile.runtime` produces the AMD64 image used to create the Daytona sandbox snapshot.
The runtime-image workflow is disabled during the identity migration, including manual dispatches.
Re-enable the workflow for validation and immutable `ghcr.io/diffexai/diffex-runtime:sha-<commit>` publication after reviewing the image and its consumers.
Keep production on its existing tested image and Daytona snapshot until the replacement image and snapshot pass release validation.
`Dockerfile.runtime` produces a Linux amd64 candidate with a digest-pinned Node base image, compiled workspace outputs, user documentation, and legal notices.
The Runtime Image workflow now permits manual validation and retains the tested image privately.
Registry publication and automatic main-branch publication remain disabled.
The owner confirmed on 2026-09-13 that no staging deployment or Daytona consumer is configured, so no existing production digest or snapshot can be assumed.
See [runtime validation and promotion prerequisites](release/runtime-validation.md) for local commands and the pending hosted acceptance.

Before a cloud release, register GHCR with Daytona, create a Daytona snapshot from that exact immutable image tag or digest, and set `DAYTONA_SANDBOX_SNAPSHOT` to the resulting snapshot name.
Do not use a floating image tag for production.
Expand Down
15 changes: 15 additions & 0 deletions docs/release/artifact-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ Acceptance executes file discovery and content search, records their versions, a
Native runners install those prerequisites through apt or Homebrew before package acceptance.
This validates the npm installation with controlled system helpers; it does not establish the separate automatic-helper-download lifecycle.

## Installed lifecycle acceptance

The acceptance command also runs `scripts/npm-lifecycle.mjs` against the exact reviewed CLI tarball.
It installs globally into an isolated prefix, checks PATH, version, help, and models, then invokes the installed self-updater with real npm subprocesses.
Only discovery and the Diffex-scoped registry are controlled fixtures; dependency downloads use npmjs.org.
An isolated version-metadata predecessor exercises upgrade to the reviewed candidate without publishing a fake release.
Fresh caches require actual target downloads.
Malformed and foreign metadata, redirects, outages, no release, and checksum mismatches are rejected without changing the installed prefix, including force.
An interrupted npm download demonstrates the documented pinned repair and, when npm reports `ENOTEMPTY`, relocation of the affected CLI installation or its exact stale backup before retrying.
Interruption itself can mutate npm's prefix and is not claimed to preserve the executable.
Uninstall verifies executable removal and retained global and project data.
The same harness verifies Bun global installation and PATH, rejects a foreign release identity without mutation, upgrades with the real Bun subprocess, checks the installed runtime hashes, and uninstalls while retaining user and project data.
An isolated Bun global scope registry ensures that the upgrade receives the reviewed candidate bytes.
`npm-lifecycle.json` records commands, identities, results, limitations, and cleanup.

## Supported distribution scope

The approved channel remains npm, installed with npm or Bun and executed on Node 22.19.0 or newer.
Expand Down
5 changes: 3 additions & 2 deletions docs/release/distribution-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"HTML export templates and their reviewed marked/highlight.js bundles",
"runtime theme and model JSON",
"SQLite migration SQL",
"documentation including inline examples",
"user-facing documentation explicitly listed in scripts/distribution-docs.json",
"Diffex-owned TUI native prebuilds using system APIs"
],
"publicHosting": "npmjs",
Expand All @@ -55,5 +55,6 @@
"protocol",
"telemetry",
"tui"
]
],
"developmentDocumentation": "exclude from all distribution artifacts, including bootstrap and hosted runtime; keep private development records in the source repository"
}
74 changes: 74 additions & 0 deletions docs/release/runtime-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Hosted runtime candidate validation

The owner confirmed on 2026-09-13 that no staging deployment or Daytona consumer is configured.
Local runtime validation is the available acceptance boundary.
It does not promote a hosted deployment or establish a prior-runtime rollback baseline.
The release remains npm-only; this image is not an interactive CLI installation channel.

## Build and inspect locally

Run from the private source checkout with Docker running:

```sh
DIFFEX_RUNTIME_COMMIT="$(git rev-parse HEAD)"
docker build --platform linux/amd64 \
--build-arg "SOURCE_COMMIT=$DIFFEX_RUNTIME_COMMIT" \
--file Dockerfile.runtime --tag diffex-runtime:candidate .
docker run --rm --platform linux/amd64 --network none \
--mount "type=bind,src=$PWD/scripts/fixtures,dst=/acceptance,readonly" \
--entrypoint node diffex-runtime:candidate /acceptance/runtime-smoke.mjs
docker save --output /tmp/diffex-runtime-candidate.tar diffex-runtime:candidate
python3 scripts/check-runtime-image.py /tmp/diffex-runtime-candidate.tar
docker inspect diffex-runtime:candidate
shasum -a 256 /tmp/diffex-runtime-candidate.tar
```

Use a clean committed source for retained acceptance and record its full commit.
Dirty-checkout results must also identify the uncommitted diff and cannot be attributed solely to the base commit label.
The Dockerfile pins the Node 24.16.0 base manifest digest and uses Linux amd64.
It hydrates validated model data, compiles workspace outputs, prunes development dependencies, and stages the required hosted runtime dependency closure.
The image carries compiled files, native runtime assets, the explicit user-documentation allowlist, and legal notices.
Project build inputs and private development documentation stay in the build stage.
Third-party redistribution materials remain with dependencies.

The fixture is mounted read-only for testing and never copied into a distribution layer.
It launches the actual server executable, uses a mock model provider and usage endpoint, verifies anonymous and incorrect-credential denial, rejects another session ID, exchanges a prompt and reply, and restarts using persisted session data.
External networking is disabled by Docker.
The test removes its directories and the container exits cleanly.
The layer checker examines every saved image layer so deleting an internal document in a later layer cannot hide its inclusion.

The manually dispatched Runtime Image workflow runs the same build, smoke, and layer checks on Linux amd64.
It retains the tested Docker archive, local image ID, archive checksum, and evidence in private Actions storage for 14 days.
It has read-only repository permissions and no registry publishing job.
There is no automatic main-branch image publication.

## Local evidence

On 2026-09-13, the development candidate passed the Linux amd64 build and smoke on Docker running on Darwin arm64.
The initial local image ID was `sha256:cfd4495ec712460a1400a1f6dda7f4352b7e48a659bed79505cf0c2f3714aad9`.
Its source label identifies base commit `06830a99220c3dad14292830171c433c916d49b1`; the branch's uncommitted runtime and packaging changes were also build inputs.
Layer inspection accepted 1,426 project files across 12 runtime workspaces and all 30 approved documentation assets.
This development image is not a registry release digest or a promoted artifact.
Final committed-candidate workflow evidence belongs in the implementation PR.

The npm lifecycle harness also passed on Darwin arm64 with Node 22.19.0, npm 10.9.3, and Bun 1.3.11.
It used the local candidate tarball, real npm installation processes, controlled release discovery, and a loopback registry for the canonical package.
Malformed release inputs and checksum mismatch preserved the installed prefix.
An interrupted download changed npm's prefix and required moving the exact stale backup before a pinned repair succeeded.
Bun global installation, PATH, real self-update, installed-byte verification, foreign-identity rejection, and removal also passed.
Removal retained user and project data, and all test-owned directories were removed.
Its version-metadata predecessor is a simulation; public-to-public upgrade acceptance remains separate.

## Registry and deployment prerequisites

Keep the future image private at `ghcr.io/diffexai/diffex-runtime` and configure scoped publisher and pull identities before publication.
Review the base-image and dependency license inventory, generate OCI SBOM and provenance attestations, and retain the image's registry manifest digest with its exact source and model-data inputs.
Docker's local exporter does not retain attestations, so the local archive does not claim SBOM/provenance acceptance.
Use a registry or OCI exporter for attested artifacts as described in [Docker's attestation documentation](https://docs.docker.com/build/metadata/attestations/).

The first hosted acceptance must pull the exact candidate from the intended staging environment, create a Daytona snapshot from its digest, and configure the cloud consumer explicitly.
Record the cloud commit, runtime commit, registry digest, snapshot, deployment owner, architecture, pull identity, and approved credential source without credential values.
Exercise real cloud allocation, attachment, streaming, reconnect, stop, cross-actor denial, and cleanup.
Only then retain the tested digest and snapshot as the first recovery baseline.
Every replacement must keep its previous tested configuration available, demonstrate restoration, and verify cleanup before retirement.
Registry publication, attestations, real provider inference, deployment promotion, and prior-runtime rollback are not completed by local testing.
Loading
Loading