Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bc4939f
feat(mirror): project files can stay on the client while agents run o…
Nardy11 Aug 9, 2026
71efa99
fix(mirror): standard-paired clients can mint the peer credential
Nardy11 Aug 10, 2026
c3afa45
fix(mirror): allow mirroring plain folders that aren't git repos yet
Nardy11 Aug 10, 2026
ae4d170
feat(web): add a dev-only React Grab overlay behind a settings flag
Nardy11 Aug 10, 2026
3884e48
feat(mirror): sync git submodule / nested-repo content instead of lea…
Nardy11 Aug 10, 2026
cc4aae9
chore(mirror): sync .env files for this repo via mirror.include
Nardy11 Aug 10, 2026
1506954
Revert "chore(mirror): sync .env files for this repo via mirror.include"
Nardy11 Aug 10, 2026
17d1bf6
feat(mirror): add per-project toggle to sync gitignored env files
Nardy11 Aug 10, 2026
20fe070
fix(mirror): refresh extra include paths on every sync, not just connect
Nardy11 Aug 10, 2026
d3905ed
fix(mirror): force-add include paths one at a time
Nardy11 Aug 10, 2026
bc0b5ff
fix(mirror): match env files at any depth, not just the repo root
Nardy11 Aug 10, 2026
af80bc0
fix(mirror): stop reconnect-retrying links the host says aren't mirrored
Nardy11 Aug 10, 2026
0ad9e47
feat(mirror): link hygiene, transfer progress, and mirror visibility
Nardy11 Aug 10, 2026
ed52294
Merge remote-tracking branch 'origin/main' into feat/project-mirroring
malekelkssas Aug 11, 2026
dd82b4a
fix(mirror): address automated review findings across security, corre…
malekelkssas Aug 11, 2026
a6567ba
fix(provider): stop surfacing command_lifecycle SDK messages as raw w…
malekelkssas Aug 11, 2026
d6b2dfb
fix(mirror): address second-pass automated review findings
malekelkssas Aug 11, 2026
0fae8b7
fix(mirror): bound GitSyncCommandError.message instead of embedding r…
malekelkssas Aug 11, 2026
a1f8729
fix(test): use harness.runEffect instead of raw Effect.runPromise
malekelkssas Aug 17, 2026
c464d4d
Merge remote-tracking branch 'upstream/main' into feat/project-mirroring
malekelkssas Aug 17, 2026
5927a35
fix(test): update tests for schema/settings fields added by the main …
malekelkssas Aug 17, 2026
3519061
fix(test): account for ReactGrabController in AppRoot children
malekelkssas Aug 17, 2026
7648881
fix(mirror): address Macroscope review findings on PR #6157
malekelkssas Aug 19, 2026
579bfec
fix(mirror): address second round of Macroscope findings
malekelkssas Aug 19, 2026
0a8510a
fix(web): hide shared-folder rows for peers that don't advertise mirr…
malekelkssas Aug 19, 2026
447bfa8
fix(mirror): address third round of Macroscope findings on PR #6157
malekelkssas Aug 20, 2026
01e7916
fix(test): use it.effect instead of manual Effect.runPromise
malekelkssas Aug 20, 2026
9740a40
fix(mirror): handle directory type-conflicts and newline paths in Git…
malekelkssas Aug 20, 2026
17366ae
fix(mirror): fail closed when discovering branches checked out elsewhere
malekelkssas Aug 20, 2026
2efec0d
fix(mirror): thread GitSync as an Effect requirement in SubmoduleSync
malekelkssas Aug 20, 2026
bbec5bd
fix(mirror): preserve the real cause on MirrorSyncFailedError/MirrorN…
malekelkssas Aug 20, 2026
6b3d036
fix(mirror): serialize revokeLink with the per-project lock
malekelkssas Aug 20, 2026
0b89a9f
fix(mirror): sanitize host-supplied syncId before it becomes a stagin…
malekelkssas Aug 20, 2026
ebe9ceb
fix(web): color the error message, not the row, on the unavailable-li…
malekelkssas Aug 20, 2026
58807c0
Merge remote-tracking branch 'upstream/main' into mirror-pr-review-wip
malekelkssas Aug 20, 2026
cfcae74
Merge remote-tracking branch 'upstream/main' into mirror-upstream-sync
malekelkssas Aug 30, 2026
a654233
fix(server): correct stale migration test labels after renumbering
malekelkssas Aug 30, 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
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const clientSettings: ClientSettings = {
planModeEnabled: false,
showSkillsInSlashMenu: false,
providerModelPreferences: {},
reactGrabEnabled: false,
sidebarAutoSettleAfterDays: 3,
sidebarAutoSettleOnMerge: true,
sidebarProjectGroupingMode: "repository_path",
Expand Down
10 changes: 10 additions & 0 deletions apps/server/integration/OrchestrationEngineHarness.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
import { ProviderService } from "../src/provider/Services/ProviderService.ts";
import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts";
import * as MirrorServiceModule from "../src/mirror/MirrorService.ts";
import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityResolver.ts";
import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "../src/orchestration/Layers/ProjectionPipeline.ts";
Expand All @@ -64,6 +65,7 @@ import {
type OrchestrationEngineShape,
} from "../src/orchestration/Services/OrchestrationEngine.ts";
import { ThreadDeletionReactor } from "../src/orchestration/Services/ThreadDeletionReactor.ts";
import { MirrorProjectDeletionReactor } from "../src/orchestration/Services/MirrorProjectDeletionReactor.ts";
import { OrchestrationReactor } from "../src/orchestration/Services/OrchestrationReactor.ts";
import { ProjectionSnapshotQuery } from "../src/orchestration/Services/ProjectionSnapshotQuery.ts";
import {
Expand Down Expand Up @@ -333,12 +335,14 @@ export const makeOrchestrationIntegrationHarness = (
generateThreadTitle: () => Effect.succeed({ title: "New thread" }),
} as unknown as TextGenerationShape);
const providerCommandReactorLayer = ProviderCommandReactorLive.pipe(
Layer.provideMerge(MirrorServiceModule.layerTest),
Layer.provideMerge(runtimeServicesLayer),
Layer.provideMerge(gitWorkflowLayer),
Layer.provideMerge(textGenerationLayer),
Layer.provideMerge(serverSettingsLayer),
);
const checkpointReactorLayer = CheckpointReactorLive.pipe(
Layer.provideMerge(MirrorServiceModule.layerTest),
Layer.provideMerge(runtimeServicesLayer),
Layer.provideMerge(
Layer.succeed(VcsStatusBroadcaster, {
Expand Down Expand Up @@ -376,6 +380,12 @@ export const makeOrchestrationIntegrationHarness = (
drainThrough: () => Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(MirrorProjectDeletionReactor, {
start: () => Effect.void,
drain: Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(AgentAwarenessRelay.AgentAwarenessRelay, {
publishThread: () => Effect.void,
Expand Down
13 changes: 13 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AuthAccessReadScope,
AuthMirrorSyncScope,
AuthOrchestrationOperateScope,
AuthOrchestrationReadScope,
AuthRelayReadScope,
Expand Down Expand Up @@ -121,6 +122,18 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.previewAutomationConnect]: AuthOrchestrationOperateScope,
[WS_METHODS.previewAutomationRespond]: AuthOrchestrationOperateScope,
[WS_METHODS.previewAutomationFocusHost]: AuthOrchestrationOperateScope,
// Minting a peer credential is part of mirror setup, not access
// administration: the issued token is scoped to mirror:sync only, so a
// standard-paired client that can already run agents may create it.
[WS_METHODS.mirrorCreatePeerCredential]: AuthOrchestrationOperateScope,
[WS_METHODS.mirrorAttach]: AuthOrchestrationOperateScope,
[WS_METHODS.mirrorDetach]: AuthOrchestrationOperateScope,
// Listing this device's shared folders is read-only introspection.
[WS_METHODS.mirrorListLinks]: AuthOrchestrationReadScope,
[WS_METHODS.mirrorConnect]: AuthMirrorSyncScope,
[WS_METHODS.mirrorRespond]: AuthMirrorSyncScope,
[WS_METHODS.mirrorRequestSync]: AuthOrchestrationOperateScope,
[WS_METHODS.subscribeMirrorStatus]: AuthOrchestrationReadScope,
[WS_METHODS.subscribePreviewEvents]: AuthOrchestrationReadScope,
[WS_METHODS.subscribeDiscoveredLocalServers]: AuthOrchestrationReadScope,
[WS_METHODS.subscribeServerConfig]: AuthOrchestrationReadScope,
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface ServerDerivedPaths {
readonly environmentThemesDir: string;
readonly providerStatusCacheDir: string;
readonly worktreesDir: string;
readonly mirrorsDir: string;
readonly attachmentsDir: string;
readonly logsDir: string;
readonly serverLogPath: string;
Expand Down Expand Up @@ -124,6 +125,7 @@ export const deriveServerPaths = Effect.fn(function* (
environmentThemesDir: join(stateDir, "themes"),
providerStatusCacheDir,
worktreesDir: join(baseDir, "worktrees"),
mirrorsDir: join(baseDir, "mirrors"),
attachmentsDir,
logsDir,
serverLogPath: join(logsDir, "server.log"),
Expand All @@ -150,6 +152,7 @@ export const ensureServerDirectories = Effect.fn(function* (derivedPaths: Server
fs.makeDirectory(derivedPaths.terminalLogsDir, { recursive: true }),
fs.makeDirectory(derivedPaths.attachmentsDir, { recursive: true }),
fs.makeDirectory(derivedPaths.worktreesDir, { recursive: true }),
fs.makeDirectory(derivedPaths.mirrorsDir, { recursive: true }),
fs.makeDirectory(path.dirname(derivedPaths.keybindingsConfigPath), { recursive: true }),
fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true }),
fs.makeDirectory(derivedPaths.providerStatusCacheDir, { recursive: true }),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const make = Effect.gen(function* () {
threadPinning: true,
threadPinReorder: true,
threadTitleRegeneration: true,
projectMirroring: true,
threadPullRequestLinking: true,
...(serverSelfUpdate === null ? {} : { serverSelfUpdate }),
...(serverSelfUpdate === "boot-service" ? { serverSelfUpdateProgress: true } : {}),
Expand Down
83 changes: 83 additions & 0 deletions apps/server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { OtlpTracer } from "effect/unstable/observability";

import * as ServerConfig from "./config.ts";
import { ASSET_ROUTE_PREFIX, resolveAsset } from "./assets/AssetAccess.ts";
import { MIRROR_BUNDLE_ROUTE_PREFIX, MirrorBundleTransfer } from "./mirror/MirrorBundleTransfer.ts";
import {
ATTACHMENT_UPLOAD_ROUTE_PREFIX,
storeAttachmentUpload,
Expand Down Expand Up @@ -290,6 +291,88 @@ export const assetRouteLayer = HttpRouter.add(
}),
);

/**
* Git bundle transfer for project mirroring. The HMAC-signed single-use
* token in the path is the entire authorization (the asset-access pattern):
* origin agents PUT sync/seed bundles up and GET apply-back bundles down.
* Bundle bytes deliberately bypass the JSON RPC WebSocket.
*/
const resolveBundleTransfer = (direction: "upload" | "download") =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const url = HttpServerRequest.toURL(request);
if (Option.isNone(url)) return null;
const token = url.value.pathname.slice(`${MIRROR_BUNDLE_ROUTE_PREFIX}/`.length);
if (token.length === 0 || token.includes("/")) return null;
const transfer = yield* MirrorBundleTransfer;
return yield* transfer.resolve(token, direction);
});

export const mirrorBundleRouteLayer = Layer.mergeAll(
HttpRouter.add(
"GET",
`${MIRROR_BUNDLE_ROUTE_PREFIX}/*`,
Effect.gen(function* () {
const resolved = yield* resolveBundleTransfer("download");
if (resolved === null) {
return HttpServerResponse.text("Not Found", { status: 404 });
}
return yield* HttpServerResponse.file(resolved.bundlePath, {
status: 200,
headers: {
"Cache-Control": "no-store",
"X-Content-Type-Options": "nosniff",
},
}).pipe(
Effect.orElseSucceed(() =>
HttpServerResponse.text("Internal Server Error", { status: 500 }),
),
);
}),
),
HttpRouter.add(
"PUT",
`${MIRROR_BUNDLE_ROUTE_PREFIX}/*`,
Effect.gen(function* () {
const resolved = yield* resolveBundleTransfer("upload");
if (resolved === null) {
return HttpServerResponse.text("Not Found", { status: 404 });
}
const request = yield* HttpServerRequest.HttpServerRequest;
const fileSystem = yield* FileSystem.FileSystem;
const transfer = yield* MirrorBundleTransfer;
// Feed the live status stream: count bytes as they land so seeding
// and syncing can show transfer progress.
const contentLength = Number.parseInt(request.headers["content-length"] ?? "", 10);
const totalBytes = Number.isFinite(contentLength) && contentLength > 0 ? contentLength : null;
let receivedBytes = 0;
const counted = request.stream.pipe(
Stream.tap((chunk) => {
receivedBytes += chunk.byteLength;
return transfer.trackUpload({
projectId: resolved.projectId,
syncId: resolved.syncId,
bytes: receivedBytes,
totalBytes,
});
}),
);
const written = yield* Stream.run(counted, fileSystem.sink(resolved.bundlePath)).pipe(
Effect.result,
Effect.ensuring(transfer.clearUpload(resolved.syncId)),
);
if (written._tag === "Failure") {
yield* fileSystem.remove(resolved.bundlePath, { force: true }).pipe(Effect.ignore);
yield* Effect.logWarning("Mirror bundle upload failed mid-stream.", {
syncId: resolved.syncId,
});
return HttpServerResponse.text("Bad Request", { status: 400 });
}
return HttpServerResponse.empty({ status: 204 });
}),
),
);

export const attachmentUploadRouteLayer = HttpRouter.add(
"POST",
`${ATTACHMENT_UPLOAD_ROUTE_PREFIX}/*`,
Expand Down
Loading
Loading