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
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ jobs:
with:
task-cache-read-token: ${{ secrets.T3_TASK_CACHE_READ_TOKEN }}

- name: Verify resolved dependency security patches
run: vp run check:dependency-security

- name: Audit workspace dependencies for critical advisories
run: vp env exec -- pnpm audit --audit-level critical

# electron's install.js (run by ensure:electron) reads this cache before it
# downloads from GitHub. The key changes only with the Electron version.
- name: Cache Electron download
Expand Down Expand Up @@ -292,7 +298,8 @@ jobs:
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.test_web != 'false' }}
runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 10
# GitHub-hosted fork runners need setup and production-boot time beyond the web suite.
timeout-minutes: ${{ github.repository == 'pingdotgg/t3code' && 10 || 20 }}
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/fork-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ jobs:
pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata
run: vp install --filter=t3... --filter=@t3tools/web... --filter=@t3tools/desktop... --filter=@t3tools/scripts...

- name: Verify resolved dependency security patches
run: vp run check:dependency-security

- name: Audit release dependencies for critical advisories
run: vp env exec -- pnpm audit --audit-level critical

- name: Align package versions to release version
run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@t3tools/tailscale": "workspace:*",
"dbus-next": "0.10.2",
"effect": "catalog:",
"electron": "44.10.0",
"electron": "44.10.1",
"electron-store": "^8.2.0",
"electron-updater": "^6.8.9",
"electron-webauthn": "catalog:",
Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/src/preview/Manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,34 @@ describe("PreviewManager", () => {
),
);

effectIt.effect("omits data payloads from registered webview reports", () =>
withManager((manager) =>
Effect.gen(function* () {
const states: PreviewManager.PreviewTabState[] = [];
const dataUrl = "data:text/plain,SECRET-PAYLOAD";
fromId.mockReturnValue({
...makeTestPreviewWebContents(async () => ({
toJPEG: () => Buffer.from("unused"),
getSize: () => ({ width: 1280, height: 720 }),
})),
getURL: () => dataUrl,
} as never);
yield* manager.subscribeStateChanges((_tabId, state) =>
Effect.sync(() => {
states.push(state);
}),
);
yield* manager.createTab("tab_data_report");
yield* manager.registerWebview("tab_data_report", 42);
expect(states.at(-1)?.navStatus).toMatchObject({
kind: "Success",
url: "data:text/plain,…",
});
expect(JSON.stringify(states)).not.toContain("SECRET-PAYLOAD");
}),
),
);

effectIt.effect("queues navigation until the webview registers", () =>
withManager((manager) =>
Effect.gen(function* () {
Expand Down
11 changes: 9 additions & 2 deletions apps/desktop/src/preview/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type {
} from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { matchesKeybindingShortcut } from "@t3tools/shared/keybindings";
import { normalizePreviewUrl } from "@t3tools/shared/preview";
import { normalizePreviewUrl, withoutDataPayload } from "@t3tools/shared/preview";
import {
BrowserWindow,
ClipboardItem,
Expand Down Expand Up @@ -822,10 +822,17 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
);

const emit = Effect.fn("PreviewManager.emit")(function* (tabId: string, state: PreviewTabState) {
const reportedState =
state.navStatus.kind === "Idle"
? state
: {
...state,
navStatus: { ...state.navStatus, url: withoutDataPayload(state.navStatus.url) },
};
const listeners = yield* Ref.get(listenersRef);
yield* Effect.forEach(
listeners,
(listener) => deliverEvent("state-change", tabId, () => listener(tabId, state)),
(listener) => deliverEvent("state-change", tabId, () => listener(tabId, reportedState)),
{ discard: true },
);
});
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"generate:expo-schema": "node scripts/fetch-expo-schema.ts"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.276",
"@anthropic-ai/claude-agent-sdk": "0.3.276",
"@cursor/sdk": "1.0.38",
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
Expand Down
57 changes: 57 additions & 0 deletions apps/server/scripts/dependency-security.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from "@effect/vitest";
import { checkDependencySecurity } from "./dependency-security.ts";

const lock = (entries: string) =>
`lockfileVersion: '9.0'\nimporters: {}\npackages:\n /bootstrap@1.0.0:\n version: 1.0.0\n---\nimporters:\n apps/server: {}\n apps/desktop: {}\npackages:\n${entries || " {}"}`;

describe("checkDependencySecurity", () => {
it("selects the workspace document and checks supported majors and prereleases", () => {
const result = checkDependencySecurity(
lock(
[
" /undici@7.29.0:\n version: 7.29.0",
" /@modelcontextprotocol/sdk@1.31.0:\n version: 1.31.0",
" /undici@8.12.2-beta.1:\n version: 8.12.2-beta.1",
" /undici@6.29.1:\n version: 6.29.1",
" /zod@3.25.0:\n version: 3.25.0",
].join("\n"),
),
);
expect(result.packages).toEqual([
{ name: "undici", version: "7.29.0" },
{ name: "@modelcontextprotocol/sdk", version: "1.31.0" },
{ name: "undici", version: "8.12.2-beta.1" },
{ name: "undici", version: "6.29.1" },
]);
expect(result.violations).toEqual([
{ name: "undici", version: "7.29.0", minimum: "7.30.1" },
{ name: "@modelcontextprotocol/sdk", version: "1.31.0", minimum: "1.32.0" },
{ name: "undici", version: "8.12.2-beta.1", minimum: "8.12.2" },
]);
});

it("accepts patched versions and permits packages omitted by filtered artifacts", () => {
expect(
checkDependencySecurity(lock(" /undici@7.30.1:\n version: 7.30.1")).violations,
).toEqual([]);
expect(checkDependencySecurity(lock("")).packages).toEqual([]);
});

it("rejects a missing workspace document and malformed checked versions", () => {
expect(() => checkDependencySecurity("importers: {}\npackages: {}\n")).toThrow(
"exactly one workspace lockfile document",
);
expect(() => checkDependencySecurity(lock(" /undici@latest:\n version: latest"))).toThrow(
"Malformed resolved version for undici",
);
});
});

it("inspects the active server SDK against the approved bundle", async () => {
const { checkInstalledClaudeSdk } = await import("./dependency-security.ts");
const sdk = await checkInstalledClaudeSdk();
expect(sdk.path).toContain("claude-agent-sdk");
expect(sdk.version).toBe(sdk.expectedVersion);
expect(sdk.sha256).toMatch(/^[a-f0-9]{64}$/);
expect(sdk.violations.length).toBe(sdk.sha256 === sdk.expectedSha256 ? 0 : 1);
});
166 changes: 166 additions & 0 deletions apps/server/scripts/dependency-security.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env node
// @effect-diagnostics nodeBuiltinImport:off - Offline CLI reads one lockfile without an Effect runtime.
import * as NodeCrypto from "node:crypto";
import * as NodeModule from "node:module";
import * as NodeFSP from "node:fs/promises";
import * as NodePath from "node:path";
import { parseAllDocuments } from "yaml";
import * as Schema from "effect/Schema";
import { compareSemverVersions, parseSemver } from "@t3tools/shared/semver";

const WorkspaceLock = Schema.Struct({
importers: Schema.Record(Schema.String, Schema.Unknown),
packages: Schema.Record(Schema.String, Schema.Unknown),
});
const PackageInfo = Schema.Record(Schema.String, Schema.Unknown);
const decodeWorkspaceLock = Schema.decodeUnknownSync(WorkspaceLock);
const SdkPackage = Schema.Struct({ version: Schema.String });
const decodeSdkPackage = Schema.decodeUnknownSync(SdkPackage);
const decodePackageInfo = Schema.decodeUnknownSync(PackageInfo);

const minimums: Readonly<Record<string, Readonly<Record<number, string>>>> = {
undici: { 6: "6.29.1", 7: "7.30.1", 8: "8.12.2" },
zod: { 4: "4.6.6" },
"body-parser": { 2: "2.4.0" },
seroval: { 1: "1.6.11" },
"proxy-addr": { 2: "2.0.8" },
hono: { 4: "4.13.11" },
"ip-address": { 10: "10.7.4" },
qs: { 6: "6.16.1" },
"fast-uri": { 3: "3.1.9" },
sharp: { 0: "0.35.5" },
katex: { 0: "0.18.10" },
"@modelcontextprotocol/sdk": { 1: "1.32.0" },
"node-forge": { 1: "1.4.1" },
"shell-quote": { 1: "1.11.0" },
"smol-toml": { 1: "1.9.0" },
"path-to-regexp": { 6: "6.3.0" },
xml2js: { 0: "0.5.0" },
};

export interface DependencySecurityResult {
readonly packages: ReadonlyArray<{ name: string; version: string }>;
readonly violations: ReadonlyArray<{ name: string; version: string; minimum: string }>;
}

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);

export function checkDependencySecurity(source: string): DependencySecurityResult {
const documents = parseAllDocuments(source);
if (documents.some((document) => document.errors.length > 0)) {
throw new Error("Lockfile contains invalid YAML");
}
const matching = documents.flatMap((document) => {
const raw: unknown = document.toJSON();
if (!isRecord(raw) || !isRecord(raw.importers)) return [];
const importerKeys = Object.keys(raw.importers);
return importerKeys.includes("apps/server") && importerKeys.includes("apps/desktop")
? [decodeWorkspaceLock(raw)]
: [];
});
if (matching.length !== 1) throw new Error("Expected exactly one workspace lockfile document");

const packages: { name: string; version: string }[] = [];
const violations: { name: string; version: string; minimum: string }[] = [];
for (const [key, rawInfo] of Object.entries(matching[0]!.packages)) {
const normalizedKey = key.startsWith("/") ? key.slice(1) : key;
const nameVersion = normalizedKey.match(/^(.*)@([^@]+)$/);
if (!nameVersion) continue;
const [, name = "", keyVersion = ""] = nameVersion;
if (!minimums[name]) continue;
decodePackageInfo(rawInfo);
const version = keyVersion.match(/^([^ (]+)/)?.[1];
if (!version) throw new Error(`Malformed resolved version for ${name}`);
const parsed = parseSemver(version);
if (!parsed) throw new Error(`Malformed resolved version for ${name}`);
const policy = minimums[name]![parsed.major];
if (!policy) continue;
packages.push({ name, version });
if (compareSemverVersions(version, policy) < 0) {
violations.push({ name, version, minimum: policy });
}
}
return { packages, violations };
}

const sdkVersion = "0.3.276";
const sdkSha256 = "7bf61ce680ce009425c2a9db2d6e54a2aff91c756bcdc970459b31b59d446348";
const serverRequire = NodeModule.createRequire(new URL("../package.json", import.meta.url));

export async function checkInstalledClaudeSdk() {
try {
const sdkPath = serverRequire.resolve("@anthropic-ai/claude-agent-sdk");
const metadata = decodeSdkPackage(
JSON.parse(
await NodeFSP.readFile(NodePath.join(NodePath.dirname(sdkPath), "package.json"), "utf8"),
) as unknown,
);
const sha256 = NodeCrypto.createHash("sha256")
.update(await NodeFSP.readFile(sdkPath))
.digest("hex");
const violations: string[] = [];
if (metadata.version !== sdkVersion) {
violations.push(`Expected Claude SDK ${sdkVersion}; found ${metadata.version}.`);
}
if (sha256 !== sdkSha256) {
violations.push(
`Claude SDK bundle SHA-256 ${sha256} differs from approved patched ${sdkSha256}.`,
);
}
return {
path: sdkPath,
version: metadata.version,
sha256,
expectedVersion: sdkVersion,
expectedSha256: sdkSha256,
violations,
};
} catch (error) {
return {
path: null,
version: null,
sha256: null,
expectedVersion: sdkVersion,
expectedSha256: sdkSha256,
violations: [
`Could not inspect active Claude SDK: ${error instanceof Error ? error.message : String(error)}`,
],
};
}
}

export async function runDependencySecurity(
lockfilePath = NodePath.resolve("pnpm-lock.yaml"),
json = false,
) {
const result = checkDependencySecurity(await NodeFSP.readFile(lockfilePath, "utf8"));
const claudeSdk = await checkInstalledClaudeSdk();
if (json) {
process.stdout.write(`${JSON.stringify({ ...result, claudeSdk }, null, 2)}\n`);
} else {
process.stdout.write(
`Checked ${result.packages.length} resolved packages; ${result.violations.length} below minimums.\n`,
);
process.stdout.write(
`Claude SDK embedded v4 array patch: ${claudeSdk.violations.length ? "guard failed" : "approved bundle"}.\n`,
);
for (const violation of claudeSdk.violations) process.stdout.write(`${violation}\n`);
for (const violation of result.violations) {
process.stdout.write(
`${violation.name}@${violation.version} is below ${violation.minimum}\n`,
);
}
}
if (result.violations.length > 0 || claudeSdk.violations.length > 0) process.exitCode = 1;
}

if (import.meta.main) {
const json = process.argv.includes("--json");
runDependencySecurity(undefined, json).catch((error: unknown) => {
process.stderr.write(
`${error instanceof Error ? error.message : "Dependency security check failed"}\n`,
);
process.exitCode = 1;
});
}
Loading
Loading