Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
18772ed
refactor(cli): keep acp-integration off serve internals (#8084)
yiliang114 Aug 14, 2026
99a1172
Merge remote-tracking branch 'origin/main' into HEAD
yiliang114 Aug 14, 2026
0691cb9
fix(cli): harden the acp/serve boundary guard (round 2)
yiliang114 Aug 14, 2026
ac83137
test(cli): pin serve boundary lint rules
yiliang114 Aug 15, 2026
ec63477
test(cli): close serve boundary lint gaps
yiliang114 Aug 15, 2026
78ead13
fix(lint): close serve-boundary entrances and harden the guard
yiliang114 Aug 15, 2026
17d16d5
fix(lint): correct TSImportType selector path and computed-template h…
yiliang114 Aug 15, 2026
4abaca1
fix(lint): close the remaining round-6 serve-boundary entrances
yiliang114 Aug 15, 2026
6f7a13d
fix(lint): cover vitest serve-boundary calls
yiliang114 Aug 15, 2026
0c8c334
fix(lint): close the round-7 serve-boundary entrance classes
yiliang114 Aug 15, 2026
1815694
refactor(lint): resolve the serve boundary by resolution, not text (#…
yiliang114 Aug 16, 2026
23cf025
Merge branch 'main' into fix/acp-serve-boundary-guard
yiliang114 Aug 16, 2026
02e502a
fix(lint): drop the dead serveGuardSyntaxRules helper
yiliang114 Aug 16, 2026
3d8be1f
fix(lint): close serve boundary resolver gaps
yiliang114 Aug 16, 2026
3cc4298
fix(lint): address serve boundary review suggestions
yiliang114 Aug 16, 2026
cf05dd5
fix(lint): close round-11 serve boundary gaps
yiliang114 Aug 16, 2026
34fc045
chore: merge main into serve boundary guard
yiliang114 Aug 16, 2026
2d59792
fix(cli): restore live session source import
yiliang114 Aug 16, 2026
3da80b9
fix(lint): clear the two lint errors breaking CI on the boundary rule
yiliang114 Aug 16, 2026
c4758e3
fix(lint): close bounded serve boundary gaps
yiliang114 Aug 16, 2026
7112ab4
fix(lint): complete the round-12 boundary escape closures
yiliang114 Aug 16, 2026
552bc7c
fix(lint): close the round-12 reviewer escape classes (#8084)
yiliang114 Aug 17, 2026
c11b30c
fix(lint): repair corrupted files from the git-API blob upload
yiliang114 Aug 17, 2026
8e583a8
Merge remote-tracking branch 'origin/main' into HEAD
yiliang114 Aug 17, 2026
ebe1cb5
fix(lint): close the round-13 serve-boundary escape classes (#8084)
yiliang114 Aug 17, 2026
9fbd906
fix(lint): close the round-13 binding-hop and callee-opacity escapes …
yiliang114 Aug 18, 2026
58d4398
refactor(cli): simplify ACP serve boundary guard
yiliang114 Aug 19, 2026
ff4e3b0
Merge remote-tracking branch 'origin/main' into codex/pr-9144-scope-back
yiliang114 Aug 20, 2026
210b2e1
fix(lint): close the round-21 contract pins and bare-barrel escape (#…
yiliang114 Aug 20, 2026
01475ab
fix(lint): close dynamic-import and js-file holes in the acp/serve gu…
yiliang114 Aug 22, 2026
5e7511a
fix(lint): make acp/serve dynamic-import guard case-insensitive
yiliang114 Aug 22, 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
68 changes: 56 additions & 12 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ import storybook from 'eslint-plugin-storybook';
import checkFile from 'eslint-plugin-check-file';
import { legacyFilenames } from './eslint.legacy-filenames.mjs';

// General syntax restrictions applied to every TS/TSX source file. Hoisted so
// surface-specific overrides (flat config keeps only the last
// no-restricted-syntax setting per file) can repeat them without drift.
const generalRestrictedSyntaxSelectors = [
{
selector: 'CallExpression[callee.name="require"]',
message: 'Avoid using require(). Use ES6 imports instead.',
},
{
selector: 'ThrowStatement > Literal:not([value=/^\\w+Error:/])',
message:
'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
},
];

export default tseslint.config(
{
// Global ignores
Expand Down Expand Up @@ -72,6 +87,26 @@ export default tseslint.config(
'import/namespace': 'off', // Disabled due to https://github.com/import-js/eslint-plugin-import/issues/2866
},
},
{
// ACP integration and the daemon are separate runtime surfaces that happen
// to share a package directory. ACP may consume neutral contracts under
// `runtime/`, but never `serve/` implementation modules — see #8084.
files: ['packages/cli/src/acp-integration/**/*.{ts,tsx,js}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/serve', '**/serve/**'],
message:
'acp-integration must not import serve/ internals. Put shared, lifecycle-free logic in packages/cli/src/runtime/ instead (#8084).',
},
],
},
],
},
},
{
// `utils/` is the layer every other directory imports, so it must not
// import back into one. The daemon direction is clean and enforced here;
Expand Down Expand Up @@ -168,18 +203,7 @@ export default tseslint.config(
'no-cond-assign': 'error',
'no-debugger': 'error',
'no-duplicate-case': 'error',
'no-restricted-syntax': [
'error',
{
selector: 'CallExpression[callee.name="require"]',
message: 'Avoid using require(). Use ES6 imports instead.',
},
{
selector: 'ThrowStatement > Literal:not([value=/^\\w+Error:/])',
message:
'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
},
],
'no-restricted-syntax': ['error', ...generalRestrictedSyntaxSelectors],
'no-unsafe-finally': 'error',
'no-console': 'error',
'no-unused-expressions': 'off', // Disable base rule
Expand All @@ -197,6 +221,26 @@ export default tseslint.config(
'default-case': 'error',
},
},
{
// no-restricted-imports only sees static import/export declarations, so a
// dynamic `await import('../serve/...')` would slip past the #8084 guard
// above. Kept after the general TS block because flat config applies only
// the last no-restricted-syntax setting per file, hence the repeated
// general selectors.
files: ['packages/cli/src/acp-integration/**/*.{ts,tsx,js}'],
rules: {
'no-restricted-syntax': [
'error',
...generalRestrictedSyntaxSelectors,
{
// \x2f is '/' — esquery selector regexes cannot contain a literal '/'.
selector: "ImportExpression[source.value=/(^|\\x2f)serve(\\x2f|$)/i]",
message:
'acp-integration must not dynamically import serve/ internals. Put shared, lifecycle-free logic in packages/cli/src/runtime/ instead (#8084).',
},
],
},
},
{
files: [
'packages/web-shell/client/**/*.{ts,tsx}',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ import {
type PermissionRuleSet,
} from '../config/permission-settings.js';
import { createLoadedSettingsAdapter } from '../config/loadedSettingsAdapter.js';
import { isCompatibleLiveSessionSource } from '../serve/conversations/session-source.js';
import { isCompatibleLiveSessionSource } from '../runtime/live-session-source.js';
import type { ApprovalModeValue } from './session/types.js';
import { z } from 'zod';
import type { CliArgs } from '../config/config.js';
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ import * as nonInteractiveCliCommands from '../../nonInteractiveCliCommands.js';
import { CommandKind } from '../../ui/commands/types.js';
import { buildAcpModelOptions } from '../../utils/acpModelUtils.js';
import { CHANNEL_PROMPT_META_KEY } from '@qwen-code/channel-base';
import { CAPTURE_SCREEN_CONTEXT_TOOL_NAME } from '../../serve/live/capture-screen-context.js';
import { SPEAK_TO_USER_TOOL_NAME } from '../../serve/live/live-speak-to-user.js';
import { CAPTURE_SCREEN_CONTEXT_TOOL_NAME } from '../live/capture-screen-context.js';
import { SPEAK_TO_USER_TOOL_NAME } from '../live/live-speak-to-user.js';
import {
collectHistoryReplayUpdates,
createReplayCumulativeUsage,
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/acp-integration/session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,19 +223,19 @@ import { normalizeChannelDeliveryText } from '../../runtime/channel-delivery.js'
import {
CAPTURE_SCREEN_CONTEXT_TOOL_NAME,
CaptureScreenContextTool,
} from '../../serve/live/capture-screen-context.js';
} from '../live/capture-screen-context.js';
import {
createLiveTaskTools,
type LiveTaskTool,
} from '../../serve/live/live-task-tools.js';
} from '../live/live-task-tools.js';
Comment thread
yiliang114 marked this conversation as resolved.
import {
SPEAK_TO_USER_TOOL_NAME,
SpeakToUserTool,
} from '../../serve/live/live-speak-to-user.js';
} from '../live/live-speak-to-user.js';
import {
LIVE_BACKEND_END_INSTRUCTIONS,
LIVE_BACKEND_START_INSTRUCTIONS,
} from '../../serve/live/live-backend-instructions.js';
} from '../live/live-backend-instructions.js';
import { readVoiceModel } from '../../services/voice-settings.js';
import {
MAX_AUDIO_BYTES,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
readLoadableLiveConversationMetadata,
type ConversationSessionMetadataStore,
type LiveSessionCreationMetadata,
} from './session-source.js';
} from './live-session-source.js';

const LIVE_ID = '550e8400-e29b-41d4-a716-446655440000';
const LIVE_CHILD_ID = '550e8400-e29b-41d4-a716-446655440001';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import {
isValidSessionId,
normalizeSessionIdForLookup,
} from '../../config/session-id.js';
} from '../config/session-id.js';

export const LIVE_SESSION_SOURCE_PREFIX = 'realtime_voice:';
export const STANDALONE_SESSION_SOURCE_TYPE = 'standalone';
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/serve/acp-http/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import {
isReservedLiveSessionSource,
isReservedStandaloneSessionSource,
readLoadableLiveConversationMetadata,
} from '../conversations/session-source.js';
} from '../../runtime/live-session-source.js';
import {
translateAndCheckAbsoluteWorkspacePath,
canonicalizeWorkspace,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/serve/live/live-session-coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ import type { LiveProviderCredential } from './provider-credentials.js';
import {
isCompatibleLiveSessionSource,
LIVE_SESSION_SOURCE_PREFIX,
} from '../conversations/session-source.js';
} from '../../runtime/live-session-source.js';
import type { LiveProviderReadiness, LiveSessionLocator } from './types.js';

export { LIVE_SESSION_SOURCE_PREFIX } from '../conversations/session-source.js';
export { LIVE_SESSION_SOURCE_PREFIX } from '../../runtime/live-session-source.js';

const MAX_COORDINATOR_REQUEST_CHARS = 32_000;
const MAX_COORDINATOR_RESULT_CHARS = 48_000;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/serve/live/live-task-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
WorkspaceRuntime,
} from '../workspace-registry.js';
import { isLiveTaskToolName, LiveTaskService } from './live-task-service.js';
import { LIVE_SESSION_SOURCE_PREFIX } from '../conversations/session-source.js';
import { LIVE_SESSION_SOURCE_PREFIX } from '../../runtime/live-session-source.js';

const persistedSessions = vi.hoisted(() => new Map<string, unknown>());
const persistedSessionOwners = vi.hoisted(() => new Map<string, string>());
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/serve/live/live-task-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { laterActivityTimestamp } from '../server/activity-timestamp.js';
import {
isCompatibleLiveSessionSource,
readLoadableLiveConversationMetadata,
} from '../conversations/session-source.js';
} from '../../runtime/live-session-source.js';
import { conversationRuntimeUnavailableError } from '../conversations/conversation-runtime-errors.js';

const DEFAULT_LIST_LIMIT = 20;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/serve/multi-workspace-sessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
} from './workspace-registry.js';
import type { WorkspaceRuntimeProvenance } from './managed-scratch-workspace.js';
import type { ConversationWorkspace } from './conversations/conversation-workspace.js';
import { LIVE_SESSION_SOURCE_PREFIX } from './conversations/session-source.js';
import { LIVE_SESSION_SOURCE_PREFIX } from '../runtime/live-session-source.js';
import { createSessionOrganizationService } from './session-organization-helpers.js';
import {
serializeWorkspaceTranscriptResponseForTesting,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/serve/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
isReservedLiveSessionSource,
isReservedStandaloneSessionSource,
readLoadableLiveConversationMetadata,
} from '../conversations/session-source.js';
} from '../../runtime/live-session-source.js';
import type { ConversationRuntimeActivityGate } from '../conversations/conversation-runtime-activity.js';
import { ConversationRuntimeOwnershipError } from '../conversations/conversation-runtime-errors.js';
import express, {
Expand Down
103 changes: 103 additions & 0 deletions scripts/tests/acp-serve-boundary-guard.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { ESLint } from 'eslint';
import { expect, it } from 'vitest';

const root = join(dirname(fileURLToPath(import.meta.url)), '../..');

const eslint = new ESLint({ cwd: root });

// Static imports are reported by no-restricted-imports; dynamic import() is
// reported by no-restricted-syntax because the former never sees it.
const BOUNDARY_RULES = new Set([
'no-restricted-imports',
'no-restricted-syntax',
]);

async function restrictedReports(statement) {
const filePath = join(
root,
'packages/cli/src/acp-integration/boundary-probe.ts',
);
const [result] = await eslint.lintText(`${statement}\n`, { filePath });
return result.messages.filter((m) => BOUNDARY_RULES.has(m.ruleId));
}

// Bare-directory specifiers resolve to packages/cli/src/serve/index.ts, a
// barrel re-exporting the full daemon surface — they must be caught by the
// same guard that blocks deep serve/ internals (#8084).
it.each(['../serve', '../../serve'])(
Comment thread
yiliang114 marked this conversation as resolved.
'blocks the bare barrel specifier %s from acp-integration',
async (specifier) => {
const reports = await restrictedReports(
`import { createServeApp } from '${specifier}';`,
);
expect(reports).toHaveLength(1);
expect(reports[0].message).toContain('acp-integration');
},
);

it('blocks a bare barrel re-export from acp-integration', async () => {
const reports = await restrictedReports(
`export { createServeApp } from '../serve';`,
);
expect(reports).toHaveLength(1);
});

it('still blocks deep serve/ internals from acp-integration', async () => {
const reports = await restrictedReports(
`import { createServeApp } from '../serve/index.js';`,
);
expect(reports).toHaveLength(1);
});

it('blocks type-only imports and re-exports from serve/ from acp-integration', async () => {
expect(
await restrictedReports(`import type { ServeAppDeps } from '../serve';`),
).toHaveLength(1);
expect(
await restrictedReports(`export type { ServeAppDeps } from '../serve';`),
).toHaveLength(1);
});

it('blocks a dynamic import() of serve/ from acp-integration', async () => {
const reports = await restrictedReports(
`async function probe() { await import('../serve/index.js'); }`,
);
expect(reports).toHaveLength(1);
expect(reports[0].message).toContain('acp-integration');
});

it('blocks a case-variant dynamic import() of Serve/ from acp-integration', async () => {
const reports = await restrictedReports(
`async function probe() { await import('../Serve/index.js'); }`,
);
expect(reports).toHaveLength(1);
});

it('allows neutral runtime/ contracts from acp-integration', async () => {
expect(
await restrictedReports(
`import { something } from '../runtime/contracts.js';`,
),
).toHaveLength(0);
expect(
await restrictedReports(
`async function probe() { await import('../runtime/contracts.js'); }`,
),
).toHaveLength(0);
// A computed specifier (not a string literal) has no source.value, so the
// dynamic guard must not reject it — the import target is unknowable at
// lint time.
expect(
await restrictedReports(
`async function probe() { const target = '../runtime/contracts.js'; await import(target); }`,
),
).toHaveLength(0);
});
4 changes: 2 additions & 2 deletions scripts/tests/cross-package-contracts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const imports = [
],
[
'LIVE_TASK_TOOL_NAMES',
'packages/cli/src/serve/live/live-task-tools.ts',
'packages/cli/src/acp-integration/live/live-task-tools.ts',
'@qwen-code/acp-bridge/bridgeOptions',
],
[
Expand All @@ -65,7 +65,7 @@ const imports = [
],
[
'LiveTaskToolName',
'packages/cli/src/serve/live/live-task-tools.ts',
'packages/cli/src/acp-integration/live/live-task-tools.ts',
'@qwen-code/acp-bridge/bridgeOptions',
],
[
Expand Down
Loading