diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx
index a0cf84d2..6491d660 100644
--- a/desktop/src/__tests__/generalSettings.test.tsx
+++ b/desktop/src/__tests__/generalSettings.test.tsx
@@ -1547,6 +1547,20 @@ describe('Settings > Providers tab', () => {
])
})
+ it('falls back to the default provider order when stored order is missing', () => {
+ providerStoreState.providerOrder = undefined as unknown as string[]
+
+ render()
+
+ const rows = screen.getAllByRole('button', { name: 'Drag to reorder' })
+ .map((handle) => handle.closest('[data-testid]')?.getAttribute('data-testid'))
+ expect(rows).toEqual([
+ 'provider-provider-1',
+ 'claude-official-provider',
+ 'openai-official-provider',
+ ])
+ })
+
it('requires confirmation before deleting a provider', async () => {
render()
diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx
index 83bf956c..3b757d49 100644
--- a/desktop/src/pages/Settings.tsx
+++ b/desktop/src/pages/Settings.tsx
@@ -270,12 +270,16 @@ function defaultProviderOrder(providers: SavedProvider[]): string[] {
]
}
-function normalizeProviderOrder(providerOrder: string[], providers: SavedProvider[]): string[] {
+function normalizeProviderOrder(providerOrder: string[] | undefined, providers: SavedProvider[]): string[] {
const knownIds = new Set(defaultProviderOrder(providers))
const seen = new Set()
const order: string[] = []
- for (const id of providerOrder.length > 0 ? providerOrder : defaultProviderOrder(providers)) {
+ const source = providerOrder && providerOrder.length > 0
+ ? providerOrder
+ : defaultProviderOrder(providers)
+
+ for (const id of source) {
if (!knownIds.has(id) || seen.has(id)) continue
seen.add(id)
order.push(id)
@@ -292,7 +296,7 @@ function normalizeProviderOrder(providerOrder: string[], providers: SavedProvide
function buildProviderListItems(
providers: SavedProvider[],
- providerOrder: string[],
+ providerOrder: string[] | undefined,
): ProviderListItem[] {
const savedItems = new Map(
providers.map((provider) => [
diff --git a/scripts/pr/quality-contract.test.ts b/scripts/pr/quality-contract.test.ts
index 128c37d9..d249a20f 100644
--- a/scripts/pr/quality-contract.test.ts
+++ b/scripts/pr/quality-contract.test.ts
@@ -11,12 +11,12 @@ describe('feature quality contract', () => {
expect(agents).toContain('`~/.claude/settings.json` is user-owned shared state')
expect(agents).toContain('persistence upgrade gate')
expect(agents).toContain('Production code changes under `desktop/src`, `src/server`, `src/tools`, `src/utils`, or `adapters` must include a same-area test file')
- expect(agents).toContain('Coverage is part of the feature, not an afterthought.')
+ expect(agents).toContain('Coverage is part of PR readiness, not an afterthought.')
expect(agents).toContain('changed executable production line must meet the changed-line coverage gate')
expect(agents).toContain('E2E is required when the feature crosses process boundaries')
expect(agents).toContain('AI agents must include this evidence')
expect(agents).toContain('Unified local entrypoint: `bun run verify`')
- expect(agents).toContain('If `bun run verify` fails, do not stop at reporting the failure')
+ expect(agents).toContain('If `bun run verify` is intentionally run and fails, do not stop at reporting the failure')
})
test('keeps PR authors accountable for tests, coverage, E2E, and risk', () => {
diff --git a/src/server/__tests__/workspace-service.test.ts b/src/server/__tests__/workspace-service.test.ts
index 8ac31c59..0b0dc9a6 100644
--- a/src/server/__tests__/workspace-service.test.ts
+++ b/src/server/__tests__/workspace-service.test.ts
@@ -115,6 +115,19 @@ describe('WorkspaceService outside-workspace preview', () => {
await expect(service.readFile('session-1', unrelatedFile)).rejects.toThrow(/outside workspace/)
})
+
+ it('does not allow relative traversal through a registered outside dir', async () => {
+ const baseDir = await makeTempDir('workspace-service-base-')
+ const workDir = path.join(baseDir, 'work')
+ const outsideFile = path.join(baseDir, 'outside.txt')
+ await fs.mkdir(workDir)
+ await fs.writeFile(outsideFile, 'secret\n')
+ registerFilesystemAccessRoot(baseDir)
+
+ const service = new WorkspaceService(async (sessionId) => sessionId === 'session-1' ? workDir : null)
+
+ await expect(service.readFile('session-1', '../outside.txt')).rejects.toThrow(/outside workspace/)
+ })
})
describe('WorkspaceService', () => {
diff --git a/src/server/services/workspaceService.ts b/src/server/services/workspaceService.ts
index 8e339e37..eda596e0 100644
--- a/src/server/services/workspaceService.ts
+++ b/src/server/services/workspaceService.ts
@@ -1065,7 +1065,7 @@ export class WorkspaceService {
// registered as access roots when the turn checkpoint is built. Preview
// those by absolute path — they have no workspace-relative form — instead
// of rejecting them as out-of-sandbox.
- if (isWithinRegisteredFilesystemRoot(absolutePath)) {
+ if (this.isAbsoluteRequestPath(requestedPath) && isWithinRegisteredFilesystemRoot(absolutePath)) {
return this.resolveOutsideWorkspacePath(absolutePath, requestedPath)
}
throw new Error(`Path is outside workspace: ${requestedPath}`)
@@ -1192,6 +1192,11 @@ export class WorkspaceService {
return isSameOrInsidePathForPlatform(targetPath, rootPath)
}
+ private isAbsoluteRequestPath(requestedPath: string): boolean {
+ const pathApi = process.platform === 'win32' ? path.win32 : path
+ return pathApi.isAbsolute(normalizeDriveRootPathForPlatform(requestedPath))
+ }
+
private normalizeRelativePath(filePath: string): string {
if (!filePath || filePath === '.') return ''
return filePath.split(path.sep).join('/')