Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
a1c7e0f
feat: add native sidecar containment
qisoft Aug 26, 2026
2324cbd
fix: harden platform containment gates
qisoft Aug 26, 2026
3492341
fix: run sandboxed macOS smoke through XPC
qisoft Aug 26, 2026
5dc0f5f
fix: use compatible systemd scope options
qisoft Aug 26, 2026
c2866dd
fix: fail closed across native setup edges
qisoft Aug 26, 2026
59e1b17
test: report packaged proof failures
qisoft Aug 26, 2026
7d8d167
fix: package macOS XPC containment boundary
qisoft Aug 26, 2026
ff0d8ac
test: report Windows containment setup stage
qisoft Aug 26, 2026
ca2a9e9
fix: close native containment review gaps
qisoft Aug 26, 2026
5ca389a
test: make containment proof non-vacuous
qisoft Aug 26, 2026
bf5c33b
fix: assign Windows containment job atomically
qisoft Aug 26, 2026
f4ca781
fix: keep containment cleanup failures observable
qisoft Aug 26, 2026
1428038
fix: clean invalid AppContainer preparations
qisoft Aug 26, 2026
398151a
fix: constrain AppContainer profile cleanup root
qisoft Aug 26, 2026
3ae0db5
fix: reject cross-root profile cleanup paths
qisoft Aug 26, 2026
6030ca0
fix: provide required AppContainer environment
qisoft Aug 26, 2026
12ccaae
test: surface packaged sidecar diagnostics
qisoft Aug 26, 2026
c1b6e4c
ci: rerun packaged containment proof
qisoft Aug 26, 2026
bcade81
fix: redirect Windows sidecar profile environment
qisoft Aug 26, 2026
adb5744
fix: verify platform-specific sidecar environment
qisoft Aug 26, 2026
562c710
fix: keep sidecar diagnostics private
qisoft Aug 26, 2026
59dbed9
fix: redact packaged proof failures
qisoft Aug 26, 2026
2420609
test: report private packaged proof stages
qisoft Aug 27, 2026
2534665
fix: preserve packaged cleanup diagnostics
qisoft Aug 27, 2026
a2957b1
fix: classify packaged proof preflight failures
qisoft Aug 27, 2026
627d67b
fix: preserve preflight cleanup failures
qisoft Aug 27, 2026
f26e791
fix: retry AppContainer profile rollback
qisoft Aug 27, 2026
139f00f
test: classify adversarial containment failures
qisoft Aug 27, 2026
cfd8438
fix: classify internal proof cleanup failures
qisoft Aug 27, 2026
6e9ad8f
fix: classify lifecycle tail cleanup
qisoft Aug 27, 2026
ae7ad45
fix: distinguish lifecycle invariant failures
qisoft Aug 27, 2026
8979aa8
test: classify contained probe exceptions
qisoft Aug 27, 2026
7c4dcfb
fix: avoid AppContainer metadata traversal
qisoft Aug 27, 2026
732f4a7
fix: precreate containment link probes
qisoft Aug 27, 2026
4fe579c
fix: prove reparse escape containment
qisoft Aug 27, 2026
0413590
fix: require exact reparse rejection
qisoft Aug 27, 2026
9496a49
fix: classify environment proof failures
qisoft Aug 27, 2026
84e58de
fix: pinpoint environment redirect failures
qisoft Aug 27, 2026
a585fc6
fix: verify disposable temp redirects
qisoft Aug 27, 2026
68aa266
test: expect platform redirect fields
qisoft Aug 27, 2026
fa3be0c
fix: isolate local app data per job
qisoft Aug 27, 2026
0ff05b1
fix: accept contained app data remapping
qisoft Aug 27, 2026
94ca10c
fix: accept contained temp remapping
qisoft Aug 27, 2026
05c8f52
fix: classify packaged helper failures
qisoft Aug 27, 2026
d0889ad
fix: mark Windows helpers AppContainer compatible
qisoft Aug 27, 2026
991bf87
fix: harden AppContainer PE marking
qisoft Aug 27, 2026
402c7f4
fix: keep marked helpers inside containment
qisoft Aug 27, 2026
cc40b3a
ci: name Windows runtime smoke accurately
qisoft Aug 27, 2026
5ab432d
fix: resolve hosted Python from MSYS2
qisoft Aug 27, 2026
4d734ed
fix: grant AppContainer runtime execution
qisoft Aug 27, 2026
81a1119
fix: seal AppContainer runtime permissions
qisoft Aug 27, 2026
9fd0e83
fix: scope runtime mutation proof to Windows
qisoft Aug 27, 2026
248371c
fix: allow contained helper processes
qisoft Aug 27, 2026
ae5fe87
fix: clean up Windows launch attributes
qisoft Aug 27, 2026
cffcc21
test: classify contained helper denial
qisoft Aug 27, 2026
1a022d5
test: identify Windows child restriction
qisoft Aug 27, 2026
86f812e
fix: match Windows mitigation ABI
qisoft Aug 27, 2026
db0b8d1
fix: stage Windows runtime outside profile data
qisoft Aug 27, 2026
84efa0b
fix: guard Windows runtime cleanup
qisoft Aug 27, 2026
858e846
fix: validate Windows runtime ancestors
qisoft Aug 27, 2026
c077e87
test: classify Windows setup failures
qisoft Aug 27, 2026
226750b
fix: resolve LocalAppData without environment
qisoft Aug 27, 2026
55edb92
fix: report canonical Windows known folder
qisoft Aug 27, 2026
501a7b4
fix: grant runtime access to every image
qisoft Aug 27, 2026
74b4c8d
fix: allow contained Windows helper processes
qisoft Aug 27, 2026
07e57bb
test: avoid Windows device access in child probe
qisoft Aug 27, 2026
b4f3959
test: isolate Windows process probes from NUL
qisoft Aug 27, 2026
46a1275
test: require measured shell denial
qisoft Aug 27, 2026
71cebc9
fix: feed native tools EOF through a pipe
qisoft Aug 27, 2026
ce516ec
fix: reap tools when stdin setup fails
qisoft Aug 27, 2026
a974b35
test: classify packaged session failures
qisoft Aug 27, 2026
6d6c057
test: classify packaged session stages
qisoft Aug 27, 2026
be8cb0f
test: preserve packaged session cleanup failures
qisoft Aug 27, 2026
f122359
test: isolate packaged proof unit cases
qisoft Aug 27, 2026
6108eb3
test: classify contained sidecar exits
qisoft Aug 27, 2026
3aa7272
fix: bound contained stderr capture
qisoft Aug 27, 2026
b7fa458
fix: accept intentional crash probe exit
qisoft Aug 27, 2026
9c52ef7
fix: require the crash probe exit code
qisoft Aug 27, 2026
f9e55e6
test: classify contained permission failures
qisoft Aug 27, 2026
eb858c0
fix: tighten sidecar failure allowlist
qisoft Aug 27, 2026
dbe40f2
fix: preserve dual-principal runtime access
qisoft Aug 27, 2026
cab3810
fix: install an exact runtime dacl
qisoft Aug 27, 2026
0ee8084
test: classify runtime access failures
qisoft Aug 27, 2026
7ab824a
fix: classify runtime root denials
qisoft Aug 27, 2026
fe322d1
test: classify runtime verification denials
qisoft Aug 27, 2026
1c78036
fix: classify denied runtime links
qisoft Aug 27, 2026
44df7a2
fix: grant scoped runtime traversal
qisoft Aug 27, 2026
bb03491
fix: traverse the runtime staging path
qisoft Aug 28, 2026
36c95c6
fix: share Windows runtime path handles
qisoft Aug 28, 2026
302ad86
fix: traverse the user runtime prefix
qisoft Aug 28, 2026
6dde110
test: preserve adversarial sidecar failures
qisoft Aug 28, 2026
58cd7a4
test: isolate adversarial proof failures
qisoft Aug 28, 2026
46e2cf4
test: surface native containment launch failures
qisoft Aug 28, 2026
ddd748e
fix: avoid reopening protected Windows ancestors
qisoft Aug 28, 2026
5a399f0
test: compare Windows runtime file identity
qisoft Aug 28, 2026
91573af
test: budget the full packaged containment proof
qisoft Aug 29, 2026
2b4bfda
fix: prepare Windows runtime ACL before copy
qisoft Aug 29, 2026
b5cf2e7
fix: resolve contained runtime entries relatively
qisoft Aug 29, 2026
5c73af1
fix: restore the contained analysis workspace
qisoft Aug 29, 2026
39d6bbc
fix: move Windows entry proof before containment
qisoft Aug 29, 2026
d6fd450
fix: reuse verified Windows runtime paths
qisoft Aug 29, 2026
22d153f
test: classify canonical preparation failures
qisoft Aug 29, 2026
8be8520
fix: reuse the native Windows workspace root
qisoft Aug 31, 2026
31b4d9c
fix: validate native workspace entries lexically
qisoft Aug 31, 2026
9ea3383
fix: anchor contained inputs to the native workspace
qisoft Aug 31, 2026
37f5f34
refactor: isolate packaged workspace preparation
qisoft Aug 31, 2026
72c1045
refactor: share packaged proof failure aggregation
qisoft Aug 31, 2026
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
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,31 @@ permissions:
contents: read

jobs:
linux-preview-containment:
name: Ubuntu 24.04 containment preview
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.13.11
- name: Build Landlock, seccomp, and cgroup launcher
run: python tools/build-native-containment.py --output-root dist/containment
- name: Verify the launcher fails closed outside its delegated scope
shell: bash
run: |
set +e
output="$(dist/containment/open-chords-containment-launcher \
--expected-unit=not-the-current.scope \
--workspace="$RUNNER_TEMP" \
--runtime-root="$GITHUB_WORKSPACE" \
-- /bin/true 2>&1)"
status=$?
set -e
test "$status" -eq 70
test "$output" = 'OC_CONTAINMENT_V1 {"error":"cgroup_scope_unverified"}'

native:
name: ${{ matrix.name }}
strategy:
Expand Down Expand Up @@ -104,8 +129,11 @@ jobs:
--native-root build/native/${{ matrix.profile }}
--output-root dist/analysis-sidecar
--platform-profile ${{ matrix.profile }}
- name: Run frozen sidecar decode smoke
- name: Validate frozen Windows runtime outside sandbox
if: runner.os == 'Windows'
run: pnpm test:frozen-sidecar
- name: Build native containment broker
run: python tools/build-native-containment.py --output-root dist/containment
- name: Build installable Forge artifact
run: pnpm make -- --arch=${{ matrix.arch }}
- name: Verify installed trust boundary
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/containment-build-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare const OPEN_CHORDS_EMBEDDED_CONTAINMENT_MANIFEST_SHA256: string;

export const EXPECTED_CONTAINMENT_MANIFEST_SHA256 =
OPEN_CHORDS_EMBEDDED_CONTAINMENT_MANIFEST_SHA256;
7 changes: 5 additions & 2 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { installDesktopIpc, publishProjectEvent } from "./desktop-ipc.ts";
import { LocalMediaService } from "./local-media.ts";
import { createMediaCleanupBeforeQuitHandler } from "./media-shutdown.ts";
import { PACKAGED_SIDECAR_PROOF_ARGUMENT } from "./packaged-sidecar-proof-constants.ts";
import { runPackagedSidecarProof } from "./packaged-sidecar-proof.ts";
import { packagedProofFailureCode, runPackagedSidecarProof } from "./packaged-sidecar-proof.ts";
import { openProjectLibrary } from "./project-library.ts";
import { installRendererProtocol, registerRendererScheme } from "./renderer-protocol.ts";
import {
Expand All @@ -28,7 +28,10 @@ if (process.argv.includes(PACKAGED_SIDECAR_PROOF_ARGUMENT)) {
.then(runPackagedSidecarProof)
.then(
() => app.exit(0),
() => app.exit(1),
(cause: unknown) => {
process.stderr.write(`Packaged sidecar proof failed: ${packagedProofFailureCode(cause)}\n`);
app.exit(1);
},
);
} else {
registerRendererScheme();
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/main/packaged-sidecar-proof-failures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function throwCombinedFailures(
message: string,
primaryFailure: { cause: unknown } | undefined,
cleanupFailures: readonly unknown[],
): void {
const failures = [
...(primaryFailure === undefined ? [] : [primaryFailure.cause]),
...cleanupFailures,
];
if (failures.length === 1) throw failures[0];
if (failures.length > 1) throw new AggregateError(failures, message);
}
185 changes: 185 additions & 0 deletions apps/desktop/src/main/packaged-sidecar-proof-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { cpSync, mkdirSync, realpathSync, rmSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

import { throwCombinedFailures } from "./packaged-sidecar-proof-failures.ts";
import {
isExpectedWindowsProfileRoot,
isExpectedWindowsRuntimeRoot,
} from "./windows-app-container-path.ts";

const WORKSPACE_FAILURE_CODES = [
"cleanup_failed",
"setup_prepare_failed",
"setup_response_failed",
"setup_validation_failed",
"setup_workspace_failed",
] as const;

type WorkspaceFailureCode = (typeof WORKSPACE_FAILURE_CODES)[number];

class PackagedWorkspaceFailure extends Error {
readonly code: WorkspaceFailureCode;

constructor(code: WorkspaceFailureCode) {
super(code);
this.name = "PackagedWorkspaceFailure";
this.code = code;
}
}

export interface PreparedPackagedWorkspace {
cleanup(): void;
runtimeRoot: string;
windowsProfile?: string;
workspace: string;
}

export function packagedWorkspaceFailureCode(cause: unknown): WorkspaceFailureCode | undefined {
return cause instanceof PackagedWorkspaceFailure ? cause.code : undefined;
}

export function preparePackagedWorkspace(
platform: "darwin" | "win32",
helperPath: string,
packagedRuntimeRoot: string,
): PreparedPackagedWorkspace {
const identifier = randomUUID();
if (platform === "darwin") {
const workspace = join(
homedir(),
"Library",
"Containers",
"io.github.qisoft.open-chords.analysis-service",
"Data",
"jobs",
identifier,
);
mkdirSync(workspace, { recursive: true, mode: 0o700 });
return {
cleanup: () => rmSync(workspace, { force: true, recursive: true }),
runtimeRoot: packagedRuntimeRoot,
workspace,
};
}
const profile = `OpenChords.Analysis.${identifier}`;
let reportedRoots: string[];
try {
reportedRoots = execFileSync(helperPath, [`--prepare=${profile}`], {
encoding: "utf8",
env: {},
windowsHide: true,
})
.trim()
.split(/\r?\n/);
} catch {
throwCombinedFailures(
"AppContainer profile preparation and cleanup failed",
{ cause: new PackagedWorkspaceFailure("setup_prepare_failed") },
privateCleanupFailures(destroyWindowsProfile(helperPath, profile)),
);
throw new PackagedWorkspaceFailure("setup_prepare_failed");
}
if (reportedRoots.length !== 3) {
throwCombinedFailures(
"AppContainer profile response and cleanup failed",
{ cause: new PackagedWorkspaceFailure("setup_response_failed") },
privateCleanupFailures(destroyWindowsProfile(helperPath, profile)),
);
throw new PackagedWorkspaceFailure("setup_response_failed");
}
const reportedProfileRoot = reportedRoots[0]!;
const reportedLocalAppDataRoot = reportedRoots[1]!;
const reportedRuntimeRoot = reportedRoots[2]!;
const localAppDataRoot = canonicalWindowsLocalAppDataRoot(reportedLocalAppDataRoot);
const profileRoot = canonicalWindowsProfileRoot(reportedProfileRoot, localAppDataRoot);
const runtimeRoot = canonicalWindowsRuntimeRoot(reportedRuntimeRoot, localAppDataRoot, profile);
if (profileRoot === null || runtimeRoot === null) {
throwCombinedFailures(
"AppContainer profile validation and cleanup failed",
{ cause: new PackagedWorkspaceFailure("setup_validation_failed") },
privateCleanupFailures(destroyWindowsProfile(helperPath, profile)),
);
throw new PackagedWorkspaceFailure("setup_validation_failed");
}
const workspace = join(profileRoot, "jobs", identifier);
try {
cpSync(packagedRuntimeRoot, runtimeRoot, { recursive: true });
mkdirSync(workspace, { recursive: true });
} catch {
throwCombinedFailures(
"AppContainer workspace setup and cleanup failed",
{ cause: new PackagedWorkspaceFailure("setup_workspace_failed") },
privateCleanupFailures(destroyWindowsProfile(helperPath, profile)),
);
}
return {
cleanup() {
throwCombinedFailures(
"AppContainer profile cleanup failed",
undefined,
destroyWindowsProfile(helperPath, profile),
);
},
runtimeRoot,
windowsProfile: profile,
workspace,
};
}

function canonicalWindowsLocalAppDataRoot(reportedRoot: string): string | null {
try {
return realpathSync(reportedRoot);
} catch {
return null;
}
}

function canonicalWindowsProfileRoot(
reportedRoot: string,
localAppDataRoot: string | null,
): string | null {
if (localAppDataRoot === null) return null;
try {
const packagesRoot = realpathSync(join(localAppDataRoot, "Packages"));
const profileRoot = realpathSync(reportedRoot);
return isExpectedWindowsProfileRoot(profileRoot, packagesRoot) ? profileRoot : null;
} catch {
return null;
}
}

function canonicalWindowsRuntimeRoot(
reportedRoot: string,
localAppDataRoot: string | null,
profile: string,
): string | null {
if (localAppDataRoot === null) return null;
try {
const runtimeRoot = realpathSync(reportedRoot);
return isExpectedWindowsRuntimeRoot(runtimeRoot, localAppDataRoot, profile)
? runtimeRoot
: null;
} catch {
return null;
}
}

function privateCleanupFailures(failures: readonly unknown[]): PackagedWorkspaceFailure[] {
return failures.map(() => new PackagedWorkspaceFailure("cleanup_failed"));
}

function destroyWindowsProfile(helperPath: string, profile: string): unknown[] {
const failures: unknown[] = [];
try {
execFileSync(helperPath, [`--destroy=${profile}`], {
env: {},
windowsHide: true,
});
} catch (cause) {
failures.push(cause);
}
return failures;
}
Loading
Loading