Skip to content
Draft
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ If a tradeoff is required, choose correctness, durability, and debuggability ove

## Current Runtime Architecture

- Desktop artifact builds record the resolved GitHub update target in the staged runtime manifest. The packaged audit must bind `app-update.yml` to that target, never to the launch environment or a hard-coded fork. Legacy manifests without the field retain the official upstream target. Reject malformed targets, duplicate metadata keys, YAML aliases/nesting, and endpoint overrides; preserve the existing packaging and signing paths on every platform.

- Backend startup must await the final successful `ProviderDaemonManager.ensureRunning` result, including its authenticated lease, before launching the backend. A provisional `currentConfig` can belong to a daemon attempt that is subsequently replaced on the same socket; inheriting it strands the backend with a rejected credential even though the replacement daemon is healthy. IPC/settings preparation may overlap daemon readiness, but provider-capable backend work must not. Startup failure or quitting during readiness must never launch the backend. Keep regression coverage for a superseded first attempt, final readiness failure, and cancellation, and preserve the separate stop-backend -> recover-daemon -> start-backend watchdog ordering.
- Daemon marker existence/read/decode errors are inconclusive ownership, not absence. Fail with a content-free marker-inspection error before entering spawn/unlink/reaping paths; only confirmed marker absence permits a fresh daemon. A later retry can adopt the same live owner after the filesystem recovers.

Expand Down
74 changes: 73 additions & 1 deletion apps/desktop/src/app/DesktopArtifactAudit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { assert, describe, it } from "@effect/vitest";
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { vi } from "vitest";

import {
auditPackagedDesktopArtifact,
containsDesktopArtifactResidue,
containsDesktopArtifactSecretMaterial,
isDesktopRuntimeManifestValid,
Expand Down Expand Up @@ -52,12 +57,79 @@ describe("DesktopArtifactAudit", () => {
assert.isFalse(containsDesktopArtifactSecretMaterial("erilog-sk-prompt-state-selector"));
});

it("requires updater metadata to target the official release repository", () => {
it("uses the official target only when a build-time target is absent", () => {
assert.isTrue(
isDesktopUpdateMetadataValid("provider: github\nowner: cafeai\nrepo: cafe-code\n"),
);
assert.isFalse(
isDesktopUpdateMetadataValid("provider: generic\nurl: https://updates.invalid\n"),
);
assert.isFalse(
isDesktopUpdateMetadataValid("provider: github\nowner: cafeai\nrepo: cafe-code\n", null),
);
});

it("matches the configured fork and accepts the builder's quoted cache and nightly fields", () => {
const target = { provider: "github", owner: "John-Ryan21337", repo: "club-code" };
const metadata =
"owner: John-Ryan21337\nrepo: club-code\nprovider: github\nreleaseType: prerelease\nchannel: nightly\nupdaterCacheDirName: '@cafecodedesktop-runtime-updater'\n";
assert.isTrue(isDesktopUpdateMetadataValid(metadata, target));
assert.isFalse(isDesktopUpdateMetadataValid(metadata));
assert.isFalse(isDesktopUpdateMetadataValid(metadata, { ...target, repo: "other" }));
assert.isFalse(isDesktopUpdateMetadataValid(metadata, { ...target, owner: "other" }));
});

it("rejects ambiguous metadata and endpoint overrides even when matching lines are present", () => {
const valid = "provider: github\nowner: cafeai\nrepo: cafe-code\n";
for (const suffix of [
"owner: attacker\n",
"repo: other\n",
"provider: generic\n",
"host: updates.invalid\n",
"url: https://updates.invalid\n",
"protocol: http\n",
"path: alternate\n",
"<<: *override\n",
"channel: unknown\n",
]) {
assert.isFalse(isDesktopUpdateMetadataValid(valid + suffix), suffix);
}
assert.isFalse(isDesktopUpdateMetadataValid(valid + " ".repeat(16_384)));
assert.isFalse(isDesktopRuntimeManifestValid({ ...validManifest, cafeCodeUpdateTarget: null }));
assert.isFalse(
isDesktopRuntimeManifestValid({
...validManifest,
cafeCodeUpdateTarget: { provider: "github", owner: "cafeai", repo: "../other" },
}),
);
});

it("binds the packaged audit to the manifest instead of the current process environment", async () => {
const resources = await mkdtemp(join(tmpdir(), "desktop-audit-target-"));
const appArchive = join(resources, "app.asar");
const target = { provider: "github", owner: "release-owner", repo: "desktop-releases" };
try {
await mkdir(join(appArchive, "apps"), { recursive: true });
await writeFile(join(appArchive, "apps", "main.js"), "console.log('desktop');\n");
await writeFile(
join(appArchive, "package.json"),
JSON.stringify({ ...validManifest, cafeCodeUpdateTarget: target }),
);
vi.stubEnv("GITHUB_REPOSITORY", "attacker/other");
vi.stubEnv("CAFE_CODE_DESKTOP_UPDATE_REPOSITORY", "attacker/other");
await writeFile(
join(resources, "app-update.yml"),
"provider: github\nowner: release-owner\nrepo: desktop-releases\n",
);
assert.isTrue(await auditPackagedDesktopArtifact(resources, "win32"));
await writeFile(
join(resources, "app-update.yml"),
"provider: github\nowner: attacker\nrepo: other\n",
);
assert.isFalse(await auditPackagedDesktopArtifact(resources, "win32"));
} finally {
vi.unstubAllEnvs();
await rm(resources, { recursive: true, force: true });
}
});
});
74 changes: 68 additions & 6 deletions apps/desktop/src/app/DesktopArtifactAudit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,30 @@ function validDependencyMap(value: unknown): boolean {
);
}

const DEFAULT_UPDATE_TARGET = { provider: "github", owner: "cafeai", repo: "cafe-code" };

function readUpdateTarget(value: unknown): Record<string, unknown> | undefined {
const target = readRecord(value);
return target?.provider === "github" &&
typeof target.owner === "string" &&
/^[a-z0-9][a-z0-9-]{0,38}$/iu.test(target.owner) &&
typeof target.repo === "string" &&
/^[a-z0-9_.-]{1,100}$/iu.test(target.repo) &&
target.repo !== "." &&
target.repo !== ".."
? target
: undefined;
}

export function isDesktopRuntimeManifestValid(value: unknown): boolean {
const manifest = readRecord(value);
if (
manifest?.name !== "@cafecode/desktop-runtime" ||
manifest.private !== true ||
manifest.main !== "apps/desktop/dist-electron/main.cjs" ||
!validDependencyMap(manifest.dependencies)
!validDependencyMap(manifest.dependencies) ||
(manifest.cafeCodeUpdateTarget !== undefined &&
readUpdateTarget(manifest.cafeCodeUpdateTarget) === undefined)
) {
return false;
}
Expand Down Expand Up @@ -106,11 +123,53 @@ export function containsDesktopArtifactResidue(path: string, source?: string): b
);
}

export function isDesktopUpdateMetadataValid(source: string): boolean {
export function isDesktopUpdateMetadataValid(
source: string,
expectedTarget: unknown = DEFAULT_UPDATE_TARGET,
): boolean {
const target = readUpdateTarget(expectedTarget);
if (!target || source.length > 16_384) return false;

// Accept only the flat scalar metadata emitted by our builder configuration.
// Duplicate keys, aliases, nested values, and endpoint overrides must fail.
const fields = new Map<string, string>();
const allowedFields = new Set([
"provider",
"owner",
"repo",
"channel",
"releaseType",
"updaterCacheDirName",
]);
for (const line of source.split(/\r?\n/u)) {
if (line.trim() === "") continue;
const match = /^([a-zA-Z]+):[ \t]*(\S(?:.*\S)?)[ \t]*$/u.exec(line);
if (!match) return false;
const [, key, raw] = match;
if (!key || !raw || !allowedFields.has(key) || fields.has(key)) return false;
let value = raw;
if (raw.startsWith('"')) {
try {
const parsed: unknown = JSON.parse(raw);
if (typeof parsed !== "string") return false;
value = parsed;
} catch {
return false;
}
} else if (raw.startsWith("'")) {
if (!/^'(?:[^']|'')*'$/u.test(raw)) return false;
value = raw.slice(1, -1).replaceAll("''", "'");
} else if (!/^[a-z0-9_@./-]+$/iu.test(raw)) {
return false;
}
fields.set(key, value);
}
return (
/^provider:\s*github\s*$/mu.test(source) &&
/^owner:\s*cafeai\s*$/mu.test(source) &&
/^repo:\s*cafe-code\s*$/mu.test(source)
fields.get("provider") === target.provider &&
fields.get("owner") === target.owner &&
fields.get("repo") === target.repo &&
(!fields.has("channel") || ["latest", "nightly"].includes(fields.get("channel")!)) &&
(!fields.has("releaseType") || ["release", "prerelease"].includes(fields.get("releaseType")!))
);
}

Expand Down Expand Up @@ -167,7 +226,10 @@ export async function auditPackagedDesktopArtifact(
]);
if (resourceEntries.some((entry) => !expectedTopLevelEntries.has(entry.name))) return false;
if (
!isDesktopUpdateMetadataValid(await readFile(join(resourcesPath, "app-update.yml"), "utf8"))
!isDesktopUpdateMetadataValid(
await readFile(join(resourcesPath, "app-update.yml"), "utf8"),
readRecord(manifest)?.cafeCodeUpdateTarget,
)
) {
return false;
}
Expand Down
45 changes: 44 additions & 1 deletion scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import { afterEach, beforeEach, vi } from "vitest";

import {
MANAGED_WINDOWS_NODE_VERSION,
createBuildConfig,
desktopArtifactListSatisfiesTarget,
resolveBuildOptions,
resolveDesktopBuildIconAssets,
Expand All @@ -23,7 +25,14 @@ import {
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";

it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
it("always emits deterministic official updater metadata", () => {
beforeEach(() => {
// Each case chooses its update source independently of the CI repository.
vi.stubEnv("CAFE_CODE_DESKTOP_UPDATE_REPOSITORY", undefined);
vi.stubEnv("GITHUB_REPOSITORY", undefined);
});
afterEach(() => vi.unstubAllEnvs());

it("emits official updater metadata when repository settings are absent", () => {
assert.deepStrictEqual(resolveGitHubPublishConfig("latest"), {
provider: "github",
owner: "cafeai",
Expand All @@ -39,6 +48,40 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
});
});

it.effect("binds the manifest to the resolved publish identity on each platform", () =>
Effect.gen(function* () {
for (const [workflowRepository, explicitRepository, owner, repo] of [
[undefined, undefined, "cafeai", "cafe-code"],
["fork-owner/cafe-fork", undefined, "fork-owner", "cafe-fork"],
[
"fork-owner/cafe-fork",
"release-owner/desktop-releases",
"release-owner",
"desktop-releases",
],
]) {
vi.stubEnv("GITHUB_REPOSITORY", workflowRepository);
vi.stubEnv("CAFE_CODE_DESKTOP_UPDATE_REPOSITORY", explicitRepository);
for (const platform of ["mac", "linux", "win"] as const) {
const config = yield* createBuildConfig(
platform,
platform === "mac" ? "dmg" : platform === "linux" ? "AppImage" : "nsis",
"0.0.17-nightly.20260413.42",
false,
false,
undefined,
);
assert.deepStrictEqual(config.extraMetadata, {
cafeCodeUpdateTarget: { provider: "github", owner, repo },
});
assert.deepStrictEqual(config.publish, [
{ provider: "github", owner, repo, releaseType: "prerelease", channel: "nightly" },
]);
}
}
}),
);

it("resolves the dedicated nightly updater channel from nightly versions", () => {
assert.equal(resolveDesktopUpdateChannel("0.0.17-nightly.20260413.42"), "nightly");
assert.equal(resolveDesktopUpdateChannel("0.0.17"), "latest");
Expand Down
9 changes: 8 additions & 1 deletion scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,7 @@ export function resolveLinuxDesktopBuildConfig(target: string): Record<string, u
};
}

const createBuildConfig = Effect.fn("createBuildConfig")(function* (
export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
platform: typeof BuildPlatform.Type,
target: string,
version: string,
Expand All @@ -935,6 +935,13 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* (
const publishConfig = resolveGitHubPublishConfig(updateChannel);
if (publishConfig) {
buildConfig.publish = [publishConfig];
buildConfig.extraMetadata = {
cafeCodeUpdateTarget: {
provider: publishConfig.provider,
owner: publishConfig.owner,
repo: publishConfig.repo,
},
};
} else if (mockUpdates) {
buildConfig.publish = [
{
Expand Down
Loading