@adecore/agents 0.25.4 → 0.25.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ export interface BackendLaunch {
14
14
  resumeNote: string | null;
15
15
  goal?: ChatGoal;
16
16
  folders?: readonly string[];
17
+ projectConfig?: boolean;
17
18
  spawn?: SpawnChatProcess;
18
19
  saveGeneratedImage?(ref: string, upload: ChatAttachmentUpload): Promise<ChatAttachment>;
19
20
  }
@@ -30,6 +30,7 @@ export interface ChatCoreHooks {
30
30
  instructionsFor?(chatId: string): string | null;
31
31
  resumeNoteFor?(chatId: string): string | null;
32
32
  foldersFor?(chatId: string): readonly string[];
33
+ projectConfigFor?(chatId: string): boolean;
33
34
  hiddenFor?(chatId: string): boolean;
34
35
  promptNotesFor?(chatId: string): PromptNotes | undefined;
35
36
  referencesFor?(chatId: string): ChatReferences | undefined;
@@ -307,6 +307,7 @@ export class ChatCore {
307
307
  instructions: () => (this.hooks.instructionsFor ? this.hooks.instructionsFor(payload.chatId) : this.instructions),
308
308
  resumeNote: () => this.hooks.resumeNoteFor?.(payload.chatId) ?? null,
309
309
  folders: () => this.hooks.foldersFor?.(payload.chatId) ?? [],
310
+ projectConfig: () => this.hooks.projectConfigFor?.(payload.chatId) ?? true,
310
311
  ...(promptNotes ? { promptNotes } : {}),
311
312
  ...(references ? { references } : {}),
312
313
  ...(this.checkpoints ? { checkpoints: this.checkpoints } : {}),
@@ -42,6 +42,7 @@ export interface ChatSessionOptions {
42
42
  instructions?(): string | null;
43
43
  resumeNote?(): string | null;
44
44
  folders?(): readonly string[];
45
+ projectConfig?(): boolean;
45
46
  promptNotes?: PromptNotes;
46
47
  references?: ChatReferences;
47
48
  checkpoints?: TurnCheckpoints;
@@ -96,6 +97,7 @@ export declare class ChatSession {
96
97
  private relocating;
97
98
  private launchedRuntimeMode;
98
99
  private launchedFolders;
100
+ private launchedProjectConfig;
99
101
  private turnReady;
100
102
  private staleResults;
101
103
  private spokenTurnId;
@@ -233,6 +235,7 @@ export declare class ChatSession {
233
235
  private watchStart;
234
236
  private clearStartWarning;
235
237
  private requiresReplacement;
238
+ private loadsProjectConfig;
236
239
  private hasNativeWork;
237
240
  private processInUse;
238
241
  private replacementWaits;
@@ -60,6 +60,7 @@ export class ChatSession {
60
60
  relocating = false;
61
61
  launchedRuntimeMode = null;
62
62
  launchedFolders = [];
63
+ launchedProjectConfig = true;
63
64
  // The checkpoint of the turn in flight; everything queued for that turn waits for it.
64
65
  turnReady = Promise.resolve();
65
66
  // Turns we settled ourselves whose `result` is still on its way; it may not close the turn after them.
@@ -1527,7 +1528,11 @@ export class ChatSession {
1527
1528
  this.info.runtimeMode !== this.launchedRuntimeMode ||
1528
1529
  this.info.account !== this.launchedAccount ||
1529
1530
  this.info.cwd !== this.launchedCwd ||
1530
- !sameFolders(this.options.folders?.() ?? [], this.launchedFolders)));
1531
+ !sameFolders(this.options.folders?.() ?? [], this.launchedFolders) ||
1532
+ this.loadsProjectConfig() !== this.launchedProjectConfig));
1533
+ }
1534
+ loadsProjectConfig() {
1535
+ return this.options.projectConfig?.() !== false;
1531
1536
  }
1532
1537
  hasNativeWork() {
1533
1538
  return runningInBackground(this.thread.list()).length > 0 || (this.info.background?.length ?? 0) > 0;
@@ -1551,6 +1556,10 @@ export class ChatSession {
1551
1556
  if (this.backend && this.starting) {
1552
1557
  return this.starting;
1553
1558
  }
1559
+ const projectConfig = this.loadsProjectConfig();
1560
+ if (!projectConfig && !this.options.provider.ignoresProjectConfig) {
1561
+ return Promise.reject(new ChatError('project-config-unsupported', `${this.options.provider.name} cannot run without the configuration of its folder`));
1562
+ }
1554
1563
  let env;
1555
1564
  try {
1556
1565
  env = this.options.env(this.thread.info.account);
@@ -1567,6 +1576,7 @@ export class ChatSession {
1567
1576
  this.launchedCwd = info.cwd;
1568
1577
  const folders = this.options.folders?.() ?? [];
1569
1578
  this.launchedFolders = folders;
1579
+ this.launchedProjectConfig = projectConfig;
1570
1580
  const launch = {
1571
1581
  command: this.options.command,
1572
1582
  cwd: info.cwd,
@@ -1581,6 +1591,7 @@ export class ChatSession {
1581
1591
  resumeNote: this.options.resumeNote?.() ?? null,
1582
1592
  ...(info.goal === undefined ? {} : { goal: info.goal }),
1583
1593
  ...(folders.length > 0 ? { folders } : {}),
1594
+ ...(projectConfig ? {} : { projectConfig }),
1584
1595
  ...(this.options.spawn ? { spawn: this.options.spawn } : {})
1585
1596
  };
1586
1597
  const made = { backend: null };
@@ -42,10 +42,17 @@ export class ClaudeBackend {
42
42
  if (this.child) {
43
43
  return Promise.resolve();
44
44
  }
45
- const { selection, runtimeMode, resume, folders } = this.launch;
45
+ const { selection, runtimeMode, resume, folders, projectConfig } = this.launch;
46
46
  const args = [
47
47
  ...this.launch.command,
48
- ...claudeArgs({ selection, runtimeMode, resume, allowedTools: this.allowedTools, ...(folders ? { folders } : {}) })
48
+ ...claudeArgs({
49
+ selection,
50
+ runtimeMode,
51
+ resume,
52
+ allowedTools: this.allowedTools,
53
+ ...(folders ? { folders } : {}),
54
+ ...(projectConfig === undefined ? {} : { projectConfig })
55
+ })
49
56
  ];
50
57
  if (this.launch.instructions !== null) {
51
58
  args.push('--append-system-prompt', this.launch.instructions);
@@ -1,4 +1,4 @@
1
1
  import { CodedError } from '../coded-error.ts';
2
- export type ChatErrorCode = 'refused-query' | 'stale-ui-query' | 'history-expired' | 'chat-not-found' | 'chat-unreadable' | 'chat-busy' | 'request-not-found' | 'chat-unsupported' | 'invalid-attachments' | 'subagent-not-found' | 'subagent-stale' | 'task-not-found' | 'goal-not-found' | 'transcript-missing' | 'transcript-format' | 'fork-failed' | 'turn-not-found' | 'item-not-found' | 'bookmark-not-found' | 'too-many-bookmarks' | 'visual-invalid' | 'visual-too-large';
2
+ export type ChatErrorCode = 'refused-query' | 'stale-ui-query' | 'history-expired' | 'chat-not-found' | 'chat-unreadable' | 'chat-busy' | 'request-not-found' | 'chat-unsupported' | 'project-config-unsupported' | 'invalid-attachments' | 'subagent-not-found' | 'subagent-stale' | 'task-not-found' | 'goal-not-found' | 'transcript-missing' | 'transcript-format' | 'fork-failed' | 'turn-not-found' | 'item-not-found' | 'bookmark-not-found' | 'too-many-bookmarks' | 'visual-invalid' | 'visual-too-large';
3
3
  export declare class ChatError<TCode extends string = ChatErrorCode> extends CodedError<TCode> {
4
4
  }
@@ -10,6 +10,7 @@ export function createClaudeProvider(options = {}) {
10
10
  capabilities: CLAUDE_CAPABILITIES,
11
11
  command: ['claude'],
12
12
  resumeCommand: CLAUDE_RESUME_COMMAND,
13
+ ignoresProjectConfig: true,
13
14
  // Read from the strings of Claude Code 2.1.282; never HOME, which moves the keychain lookup along with it.
14
15
  home: {
15
16
  env: 'CLAUDE_CONFIG_DIR',
@@ -6,6 +6,7 @@ interface ClaudeLaunch {
6
6
  resume: string | null;
7
7
  allowedTools?: readonly string[];
8
8
  folders?: readonly string[];
9
+ projectConfig?: boolean;
9
10
  }
10
11
  export declare function claudeArgs(launch: ClaudeLaunch): string[];
11
12
  export declare function claudeEnv(selection: ModelSelection): Record<string, string>;
@@ -11,6 +11,12 @@ const CLAUDE_CHAT_ARGS = [
11
11
  '--permission-prompt-tool',
12
12
  'stdio'
13
13
  ];
14
+ /*
15
+ * Only the person's own settings load, and only the MCP servers a caller passes: the project and
16
+ * local settings files, the project's `.mcp.json` and the project's skills and commands stay out.
17
+ * The latter flag also drops the person's own MCP servers and connectors (measured on Claude Code 2.1.300).
18
+ */
19
+ const CLAUDE_WITHOUT_PROJECT_CONFIG_ARGS = ['--setting-sources=user', '--strict-mcp-config'];
14
20
  // Effort values the CLI takes on its flag; anything else is asked for in the prompt instead.
15
21
  const FLAG_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
16
22
  const PERMISSION_MODE = {
@@ -30,7 +36,8 @@ export function claudeArgs(launch) {
30
36
  const args = [
31
37
  ...CLAUDE_CHAT_ARGS,
32
38
  ...(launch.allowedTools ?? []).map((tool) => `--allowedTools=${tool}`),
33
- ...(launch.folders ?? []).map((folder) => `--add-dir=${folder}`)
39
+ ...(launch.folders ?? []).map((folder) => `--add-dir=${folder}`),
40
+ ...(launch.projectConfig === false ? CLAUDE_WITHOUT_PROJECT_CONFIG_ARGS : [])
34
41
  ];
35
42
  const contextWindow = launch.selection.options.contextWindow;
36
43
  args.push('--model', `${launch.selection.model}${contextWindow === '1m' ? '[1m]' : ''}`);
@@ -11,6 +11,9 @@ export function createCodexProvider(options = {}) {
11
11
  capabilities: CODEX_CAPABILITIES,
12
12
  command: ['codex', ...CODEX_CHAT_ARGS],
13
13
  resumeCommand: CODEX_RESUME_COMMAND,
14
+ // A folder's `.codex` layer loads once the person's own config trusts the folder or its repository, which a worktree inherits;
15
+ // neither `-c` nor `project_root_markers` turns it off, and `--disable hooks` would take the person's hooks along (Codex 0.162.1).
16
+ ignoresProjectConfig: false,
14
17
  // The auth variables Codex 0.157 names in its own "no Codex credentials were found" advice.
15
18
  home: { env: 'CODEX_HOME', fallback: '.codex', loginEnv: ['OPENAI_API_KEY', 'CODEX_API_KEY', 'CODEX_ACCESS_TOKEN'], loginCommand: 'codex login' },
16
19
  detect: detectCli,
@@ -15,6 +15,7 @@ export interface ChatProvider {
15
15
  readonly capabilities: ProviderCapabilities;
16
16
  readonly command: string[];
17
17
  readonly resumeCommand: string;
18
+ readonly ignoresProjectConfig?: boolean;
18
19
  readonly home?: ProviderHome;
19
20
  oneShotArgs?(prompt: string): string[];
20
21
  readonly oneShotStdinArgs?: readonly string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.25.4",
3
+ "version": "0.25.5",
4
4
  "description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -81,8 +81,8 @@
81
81
  "test": "bun --conditions=source test"
82
82
  },
83
83
  "dependencies": {
84
- "@adecore/agent-contracts": "0.25.4",
85
- "@adecore/intelligent-ui": "0.25.4",
84
+ "@adecore/agent-contracts": "0.25.5",
85
+ "@adecore/intelligent-ui": "0.25.5",
86
86
  "zod": "^4.6.5"
87
87
  },
88
88
  "devDependencies": {
@@ -45,6 +45,8 @@ export interface BackendLaunch {
45
45
  // Folders outside `cwd` the agent may reach without asking. Claude Code takes them as `--add-dir`; Codex's
46
46
  // sandbox reads the whole disk already and keeps writes to the workspace, so it needs nothing.
47
47
  folders?: readonly string[];
48
+ // False keeps the CLI from loading configuration out of `cwd` (settings, hooks, MCP servers); absent loads it.
49
+ projectConfig?: boolean;
48
50
  // How the CLI is started; a test runs a fake in the same process, everything else spawns it.
49
51
  spawn?: SpawnChatProcess;
50
52
  saveGeneratedImage?(ref: string, upload: ChatAttachmentUpload): Promise<ChatAttachment>;
@@ -125,6 +125,12 @@ export interface ChatCoreHooks {
125
125
  * next turn in a new process that resumes the thread, and never cuts a turn in flight.
126
126
  */
127
127
  foldersFor?(chatId: string): readonly string[];
128
+ /*
129
+ * Whether this chat's CLI loads the configuration of its working folder; false for a folder whose contents
130
+ * the host does not trust. Asked before every turn, like the folders. A provider with `ignoresProjectConfig`
131
+ * unset refuses the turn with `project-config-unsupported`, so a host can check the flag first.
132
+ */
133
+ projectConfigFor?(chatId: string): boolean;
128
134
  /* Whether no list of chats shows this one, which `ChatInfo.hidden` says to every client. */
129
135
  hiddenFor?(chatId: string): boolean;
130
136
  /* What the host says in front of a chat's next real prompt, beside what the chat keeps itself. */
@@ -490,6 +496,7 @@ export class ChatCore {
490
496
  instructions: () => (this.hooks.instructionsFor ? this.hooks.instructionsFor(payload.chatId) : this.instructions),
491
497
  resumeNote: () => this.hooks.resumeNoteFor?.(payload.chatId) ?? null,
492
498
  folders: () => this.hooks.foldersFor?.(payload.chatId) ?? [],
499
+ projectConfig: () => this.hooks.projectConfigFor?.(payload.chatId) ?? true,
493
500
  ...(promptNotes ? { promptNotes } : {}),
494
501
  ...(references ? { references } : {}),
495
502
  ...(this.checkpoints ? { checkpoints: this.checkpoints } : {}),
@@ -78,6 +78,8 @@ export interface ChatSessionOptions {
78
78
  resumeNote?(): string | null;
79
79
  // The folders beside the working directory, asked before every turn; a change starts the next turn in a new process.
80
80
  folders?(): readonly string[];
81
+ // False runs the CLI without the configuration of the working folder; asked before every turn like the folders.
82
+ projectConfig?(): boolean;
81
83
  promptNotes?: PromptNotes;
82
84
  references?: ChatReferences;
83
85
  // Trees per turn, so a settled turn can show what the working tree holds against its start.
@@ -184,6 +186,7 @@ export class ChatSession {
184
186
  private relocating = false;
185
187
  private launchedRuntimeMode: RuntimeMode | null = null;
186
188
  private launchedFolders: readonly string[] = [];
189
+ private launchedProjectConfig = true;
187
190
  // The checkpoint of the turn in flight; everything queued for that turn waits for it.
188
191
  private turnReady: Promise<void> = Promise.resolve();
189
192
  // Turns we settled ourselves whose `result` is still on its way; it may not close the turn after them.
@@ -1760,10 +1763,15 @@ export class ChatSession {
1760
1763
  this.info.runtimeMode !== this.launchedRuntimeMode ||
1761
1764
  this.info.account !== this.launchedAccount ||
1762
1765
  this.info.cwd !== this.launchedCwd ||
1763
- !sameFolders(this.options.folders?.() ?? [], this.launchedFolders))
1766
+ !sameFolders(this.options.folders?.() ?? [], this.launchedFolders) ||
1767
+ this.loadsProjectConfig() !== this.launchedProjectConfig)
1764
1768
  );
1765
1769
  }
1766
1770
 
1771
+ private loadsProjectConfig(): boolean {
1772
+ return this.options.projectConfig?.() !== false;
1773
+ }
1774
+
1767
1775
  private hasNativeWork(): boolean {
1768
1776
  return runningInBackground(this.thread.list()).length > 0 || (this.info.background?.length ?? 0) > 0;
1769
1777
  }
@@ -1789,6 +1797,12 @@ export class ChatSession {
1789
1797
  if (this.backend && this.starting) {
1790
1798
  return this.starting;
1791
1799
  }
1800
+ const projectConfig = this.loadsProjectConfig();
1801
+ if (!projectConfig && !this.options.provider.ignoresProjectConfig) {
1802
+ return Promise.reject(
1803
+ new ChatError('project-config-unsupported', `${this.options.provider.name} cannot run without the configuration of its folder`)
1804
+ );
1805
+ }
1792
1806
  let env: Record<string, string>;
1793
1807
  try {
1794
1808
  env = this.options.env(this.thread.info.account);
@@ -1804,6 +1818,7 @@ export class ChatSession {
1804
1818
  this.launchedCwd = info.cwd;
1805
1819
  const folders = this.options.folders?.() ?? [];
1806
1820
  this.launchedFolders = folders;
1821
+ this.launchedProjectConfig = projectConfig;
1807
1822
  const launch: BackendLaunch = {
1808
1823
  command: this.options.command,
1809
1824
  cwd: info.cwd,
@@ -1818,6 +1833,7 @@ export class ChatSession {
1818
1833
  resumeNote: this.options.resumeNote?.() ?? null,
1819
1834
  ...(info.goal === undefined ? {} : { goal: info.goal }),
1820
1835
  ...(folders.length > 0 ? { folders } : {}),
1836
+ ...(projectConfig ? {} : { projectConfig }),
1821
1837
  ...(this.options.spawn ? { spawn: this.options.spawn } : {})
1822
1838
  };
1823
1839
  const made: { backend: ChatBackend | null } = { backend: null };
@@ -54,10 +54,17 @@ export class ClaudeBackend implements ChatBackend {
54
54
  if (this.child) {
55
55
  return Promise.resolve();
56
56
  }
57
- const { selection, runtimeMode, resume, folders } = this.launch;
57
+ const { selection, runtimeMode, resume, folders, projectConfig } = this.launch;
58
58
  const args = [
59
59
  ...this.launch.command,
60
- ...claudeArgs({ selection, runtimeMode, resume, allowedTools: this.allowedTools, ...(folders ? { folders } : {}) })
60
+ ...claudeArgs({
61
+ selection,
62
+ runtimeMode,
63
+ resume,
64
+ allowedTools: this.allowedTools,
65
+ ...(folders ? { folders } : {}),
66
+ ...(projectConfig === undefined ? {} : { projectConfig })
67
+ })
61
68
  ];
62
69
  if (this.launch.instructions !== null) {
63
70
  args.push('--append-system-prompt', this.launch.instructions);
@@ -10,6 +10,8 @@ export type ChatErrorCode =
10
10
  | 'chat-busy'
11
11
  | 'request-not-found'
12
12
  | 'chat-unsupported'
13
+ // The host asked for a chat that ignores the configuration of its folder, and this CLI cannot.
14
+ | 'project-config-unsupported'
13
15
  | 'invalid-attachments'
14
16
  | 'subagent-not-found'
15
17
  // The sub-agent was started by a CLI process that has ended, so no process here can stop it.
@@ -12,6 +12,7 @@ export function createClaudeProvider(options: ClaudeBackendOptions = {}): ChatPr
12
12
  capabilities: CLAUDE_CAPABILITIES,
13
13
  command: ['claude'],
14
14
  resumeCommand: CLAUDE_RESUME_COMMAND,
15
+ ignoresProjectConfig: true,
15
16
  // Read from the strings of Claude Code 2.1.282; never HOME, which moves the keychain lookup along with it.
16
17
  home: {
17
18
  env: 'CLAUDE_CONFIG_DIR',
@@ -14,6 +14,13 @@ const CLAUDE_CHAT_ARGS = [
14
14
  'stdio'
15
15
  ];
16
16
 
17
+ /*
18
+ * Only the person's own settings load, and only the MCP servers a caller passes: the project and
19
+ * local settings files, the project's `.mcp.json` and the project's skills and commands stay out.
20
+ * The latter flag also drops the person's own MCP servers and connectors (measured on Claude Code 2.1.300).
21
+ */
22
+ const CLAUDE_WITHOUT_PROJECT_CONFIG_ARGS = ['--setting-sources=user', '--strict-mcp-config'];
23
+
17
24
  // Effort values the CLI takes on its flag; anything else is asked for in the prompt instead.
18
25
  const FLAG_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
19
26
 
@@ -37,6 +44,7 @@ interface ClaudeLaunch {
37
44
  resume: string | null;
38
45
  allowedTools?: readonly string[];
39
46
  folders?: readonly string[];
47
+ projectConfig?: boolean;
40
48
  }
41
49
 
42
50
  export function claudeArgs(launch: ClaudeLaunch): string[] {
@@ -44,7 +52,8 @@ export function claudeArgs(launch: ClaudeLaunch): string[] {
44
52
  const args = [
45
53
  ...CLAUDE_CHAT_ARGS,
46
54
  ...(launch.allowedTools ?? []).map((tool) => `--allowedTools=${tool}`),
47
- ...(launch.folders ?? []).map((folder) => `--add-dir=${folder}`)
55
+ ...(launch.folders ?? []).map((folder) => `--add-dir=${folder}`),
56
+ ...(launch.projectConfig === false ? CLAUDE_WITHOUT_PROJECT_CONFIG_ARGS : [])
48
57
  ];
49
58
  const contextWindow = launch.selection.options.contextWindow;
50
59
  args.push('--model', `${launch.selection.model}${contextWindow === '1m' ? '[1m]' : ''}`);
@@ -13,6 +13,9 @@ export function createCodexProvider(options: CodexBackendOptions = {}): ChatProv
13
13
  capabilities: CODEX_CAPABILITIES,
14
14
  command: ['codex', ...CODEX_CHAT_ARGS],
15
15
  resumeCommand: CODEX_RESUME_COMMAND,
16
+ // A folder's `.codex` layer loads once the person's own config trusts the folder or its repository, which a worktree inherits;
17
+ // neither `-c` nor `project_root_markers` turns it off, and `--disable hooks` would take the person's hooks along (Codex 0.162.1).
18
+ ignoresProjectConfig: false,
16
19
  // The auth variables Codex 0.157 names in its own "no Codex credentials were found" advice.
17
20
  home: { env: 'CODEX_HOME', fallback: '.codex', loginEnv: ['OPENAI_API_KEY', 'CODEX_API_KEY', 'CODEX_ACCESS_TOKEN'], loginCommand: 'codex login' },
18
21
  detect: detectCli,
@@ -28,6 +28,8 @@ export interface ChatProvider {
28
28
  readonly command: string[];
29
29
  // What a terminal runs to continue one of this CLI's sessions; `{id}` stands for the session id.
30
30
  readonly resumeCommand: string;
31
+ /* Whether `BackendLaunch.projectConfig: false` keeps this CLI from loading configuration out of the working folder; absent is no. */
32
+ readonly ignoresProjectConfig?: boolean;
31
33
  /* Where this CLI keeps its login, settings and transcripts, which is what an account is. Absent on a
32
34
  CLI with no variable to move that folder, which then only has its default account. */
33
35
  readonly home?: ProviderHome;