@cjhyy/code-shell-core 0.8.20 → 0.9.0

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.
Files changed (65) hide show
  1. package/dist/automation/desktop-authority-client.d.ts +9 -0
  2. package/dist/automation/desktop-authority-client.js +47 -0
  3. package/dist/automation/scheduler.d.ts +8 -0
  4. package/dist/automation/scheduler.js +37 -1
  5. package/dist/automation/store.js +14 -1
  6. package/dist/capabilities/index.d.ts +1 -0
  7. package/dist/cli/agent-server-stdio.js +4 -1
  8. package/dist/engine/engine-workspace-authority.d.ts +35 -0
  9. package/dist/engine/engine-workspace-authority.js +136 -0
  10. package/dist/engine/engine.d.ts +5 -10
  11. package/dist/engine/engine.js +84 -101
  12. package/dist/engine/input-attachments.d.ts +1 -0
  13. package/dist/engine/input-attachments.js +6 -2
  14. package/dist/engine/run-environment.d.ts +8 -3
  15. package/dist/engine/run-environment.js +33 -8
  16. package/dist/engine/run-image-input.d.ts +1 -0
  17. package/dist/engine/run-image-input.js +1 -0
  18. package/dist/engine/run-session-open.d.ts +2 -1
  19. package/dist/engine/run-session-open.js +6 -0
  20. package/dist/engine/run-setup.d.ts +1 -0
  21. package/dist/engine/run-setup.js +2 -1
  22. package/dist/engine/run-tooling.d.ts +2 -0
  23. package/dist/engine/run-tooling.js +3 -0
  24. package/dist/engine/run-types.d.ts +2 -0
  25. package/dist/engine/run-workspace.d.ts +6 -1
  26. package/dist/engine/run-workspace.js +47 -0
  27. package/dist/engine/subagent-spawner.d.ts +1 -0
  28. package/dist/engine/subagent-spawner.js +1 -0
  29. package/dist/engine/types.d.ts +2 -0
  30. package/dist/index.d.ts +3 -3
  31. package/dist/index.extension.d.ts +1 -1
  32. package/dist/index.internal.d.ts +2 -2
  33. package/dist/index.internal.js +1 -0
  34. package/dist/index.js +1 -1
  35. package/dist/plugins/pluginAutomationTemplates.d.ts +2 -0
  36. package/dist/plugins/pluginAutomationTemplates.js +4 -0
  37. package/dist/prompt/composer.d.ts +4 -1
  38. package/dist/prompt/composer.js +11 -3
  39. package/dist/protocol/chat-session-manager.d.ts +42 -1
  40. package/dist/protocol/chat-session-manager.js +167 -4
  41. package/dist/protocol/chat-session.d.ts +9 -1
  42. package/dist/protocol/chat-session.js +20 -2
  43. package/dist/protocol/mobile-remote-types.d.ts +15 -0
  44. package/dist/protocol/server.d.ts +1 -2
  45. package/dist/protocol/server.js +21 -51
  46. package/dist/protocol/session-workspace-rpc.d.ts +23 -0
  47. package/dist/protocol/session-workspace-rpc.js +171 -0
  48. package/dist/protocol/types.d.ts +45 -1
  49. package/dist/protocol/types.js +4 -0
  50. package/dist/session/session-manager.d.ts +18 -0
  51. package/dist/session/session-manager.js +87 -0
  52. package/dist/tool-system/builtin/cron.d.ts +11 -0
  53. package/dist/tool-system/builtin/cron.js +27 -2
  54. package/dist/tool-system/builtin/edit.js +5 -3
  55. package/dist/tool-system/builtin/write.js +5 -3
  56. package/dist/tool-system/context.d.ts +4 -1
  57. package/dist/tool-system/executor.js +1 -1
  58. package/dist/tool-system/path-policy.d.ts +11 -3
  59. package/dist/tool-system/path-policy.js +56 -36
  60. package/dist/types.d.ts +6 -0
  61. package/dist/workspace/canonical-key.d.ts +7 -0
  62. package/dist/workspace/canonical-key.js +39 -0
  63. package/dist/workspace/workspace-context.d.ts +26 -0
  64. package/dist/workspace/workspace-context.js +112 -0
  65. package/package.json +1 -1
@@ -9,6 +9,7 @@ import { modelEntriesFromConnections } from "./model-connections-pool.js";
9
9
  import { cumulativeCacheHitRate, foldRunUsage, normalizeCumulativeUsageCounters, } from "../session/usage.js";
10
10
  import { enqueueSteerItem, consumeSteerItems, removeSteerItem, } from "./steer-queue.js";
11
11
  import { RunEnvironmentResolver } from "./run-environment.js";
12
+ import { legacySingleRootWorkspace } from "../workspace/workspace-context.js";
12
13
  import { asyncAgentRegistry } from "../tool-system/builtin/agent-registry.js";
13
14
  import { skillToolDef } from "../tool-system/builtin/skill.js";
14
15
  import { backgroundShellManager } from "../runtime/background-shell.js";
@@ -21,7 +22,8 @@ import { runShellHook, shellHookMatches } from "../hooks/shell-runner.js";
21
22
  import { ContextManager } from "../context/manager.js";
22
23
  import { CONTEXT_PACKAGE_MAX_OUTPUT_TOKENS, buildAnchoredSummaryMessage, buildContextPackagePromptFromSerialized, estimateTokens, groupMessagesByApiRound, serializeContextPackageMessages, clampContextRatios as clampContextRatiosImpl, } from "../context/compaction.js";
23
24
  import { PromptComposer } from "../prompt/composer.js";
24
- import { SessionManager, assertSafeSessionId, isEphemeralSessionState, sessionsRoot, } from "../session/session-manager.js";
25
+ import { SessionManager, isEphemeralSessionState, sessionsRoot, } from "../session/session-manager.js";
26
+ import { createAuthorizedSessionMessageService, migrateOwnedSessionMainRoot, releaseOwnedSessionWorkspace, SessionWorkspaceAuthorityTracker, setOwnedSessionWorkspace, } from "./engine-workspace-authority.js";
25
27
  import { createRunUsageAccounting, wireRunModelFacade } from "./run-accounting.js";
26
28
  import { logger, runWithSid } from "../logging/logger.js";
27
29
  import { recordSessionStart } from "../logging/session-recorder.js";
@@ -198,6 +200,7 @@ export class Engine {
198
200
  lastMessages;
199
201
  lastSessionId;
200
202
  compactedMessagesBySession = new Map();
203
+ workspaceAuthorities = new SessionWorkspaceAuthorityTracker();
201
204
  /**
202
205
  * SIDs whose ctx-bar seed we've already emitted in this process. The seed
203
206
  * is a rough char/4 estimate; only useful before the first real
@@ -260,6 +263,7 @@ export class Engine {
260
263
  * writers are additionally fenced by SessionManager's persisted revision CAS.
261
264
  */
262
265
  runInProgress = false;
266
+ disposed = false;
263
267
  agentControlStateListener;
264
268
  agentDirectionsDeliveredListener;
265
269
  /** Permission update requested while runInProgress. Applied in run() finally. */
@@ -404,8 +408,7 @@ export class Engine {
404
408
  throw new ConfigError("EngineConfig.composition is mutually exclusive with modules");
405
409
  }
406
410
  // Single composition fact source for every module contribution.
407
- this.composition =
408
- config.composition ?? compileComposition({ modules: config.modules ?? [] });
411
+ this.composition = config.composition ?? compileComposition({ modules: config.modules ?? [] });
409
412
  this.toolCatalog = compositionToolCatalog(this.composition);
410
413
  this.toolGuards = new Map(this.toolCatalog.flatMap((tool) => tool.exposure.availability
411
414
  ? [[tool.definition.name, tool.exposure.availability]]
@@ -941,6 +944,8 @@ export class Engine {
941
944
  * policy (for example ChatSession's FIFO queue).
942
945
  */
943
946
  async run(task, options) {
947
+ if (this.disposed)
948
+ throw new Error("Engine has been disposed");
944
949
  if (this.runInProgress) {
945
950
  throw new Error("Engine.run() cannot start while another run is in progress");
946
951
  }
@@ -987,12 +992,16 @@ export class Engine {
987
992
  resolveBehaviorProfile: (kind, mode) => this.resolveBehaviorProfile(kind, mode),
988
993
  configPermissionMode: this.config.permissionMode,
989
994
  configCwd: this.config.cwd,
995
+ configWorkspaceContext: this.config.workspaceContext,
990
996
  settings: this.getSettingsManager(),
991
997
  processCwd: process.cwd(),
992
998
  });
993
999
  if (!workspaceResolved.ok)
994
1000
  return workspaceResolved.result;
995
- const { sessionKind, sessionWorkspaceProfile, profile, profileParams, runPermissionMode, runPlanMode, cwd, profileState: { workspaceProfile: runWorkspaceProfile, sessionProfileOverrides, profileMemoryDir, }, } = workspaceResolved.resolution;
1001
+ const { sessionKind, sessionWorkspaceProfile, profile, profileParams, runPermissionMode, runPlanMode, cwd, workspaceContext, authoritativeWorkspaceContext, profileState: { workspaceProfile: runWorkspaceProfile, sessionProfileOverrides, profileMemoryDir, }, } = workspaceResolved.resolution;
1002
+ if (options?.sessionId) {
1003
+ this.workspaceAuthorities.remember(options.sessionId, workspaceContext);
1004
+ }
996
1005
  /** Structured results the profile's run services report; keyed per profile contract. */
997
1006
  let profileReportedResults;
998
1007
  // Wrap the caller's onStream so we can intercept `task_update`
@@ -1018,6 +1027,7 @@ export class Engine {
1018
1027
  llm: this.config.llm,
1019
1028
  sessionId: options?.sessionId,
1020
1029
  attachments: options?.attachments,
1030
+ workspaceContext,
1021
1031
  });
1022
1032
  if (!imageInput.ok)
1023
1033
  return imageInput.result;
@@ -1039,6 +1049,7 @@ export class Engine {
1039
1049
  const toolCtx = await this.wireRunSandboxToolContext({
1040
1050
  options,
1041
1051
  cwd,
1052
+ workspaceContext,
1042
1053
  runPermissionMode,
1043
1054
  runPlanMode,
1044
1055
  profile,
@@ -1080,6 +1091,12 @@ export class Engine {
1080
1091
  cwd,
1081
1092
  sessionKind,
1082
1093
  sessionWorkspaceProfile,
1094
+ projectBinding: authoritativeWorkspaceContext
1095
+ ? {
1096
+ projectId: workspaceContext.projectId,
1097
+ mainRootId: workspaceContext.sessionMainRootId,
1098
+ }
1099
+ : undefined,
1083
1100
  ...(options?.sessionBrief ? { sessionBrief: options.sessionBrief } : {}),
1084
1101
  llmModel: this.config.llm.model,
1085
1102
  llmProvider: this.config.llm.provider,
@@ -1181,6 +1198,7 @@ export class Engine {
1181
1198
  options,
1182
1199
  session,
1183
1200
  cwd,
1201
+ workspaceContext,
1184
1202
  toolCtx,
1185
1203
  profile,
1186
1204
  profileParams,
@@ -1463,66 +1481,12 @@ export class Engine {
1463
1481
  }
1464
1482
  /** Attach a closed-set, host-routed Session message sender to this run. */
1465
1483
  attachSessionMessageService(toolCtx, session, options) {
1466
- const sourceSessionId = session.state.sessionId;
1467
- const sourceRoot = this.sessionManager.readSessionMainRoot(sourceSessionId);
1468
- if (!sourceRoot)
1469
- return;
1470
- if (!this.sessionMessageRouter)
1471
- return;
1472
- const catalog = [];
1473
- const seen = new Set();
1474
- const rawTargets = Array.isArray(options?.sessionMessageTargets)
1475
- ? [...options.sessionMessageTargets]
1476
- : [];
1477
- for (const raw of rawTargets.slice(0, 100)) {
1478
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
1479
- continue;
1480
- const candidate = raw;
1481
- const sessionId = typeof candidate.sessionId === "string" ? candidate.sessionId : "";
1482
- try {
1483
- assertSafeSessionId(sessionId);
1484
- }
1485
- catch {
1486
- continue;
1487
- }
1488
- if (seen.has(sessionId))
1489
- continue;
1490
- if (candidate.workspaceRoot !== sourceRoot)
1491
- continue;
1492
- const title = typeof candidate.title === "string" ? candidate.title.trim() : "";
1493
- if (!title || title.length > 512)
1494
- continue;
1495
- const workspaceProfile = typeof candidate.workspaceProfile === "string"
1496
- ? candidate.workspaceProfile.trim().slice(0, 256)
1497
- : "";
1498
- seen.add(sessionId);
1499
- catalog.push({
1500
- sessionId,
1501
- title,
1502
- workspaceRoot: sourceRoot,
1503
- ...(workspaceProfile ? { workspaceProfile } : {}),
1504
- });
1505
- }
1506
- const targets = catalog.filter((target) => target.sessionId !== sourceSessionId);
1507
- toolCtx.sessionMessages = {
1508
- targets,
1509
- send: async ({ targetSessionId, message }) => {
1510
- const target = targets.find((candidate) => candidate.sessionId === targetSessionId);
1511
- if (!target)
1512
- throw new Error("target Session is not in the host-authorized project list");
1513
- if (!message.trim())
1514
- throw new Error("message is required");
1515
- if (message.length > 48_000)
1516
- throw new Error("message exceeds 48000 characters");
1517
- await this.sessionMessageRouter({
1518
- sourceSessionId,
1519
- target,
1520
- message,
1521
- catalog,
1522
- });
1523
- return target;
1524
- },
1525
- };
1484
+ toolCtx.sessionMessages = createAuthorizedSessionMessageService({
1485
+ sessionManager: this.sessionManager,
1486
+ router: this.sessionMessageRouter,
1487
+ sourceSessionId: session.state.sessionId,
1488
+ rawTargets: options?.sessionMessageTargets,
1489
+ });
1526
1490
  }
1527
1491
  /**
1528
1492
  * Await the parallel prompt/context assembly (LLM client handshake, system
@@ -1656,12 +1620,13 @@ export class Engine {
1656
1620
  * lazily via `getSession` because it is opened after this wiring runs.
1657
1621
  */
1658
1622
  async wireRunSandboxToolContext(args) {
1659
- const { options, cwd, runPermissionMode, runPlanMode, profile, profileParams, sessionProfileOverrides, profileMemoryDir, getSession, reportResult, } = args;
1623
+ const { options, cwd, workspaceContext, runPermissionMode, runPlanMode, profile, profileParams, sessionProfileOverrides, profileMemoryDir, getSession, reportResult, } = args;
1660
1624
  // Resolve before constructing the child spawner: a parent sandbox may come
1661
1625
  // solely from project/user settings rather than config.sandbox, while a
1662
1626
  // child intentionally skips project settings. Passing the complete
1663
1627
  // effective config is what makes undefined role sandbox mean inherit.
1664
- const sandboxConfig = this.runEnvironmentResolver.resolveSandboxConfig(cwd);
1628
+ const sandboxRun = { cwd, workspaceContext };
1629
+ const sandboxConfig = this.runEnvironmentResolver.resolveSandboxConfig(sandboxRun);
1665
1630
  // Build the per-Engine ToolContext that will be threaded through every
1666
1631
  // tool call. Replaces the old module-level singleton setters used by
1667
1632
  // built-ins and product capabilities.
@@ -1670,6 +1635,7 @@ export class Engine {
1670
1635
  parentSandbox: sandboxConfig,
1671
1636
  presetName: this.preset.name,
1672
1637
  cwd,
1638
+ workspaceContext,
1673
1639
  permissionMode: runPermissionMode,
1674
1640
  modelPool: this.modelPool,
1675
1641
  parentStream: options?.onStream,
@@ -1696,7 +1662,7 @@ export class Engine {
1696
1662
  //
1697
1663
  // Backend is cached per runtime/engine so the capability probe runs once
1698
1664
  // per (mode, cwd) instead of every turn.
1699
- const sandboxBackend = await this.runEnvironmentResolver.resolveSandbox(cwd);
1665
+ const sandboxBackend = await this.runEnvironmentResolver.resolveSandbox(sandboxRun);
1700
1666
  // Observability: surface what sandbox actually applied this run — the
1701
1667
  // configured mode vs the resolved backend (auto may downgrade to off when
1702
1668
  // no OS backend is available) + the network policy. Without this you can't
@@ -1720,6 +1686,7 @@ export class Engine {
1720
1686
  ? sandboxBackend
1721
1687
  : { ...sandboxBackend, network: sandboxConfig.network },
1722
1688
  cwd,
1689
+ workspace: workspaceContext,
1723
1690
  shellEnv: this.runEnvironmentResolver.readShellEnv(cwd),
1724
1691
  profile,
1725
1692
  profileParams,
@@ -1976,7 +1943,7 @@ export class Engine {
1976
1943
  * this method — only the composer and tool defs cross back out.
1977
1944
  */
1978
1945
  async wireRunTooling(args) {
1979
- const { options, session, cwd, toolCtx, profile, profileParams, runWorkspaceProfile, profileMemoryDir, sessionProfileOverrides, runPlanMode, } = args;
1946
+ const { options, session, cwd, workspaceContext, toolCtx, profile, profileParams, runWorkspaceProfile, profileMemoryDir, sessionProfileOverrides, runPlanMode, } = args;
1980
1947
  const visibilityExplicitGoal = normalizeGoal(options?.goal);
1981
1948
  const visibilityLifecycle = session.state.goalLifecycle;
1982
1949
  const visibilityStoredGoal = visibilityLifecycle && isGoalLifecycleCurrent(visibilityLifecycle)
@@ -1995,6 +1962,7 @@ export class Engine {
1995
1962
  const profileCanUseSkills = !runAllowedToolNames || runAllowedToolNames.has(skillToolDef.name);
1996
1963
  const promptComposer = new PromptComposer(buildPromptComposerConfig({
1997
1964
  cwd,
1965
+ workspace: workspaceContext,
1998
1966
  model: this.config.llm.model,
1999
1967
  preset: this.preset,
2000
1968
  customSystemPrompt: profile?.disableInstructions
@@ -2157,6 +2125,7 @@ export class Engine {
2157
2125
  llm: this.config.llm,
2158
2126
  sessionId: sid,
2159
2127
  attachments: item.attachments,
2128
+ workspaceContext: toolCtx.workspace,
2160
2129
  });
2161
2130
  if (!steerImageInput.ok) {
2162
2131
  throw new Error(steerImageInput.result.text);
@@ -2526,6 +2495,27 @@ export class Engine {
2526
2495
  getConfig() {
2527
2496
  return this.config;
2528
2497
  }
2498
+ /** Release Engine-local registrations after its owning ChatSession is replaced. */
2499
+ async dispose() {
2500
+ if (this.disposed)
2501
+ return;
2502
+ if (this.runInProgress)
2503
+ throw new Error("Engine cannot be disposed while a run is in progress");
2504
+ this.disposed = true;
2505
+ try {
2506
+ const mcp = this.runtime?.mcpPool ?? this.mcpManager;
2507
+ await mcp?.unregisterOwner(this);
2508
+ }
2509
+ finally {
2510
+ this.hooks.clear();
2511
+ this.settingsHookHandles = [];
2512
+ this.workspaceAuthorities.clear();
2513
+ this.steerQueueBySid.clear();
2514
+ this.compactedMessagesBySession.clear();
2515
+ this.agentDefsCache = undefined;
2516
+ this.lastContextManager = undefined;
2517
+ }
2518
+ }
2529
2519
  /**
2530
2520
  * Config hot-reload "layer 2": merge a disk-default config patch into this
2531
2521
  * ALREADY-RUNNING session's `this.config`, reload settings hooks, and
@@ -2797,40 +2787,30 @@ export class Engine {
2797
2787
  }
2798
2788
  return had;
2799
2789
  }
2800
- /**
2801
- * Persist a workspace pointer through the Engine that owns the live bundle.
2802
- * Host-side workspace actions use this RPC-facing seam so advancing the disk
2803
- * revision also rebases the active run before its next progress write.
2804
- */
2805
2790
  setSessionWorkspace(sessionId, workspace) {
2806
- if (!sessionId || !this.sessionManager.exists(sessionId))
2807
- return null;
2808
- try {
2809
- const stateRevision = this.sessionManager.setSessionWorkspace(sessionId, workspace);
2810
- if (this.activeRunSession?.state.sessionId === sessionId) {
2811
- Object.assign(this.activeRunSession.state, { workspace, stateRevision });
2812
- }
2813
- return workspace;
2814
- }
2815
- catch {
2816
- return null;
2817
- }
2791
+ return setOwnedSessionWorkspace({
2792
+ sessionManager: this.sessionManager,
2793
+ activeState: this.activeRunSession?.state,
2794
+ sessionId,
2795
+ workspace,
2796
+ });
2797
+ }
2798
+ migrateSessionMainRoot(sessionId, project, mainRoot) {
2799
+ return migrateOwnedSessionMainRoot({
2800
+ sessionManager: this.sessionManager,
2801
+ activeState: this.activeRunSession?.state,
2802
+ authorities: this.workspaceAuthorities,
2803
+ sessionId,
2804
+ project,
2805
+ mainRoot,
2806
+ });
2818
2807
  }
2819
- /**
2820
- * Reset a session's workspace pointer back to its main root. If the session is
2821
- * actively running, mutate that live SessionBundle first so the run's next
2822
- * saveState cannot resurrect a stale worktree pointer.
2823
- */
2824
2808
  releaseSessionWorkspace(sessionId) {
2825
- if (!sessionId || !this.sessionManager.exists(sessionId))
2826
- return null;
2827
- const mainRoot = this.sessionManager.readSessionMainRoot(sessionId) ??
2828
- (this.activeRunSession?.state.sessionId === sessionId
2829
- ? this.activeRunSession.state.cwd
2830
- : undefined);
2831
- if (!mainRoot)
2832
- return null;
2833
- return this.setSessionWorkspace(sessionId, { root: mainRoot, kind: "main" });
2809
+ return releaseOwnedSessionWorkspace({
2810
+ sessionManager: this.sessionManager,
2811
+ activeState: this.activeRunSession?.state,
2812
+ sessionId,
2813
+ });
2834
2814
  }
2835
2815
  injectContext(sessionId, content) {
2836
2816
  const session = this.sessionManager.resume(sessionId);
@@ -3295,7 +3275,10 @@ export class Engine {
3295
3275
  const service = value({
3296
3276
  isSubAgent: this.config.isSubAgent === true,
3297
3277
  settings: this.getSettingsManager(),
3298
- resolveSandbox: (cwd) => this.runEnvironmentResolver.resolveSandbox(cwd),
3278
+ resolveSandbox: (cwd) => this.runEnvironmentResolver.resolveSandbox({
3279
+ cwd,
3280
+ workspaceContext: legacySingleRootWorkspace(cwd),
3281
+ }),
3299
3282
  readShellEnv: (cwd) => this.runEnvironmentResolver.readShellEnv(cwd),
3300
3283
  getSessionManager: () => this.sessionManager,
3301
3284
  });
@@ -3311,7 +3294,7 @@ export class Engine {
3311
3294
  capabilityServices,
3312
3295
  askUser: this.config.askUser,
3313
3296
  browser: this.config.browserBridge,
3314
- workspace: this.config.workspaceBridge,
3297
+ workspaceBridge: this.config.workspaceBridge,
3315
3298
  panels: this.config.panelBridge,
3316
3299
  injectCredentialToBrowser: this.config.injectCredentialToBrowser,
3317
3300
  isSubAgent: this.config.isSubAgent === true,
@@ -9,5 +9,6 @@ export interface InputAttachmentContext {
9
9
  export interface BuildInputAttachmentContextOptions {
10
10
  includeImageBytes?: boolean;
11
11
  expectedSessionId?: string;
12
+ workspaceRoots?: readonly string[];
12
13
  }
13
14
  export declare function buildInputAttachmentContext(attachments: readonly InputAttachmentMeta[] | undefined, cwd: string, options?: BuildInputAttachmentContextOptions): Promise<InputAttachmentContext>;
@@ -113,7 +113,11 @@ export async function buildInputAttachmentContext(attachments, cwd, options = {}
113
113
  });
114
114
  continue;
115
115
  }
116
- const policy = classifyPath(resolved, { workspaceRoot: cwdReal, operation: "read" });
116
+ const policy = classifyPath(resolved, {
117
+ workspaceRoot: cwdReal,
118
+ workspaceRoots: options.workspaceRoots,
119
+ operation: "read",
120
+ });
117
121
  if (policy.decision !== "allow") {
118
122
  errors.push(`attachment ${id === "(unknown)" ? displayPath : id} blocked by path policy: ${policy.reason}`);
119
123
  diagnostics.push({
@@ -141,7 +145,7 @@ export async function buildInputAttachmentContext(attachments, cwd, options = {}
141
145
  continue;
142
146
  }
143
147
  if (attachment.kind === "directory" || info.isDirectory()) {
144
- const tree = await directoryTree(realPath, cwdReal).catch((err) => ({
148
+ const tree = await directoryTree(realPath, policy.matchedRoot ?? cwdReal).catch((err) => ({
145
149
  lines: [`(directory tree unavailable: ${err.message})`],
146
150
  truncated: true,
147
151
  entryCount: 0,
@@ -2,6 +2,7 @@ import type { CredentialAccess } from "../credentials/access.js";
2
2
  import { type SandboxBackend, type SandboxConfig } from "../tool-system/sandbox/index.js";
3
3
  import type { EngineRuntime } from "./runtime.js";
4
4
  import type { EngineConfig } from "./types.js";
5
+ import type { WorkspaceContext } from "../workspace/workspace-context.js";
5
6
  type EnvironmentSettings = {
6
7
  get(): unknown;
7
8
  getForScope(scope: "user" | "project", cwd?: string): unknown;
@@ -13,14 +14,18 @@ export interface RunEnvironmentResolverDeps {
13
14
  runtime?: Pick<EngineRuntime, "resolveSandbox">;
14
15
  resolveBackend?: (config: SandboxConfig, cwd: string) => Promise<SandboxBackend>;
15
16
  }
17
+ export interface RunEnvironmentInput {
18
+ cwd: string;
19
+ workspaceContext: WorkspaceContext;
20
+ }
16
21
  export declare class RunEnvironmentResolver {
17
22
  private readonly deps;
18
23
  private readonly sandboxCache;
19
24
  private readonly resolveBackend;
20
25
  constructor(deps: RunEnvironmentResolverDeps);
21
- resolveSandboxConfig(cwd: string): SandboxConfig;
22
- resolveSandbox(cwd: string): Promise<SandboxBackend>;
23
- resolve(cwd: string): Promise<{
26
+ resolveSandboxConfig(run: RunEnvironmentInput): SandboxConfig;
27
+ resolveSandbox(run: RunEnvironmentInput): Promise<SandboxBackend>;
28
+ resolve(run: RunEnvironmentInput): Promise<{
24
29
  sandbox: SandboxBackend;
25
30
  sandboxConfig: SandboxConfig;
26
31
  shellEnv?: Record<string, string>;
@@ -1,6 +1,28 @@
1
- import { resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
1
+ import { expandPath, resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
2
2
  import { resolveSandboxConfig } from "./sandbox-config.js";
3
3
  import { sandboxCacheKey } from "./sandbox-cache-key.js";
4
+ import { canonicalKey, canonicalPath } from "../workspace/canonical-key.js";
5
+ function appendWorkspaceRoots(config, run) {
6
+ const writableRoots = [];
7
+ const seen = new Set();
8
+ // Key configured roots after placeholder expansion so `${workspace}` wins
9
+ // over the equivalent primary path. Keep the first spelling and order.
10
+ for (const root of config.writableRoots) {
11
+ const key = canonicalKey(expandPath(root, run.cwd));
12
+ if (seen.has(key))
13
+ continue;
14
+ seen.add(key);
15
+ writableRoots.push(root);
16
+ }
17
+ for (const root of run.workspaceContext.roots) {
18
+ const key = canonicalKey(root.path);
19
+ if (seen.has(key))
20
+ continue;
21
+ seen.add(key);
22
+ writableRoots.push(canonicalPath(root.path));
23
+ }
24
+ return { ...config, writableRoots };
25
+ }
4
26
  export class RunEnvironmentResolver {
5
27
  deps;
6
28
  sandboxCache = new Map();
@@ -9,7 +31,8 @@ export class RunEnvironmentResolver {
9
31
  this.deps = deps;
10
32
  this.resolveBackend = deps.resolveBackend ?? resolveSandboxBackend;
11
33
  }
12
- resolveSandboxConfig(cwd) {
34
+ resolveSandboxConfig(run) {
35
+ const { cwd } = run;
13
36
  const config = this.deps.config();
14
37
  let projectSandbox;
15
38
  let globalSandbox;
@@ -24,10 +47,11 @@ export class RunEnvironmentResolver {
24
47
  catch {
25
48
  // Missing settings fall through to the run default.
26
49
  }
27
- return resolveSandboxConfig(config.sandbox, projectSandbox, globalSandbox, config.headless === true);
50
+ return appendWorkspaceRoots(resolveSandboxConfig(config.sandbox, projectSandbox, globalSandbox, config.headless === true), run);
28
51
  }
29
- resolveSandbox(cwd) {
30
- const config = this.resolveSandboxConfig(cwd);
52
+ resolveSandbox(run) {
53
+ const { cwd } = run;
54
+ const config = this.resolveSandboxConfig(run);
31
55
  if (this.deps.runtime)
32
56
  return this.deps.runtime.resolveSandbox(config, cwd);
33
57
  const key = sandboxCacheKey(config, cwd);
@@ -42,9 +66,10 @@ export class RunEnvironmentResolver {
42
66
  }
43
67
  return cached;
44
68
  }
45
- async resolve(cwd) {
46
- const sandboxConfig = this.resolveSandboxConfig(cwd);
47
- const backend = await this.resolveSandbox(cwd);
69
+ async resolve(run) {
70
+ const { cwd } = run;
71
+ const sandboxConfig = this.resolveSandboxConfig(run);
72
+ const backend = await this.resolveSandbox(run);
48
73
  return {
49
74
  sandbox: backend.name === "off" ? backend : { ...backend, network: sandboxConfig.network },
50
75
  sandboxConfig,
@@ -18,5 +18,6 @@ export declare function prepareRunImageInput(args: {
18
18
  llm: Pick<LLMConfig, "provider" | "providerKind" | "model">;
19
19
  sessionId?: string;
20
20
  attachments?: readonly InputAttachmentMeta[];
21
+ workspaceContext?: import("../workspace/workspace-context.js").WorkspaceContext;
21
22
  }): Promise<PrepareRunImageInputResult>;
22
23
  export declare function buildRunUserMessageContent(parsedTask: ParsedTask, cwd: string, taskText: string): string | ContentBlock[];
@@ -38,6 +38,7 @@ export async function prepareRunImageInput(args) {
38
38
  attachmentContext = await buildInputAttachmentContext(args.attachments, cwd, {
39
39
  includeImageBytes: cap.supportsVision,
40
40
  expectedSessionId: sessionId,
41
+ workspaceRoots: args.workspaceContext?.roots.map((root) => root.path),
41
42
  });
42
43
  }
43
44
  catch (err) {
@@ -1,4 +1,4 @@
1
- import type { Message, SessionKind } from "../types.js";
1
+ import type { Message, SessionKind, SessionProjectBinding } from "../types.js";
2
2
  import type { SessionBundle, SessionManager } from "../session/session-manager.js";
3
3
  import type { ParsedTask } from "./parse-task.js";
4
4
  import type { buildRunUserMessageContent } from "./run-image-input.js";
@@ -13,6 +13,7 @@ export interface OpenRunSessionArgs {
13
13
  cwd: string;
14
14
  sessionKind: SessionKind;
15
15
  sessionWorkspaceProfile: string | undefined;
16
+ projectBinding?: SessionProjectBinding;
16
17
  /** Standing brief resent by the host each turn until the Session persists it. */
17
18
  sessionBrief?: string;
18
19
  llmModel: string;
@@ -30,6 +30,9 @@ export function openRunSession(args) {
30
30
  if (options?.sessionId && args.sessionManager.exists(options.sessionId)) {
31
31
  resumedFromDisk = true;
32
32
  session = args.sessionManager.resumeForRun(options.sessionId);
33
+ if (!session.state.project && args.projectBinding) {
34
+ args.sessionManager.saveStateOrUpdateFields(session.state, { project: args.projectBinding });
35
+ }
33
36
  const cachedCompacted = args.cachedCompactedMessages;
34
37
  messages = cachedCompacted ? [...cachedCompacted] : session.transcript.toMessages();
35
38
  // If the previous run was Ctrl+C'd or crashed between an assistant
@@ -114,6 +117,9 @@ export function openRunSession(args) {
114
117
  // Cold start: shape (2) reuses the host-supplied sid; shape (3)
115
118
  // lets sessionManager generate one with nanoid.
116
119
  session = args.sessionManager.create(args.cwd, args.llmModel, args.llmProvider, options?.sessionId, args.isSubAgent ? getCurrentSid() : undefined, args.isSubAgent ? "subagent" : args.origin, args.sessionKind, options?.ephemeral === true);
120
+ if (args.projectBinding) {
121
+ args.sessionManager.saveStateOrUpdateFields(session.state, { project: args.projectBinding });
122
+ }
117
123
  const userMsg = { role: "user", content: args.userMessageContent };
118
124
  claimClientMessageId(session, options?.clientMessageId, "submit");
119
125
  if (args.parsedTask.hasImages)
@@ -16,6 +16,7 @@ export declare function resolveRunProfileState(args: {
16
16
  }): RunProfileState;
17
17
  export interface RunPromptComposerConfigInput {
18
18
  cwd: ComposerOptions["cwd"];
19
+ workspace: ComposerOptions["workspace"];
19
20
  /** Standing brief persisted on the Session (see SessionState.sessionBrief). */
20
21
  sessionBrief?: ComposerOptions["sessionBrief"];
21
22
  model: ComposerOptions["model"];
@@ -23,9 +23,10 @@ export function resolveRunProfileState(args) {
23
23
  }
24
24
  /** Build the prompt-composer options for a run without capturing the Engine facade. */
25
25
  export function buildPromptComposerConfig(args) {
26
- const { cwd, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, memoryCurrentProjectOnly, disableInstructions, disableMemoryContext, disableCapabilityContext, disableSourcesContext, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
26
+ const { cwd, workspace, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, memoryCurrentProjectOnly, disableInstructions, disableMemoryContext, disableCapabilityContext, disableSourcesContext, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
27
27
  return {
28
28
  cwd,
29
+ workspace,
29
30
  model,
30
31
  preset,
31
32
  customSystemPrompt,
@@ -29,6 +29,7 @@ export declare function buildRunToolContext(args: {
29
29
  agentDefinitions: ToolContext["agentDefinitions"];
30
30
  sandbox: ToolContext["sandbox"];
31
31
  cwd: string;
32
+ workspace: ToolContext["workspace"];
32
33
  shellEnv: ToolContext["shellEnv"];
33
34
  profile: RunBehaviorProfile | undefined;
34
35
  profileParams: Readonly<Record<string, unknown>>;
@@ -73,6 +74,7 @@ export declare function connectRunMcp(args: {
73
74
  */
74
75
  export interface ToolVisibilityInputs {
75
76
  cwd: string;
77
+ workspace?: ToolContext["workspace"];
76
78
  hasGoal: boolean;
77
79
  sessionId?: string;
78
80
  settingsScope?: SettingsScope;
@@ -28,6 +28,7 @@ export function buildRunToolContext(args) {
28
28
  // properties and survive the spread. Off keeps network undefined.
29
29
  sandbox: args.sandbox,
30
30
  cwd: args.cwd,
31
+ workspace: args.workspace,
31
32
  shellEnv: args.shellEnv,
32
33
  // TodoWrite reads this to push task_update events independently
33
34
  // of its return value, so the UI's pinned task panel refreshes
@@ -166,6 +167,7 @@ export async function connectRunMcp(args) {
166
167
  export function buildToolVisibility(inputs) {
167
168
  return {
168
169
  cwd: inputs.cwd,
170
+ ...(inputs.workspace ? { workspace: inputs.workspace } : {}),
169
171
  hasGoal: inputs.hasGoal,
170
172
  ...(inputs.sessionId ? { sessionId: inputs.sessionId } : {}),
171
173
  ...(inputs.settingsScope ? { settingsScope: inputs.settingsScope } : {}),
@@ -186,6 +188,7 @@ export function assembleRunToolDefs(args) {
186
188
  const guardCwd = args.guardCwd;
187
189
  const toolVisibility = buildToolVisibility({
188
190
  cwd: guardCwd,
191
+ workspace: toolCtx.workspace,
189
192
  hasGoal: args.hasRunnableGoal,
190
193
  sessionId: toolCtx.sessionId,
191
194
  settingsScope: args.settingsScope,
@@ -87,6 +87,8 @@ export declare const ISOLATED_TASK_SYSTEM_PROMPT = "# Isolated Task Boundary\n\n
87
87
  export declare const ISOLATED_TASK_PROFILE: RunBehaviorProfile;
88
88
  export interface EngineRunOptions {
89
89
  cwd?: string;
90
+ /** Trusted run-scoped root authorization. Protocol hosts must strip untrusted values. */
91
+ workspaceContext?: import("../workspace/workspace-context.js").WorkspaceContext;
90
92
  onStream?: StreamCallback;
91
93
  signal?: AbortSignal;
92
94
  sessionId?: string;
@@ -11,6 +11,7 @@ import type { SettingsManager } from "../settings/manager.js";
11
11
  import type { EngineConfig, EngineResult } from "./types.js";
12
12
  import type { EngineRunOptions, RunBehaviorProfile } from "./run-types.js";
13
13
  import { type RunProfileState } from "./run-setup.js";
14
+ import { type WorkspaceContext } from "../workspace/workspace-context.js";
14
15
  /**
15
16
  * Resolve the working directory for a run. Precedence for legacy sessions:
16
17
  * options.cwd > resumed session's state.cwd > config.cwd > process.cwd()
@@ -30,6 +31,9 @@ export interface RunWorkspaceResolution {
30
31
  runPermissionMode: NonNullable<EngineConfig["permissionMode"]>;
31
32
  runPlanMode: boolean;
32
33
  cwd: string;
34
+ workspaceContext: WorkspaceContext;
35
+ /** False only for the compatibility context synthesized from a lone cwd. */
36
+ authoritativeWorkspaceContext: boolean;
33
37
  profileState: RunProfileState;
34
38
  }
35
39
  export type ResolveRunWorkspaceResult = {
@@ -41,10 +45,11 @@ export type ResolveRunWorkspaceResult = {
41
45
  };
42
46
  export declare function resolveRunWorkspace(args: {
43
47
  options: EngineRunOptions | undefined;
44
- sessionManager: Pick<SessionManager, "exists" | "readSessionKind" | "readSessionWorkspaceProfile" | "resolveSessionWorkspaceForResume" | "readSessionMainRoot">;
48
+ sessionManager: Pick<SessionManager, "exists" | "readSessionKind" | "readSessionWorkspaceProfile" | "resolveSessionWorkspaceForResume" | "readSessionMainRoot" | "readSessionProjectBinding">;
45
49
  resolveBehaviorProfile: (sessionKind: string, behaviorMode: string | undefined) => RunBehaviorProfile | undefined;
46
50
  configPermissionMode: EngineConfig["permissionMode"];
47
51
  configCwd: string | undefined;
52
+ configWorkspaceContext: WorkspaceContext | undefined;
48
53
  settings: SettingsManager;
49
54
  processCwd: string;
50
55
  }): Promise<ResolveRunWorkspaceResult>;