@lazyingart/agintiflow 0.20.303 → 0.20.305

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.303",
3
+ "version": "0.20.305",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
@@ -2370,6 +2370,35 @@ try {
2370
2370
  /outside the active task artifact scope/.test(String(outsideScopedRead.reason || "")),
2371
2371
  "task-scoped artifact tools could read an unrelated repository file"
2372
2372
  );
2373
+ const writeScopedRead = await executeWorkspaceTool(
2374
+ "read_file",
2375
+ { path: "unrelated.md" },
2376
+ {
2377
+ commandCwd: workspace,
2378
+ allowFileTools: true,
2379
+ workspaceWritePathScopeRoots: ["output/task-scope"],
2380
+ }
2381
+ );
2382
+ assert(
2383
+ writeScopedRead.content === "unrelated\n",
2384
+ "write-scoped artifact work could not read safe workspace evidence"
2385
+ );
2386
+ const outsideWriteScopedWrite = await executeWorkspaceTool(
2387
+ "write_file",
2388
+ { path: "outside-scoped-write.md", content: "blocked\n" },
2389
+ {
2390
+ commandCwd: workspace,
2391
+ allowFileTools: true,
2392
+ workspaceWritePathScopeRoots: ["output/task-scope"],
2393
+ }
2394
+ );
2395
+ assert(
2396
+ outsideWriteScopedWrite.blocked === true &&
2397
+ /outside the active task artifact scope/.test(
2398
+ String(outsideWriteScopedWrite.reason || "")
2399
+ ),
2400
+ "write-scoped artifact work could mutate an unrelated repository file"
2401
+ );
2373
2402
  const filenameSearch = await executeWorkspaceTool(
2374
2403
  "search_files",
2375
2404
  { path: ".", query: "routine_entry.py", maxResults: 5 },
@@ -5575,12 +5575,77 @@ const scopedRuntime = nextStepRuntimeConfig(
5575
5575
  }
5576
5576
  );
5577
5577
  assertStrict.equal(scopedRuntime.scopedArtifactTask, true);
5578
- assertStrict.deepEqual(scopedRuntime.workspacePathScopeRoots, ["output/wechat_worker/task"]);
5578
+ assertStrict.deepEqual(scopedRuntime.workspaceWritePathScopeRoots, ["output/wechat_worker/task"]);
5579
+ assertStrict.equal(
5580
+ scopedRuntime.workspacePathScopeRoots,
5581
+ undefined,
5582
+ "task artifact output incorrectly hid repository evidence from safe reads"
5583
+ );
5579
5584
  assertStrict.equal(
5580
5585
  scopedRuntime.repositoryGroundingRequired,
5581
5586
  undefined,
5582
5587
  "an explicit task artifact root still activated generic repository grounding"
5583
5588
  );
5589
+ const readOnlyAuditRequest =
5590
+ "Check readiness, read configs/model-policy.json, and report git status without changing source files or sending messages.";
5591
+ const readOnlyAuditPrompt = `AGINTI_EVIDENCE_SCOPE_JSON: ${JSON.stringify({
5592
+ mode: "task",
5593
+ request: readOnlyAuditRequest,
5594
+ artifact_root: "/workspace/output/webapp/agent/tasks/read-only-audit",
5595
+ })}`;
5596
+ const readOnlyAuditRuntime = nextStepRuntimeConfig(
5597
+ {
5598
+ provider: "localllm",
5599
+ goal: readOnlyAuditPrompt,
5600
+ commandCwd: "/workspace",
5601
+ },
5602
+ {
5603
+ goal: readOnlyAuditPrompt,
5604
+ commandCwd: "/workspace",
5605
+ meta: {
5606
+ goalContract: {
5607
+ revision: 1,
5608
+ currentRequest: readOnlyAuditRequest,
5609
+ },
5610
+ activeExecutionContract: {
5611
+ revision: 1,
5612
+ startedMutationRevision: 0,
5613
+ requiresWorkspaceMutation: false,
5614
+ requiresFileMutation: false,
5615
+ requiresSourceGrounding: false,
5616
+ },
5617
+ projectVerification: { mutationRevision: 0 },
5618
+ },
5619
+ }
5620
+ );
5621
+ assertStrict.equal(
5622
+ readOnlyAuditRuntime.scopedArtifactTask,
5623
+ undefined,
5624
+ "an optional host result directory incorrectly sandboxed a read-only workspace audit"
5625
+ );
5626
+ assertStrict.equal(
5627
+ readOnlyAuditRuntime.workspaceWritePathScopeRoots,
5628
+ undefined,
5629
+ "a read-only workspace audit inherited an unrelated artifact write scope"
5630
+ );
5631
+ const readOnlyAuditTools = selectProgressiveTools(allTools, {
5632
+ config: {
5633
+ provider: "localllm",
5634
+ progressiveTools: true,
5635
+ ...readOnlyAuditRuntime,
5636
+ },
5637
+ goal: readOnlyAuditPrompt,
5638
+ profile: "auto",
5639
+ messages: [{ role: "user", content: readOnlyAuditPrompt }],
5640
+ });
5641
+ assert(
5642
+ names(readOnlyAuditTools).includes("run_command"),
5643
+ "a command-evidence audit omitted run_command from the compact local surface"
5644
+ );
5645
+ assert(
5646
+ names(readOnlyAuditTools).includes("read_file"),
5647
+ "an audit with an exact workspace input omitted read_file from the compact local surface"
5648
+ );
5584
5649
  const scopedPathWriteDescriptor = {
5585
5650
  type: "function",
5586
5651
  function: {
@@ -5683,9 +5748,9 @@ assertStrict.equal(
5683
5748
  "long-context compaction discarded the host-owned artifact scope"
5684
5749
  );
5685
5750
  assertStrict.deepEqual(
5686
- longScopedRuntime.workspacePathScopeRoots,
5751
+ longScopedRuntime.workspaceWritePathScopeRoots,
5687
5752
  ["output/wechat_worker/long-scoped-task"],
5688
- "long-context artifact repair escaped its exact task root"
5753
+ "long-context artifact writes escaped their exact task root"
5689
5754
  );
5690
5755
  assertStrict.equal(
5691
5756
  longScopedRuntime.completionFreshMutationRequired,
@@ -205,6 +205,20 @@ const readOnlyReviewContract = deriveScsTaskContract({
205
205
  taskProfile: "review",
206
206
  });
207
207
 
208
+ const readOnlyOperatorAuditRequest =
209
+ "This is a short imperfect operator audit. Check these three things without changing source files or sending messages: 1) run curl -fsS http://127.0.0.1:8008/readyz and state whether LocalLLM is ready; 2) read configs/model-policy.json and name the configured AgInTi provider order; 3) run git status --short and say only whether tracked source changes exist, without treating ignored runtime output as source. Cover all three numbered items in one concise answer and write the normal task result.";
210
+ const readOnlyOperatorAuditContract = deriveScsTaskContract({
211
+ goal: [
212
+ readOnlyOperatorAuditRequest,
213
+ `AGINTI_EVIDENCE_SCOPE_JSON: ${JSON.stringify({
214
+ mode: "task",
215
+ request: readOnlyOperatorAuditRequest,
216
+ artifact_root: "/tmp/aginti-read-only-audit",
217
+ })}`,
218
+ ].join("\n\n"),
219
+ taskProfile: "auto",
220
+ });
221
+
208
222
  const explicitReviewRepairContract = deriveScsTaskContract({
209
223
  goal: [
210
224
  "Review focus: inspect changed files and fix confirmed defects.",
@@ -374,6 +388,34 @@ assert.equal(
374
388
  true,
375
389
  "an explicit review-and-fix focus lost its positive mutation request"
376
390
  );
391
+ assert.equal(
392
+ readOnlyOperatorAuditContract.requiresWorkspaceMutation,
393
+ false,
394
+ "a host-managed task response was mistaken for a workspace mutation"
395
+ );
396
+ assert.equal(
397
+ readOnlyOperatorAuditContract.requiresFileMutation,
398
+ false,
399
+ "an observational source-status mention was mistaken for a source mutation"
400
+ );
401
+ assert(
402
+ readOnlyOperatorAuditContract.requiredEvidence.some(
403
+ (item) => item.category === "command"
404
+ ) &&
405
+ !readOnlyOperatorAuditContract.requiredEvidence.some(
406
+ (item) => ["artifact", "file"].includes(item.category)
407
+ ),
408
+ "the phrase 'cover all items' invented cover-art or file evidence"
409
+ );
410
+ assert(
411
+ readOnlyOperatorAuditContract.forbiddenActions.some((item) =>
412
+ /changing source files or sending messages/i.test(item)
413
+ ) &&
414
+ !readOnlyOperatorAuditContract.forbiddenActions.some((item) =>
415
+ /run curl/i.test(item)
416
+ ),
417
+ "a colon after a without-clause swallowed the following command"
418
+ );
377
419
  assert(
378
420
  pageSafeReportContract.requiredEvidence.some((item) => item.category === "file"),
379
421
  "a scoped existing-report edit did not require file evidence"
@@ -15034,8 +15034,30 @@ export function nextStepRuntimeConfig(config = {}, state = {}) {
15034
15034
  const scopedRootRelative = resolvedScopedRoot
15035
15035
  ? path.relative(commandCwd, resolvedScopedRoot)
15036
15036
  : "";
15037
- const scopedArtifactTask = Boolean(
15037
+ const exactScopedOutputs = Array.isArray(groundingTaskContract.exactOutputPaths)
15038
+ ? groundingTaskContract.exactOutputPaths
15039
+ .map((item) => String(item || "").trim())
15040
+ .filter(Boolean)
15041
+ : [];
15042
+ const exactOutputsStayInsideScopedRoot = Boolean(
15038
15043
  resolvedScopedRoot &&
15044
+ exactScopedOutputs.length > 0 &&
15045
+ exactScopedOutputs.every((item) => {
15046
+ const absoluteOutput = path.resolve(commandCwd, item);
15047
+ const relativeOutput = path.relative(resolvedScopedRoot, absoluteOutput);
15048
+ return (
15049
+ relativeOutput === "" ||
15050
+ (!relativeOutput.startsWith("..") && !path.isAbsolute(relativeOutput))
15051
+ );
15052
+ })
15053
+ );
15054
+ const scopedArtifactTask = Boolean(
15055
+ (
15056
+ groundingTaskContract.scopedArtifactDeliverable === true ||
15057
+ groundingTaskContract.scopedArtifactOperation === true ||
15058
+ exactOutputsStayInsideScopedRoot
15059
+ ) &&
15060
+ resolvedScopedRoot &&
15039
15061
  scopedRootRelative &&
15040
15062
  !scopedRootRelative.startsWith("..") &&
15041
15063
  !path.isAbsolute(scopedRootRelative)
@@ -15043,7 +15065,7 @@ export function nextStepRuntimeConfig(config = {}, state = {}) {
15043
15065
  if (scopedArtifactTask) {
15044
15066
  runtimeConfig.scopedArtifactTask = true;
15045
15067
  runtimeConfig.scopedArtifactRoot = scopedRootRelative.replace(/\\/g, "/");
15046
- runtimeConfig.workspacePathScopeRoots = [runtimeConfig.scopedArtifactRoot];
15068
+ runtimeConfig.workspaceWritePathScopeRoots = [runtimeConfig.scopedArtifactRoot];
15047
15069
  }
15048
15070
  if (
15049
15071
  (
@@ -4,7 +4,11 @@ import {
4
4
  hasLocalResearchWorkspaceIntent,
5
5
  shouldStartWithDeepResearch,
6
6
  } from "./research-routing.js";
7
- import { hasAgintiEvidenceScope, scopedChatopsEvidenceGoal } from "./scs-evidence.js";
7
+ import {
8
+ deriveScsTaskContract,
9
+ hasAgintiEvidenceScope,
10
+ scopedChatopsEvidenceGoal,
11
+ } from "./scs-evidence.js";
8
12
  import {
9
13
  INTEGRATION_TEXT_WORKSPACE_PROFILE_ID,
10
14
  isIntegrationTextWorkspaceToolAllowed,
@@ -2204,6 +2208,32 @@ function compactToolNames({ config, goal, profile, messages }) {
2204
2208
  const explicitBundles = bundlesForProfile(profileId(config, profile));
2205
2209
  const inferred = inferredBundles(taskText(goal, config, messages));
2206
2210
  const explicitProfileTools = profileToolNames(profile);
2211
+ const taskContract = deriveScsTaskContract({
2212
+ goal: goal || config.goal || "",
2213
+ taskProfile: profileId(config, profile),
2214
+ });
2215
+ const contractRequiredTools = [];
2216
+ if (
2217
+ (Array.isArray(taskContract.requiredEvidence) &&
2218
+ taskContract.requiredEvidence.some(
2219
+ (item) => String(item?.category || "") === "command"
2220
+ )) ||
2221
+ (Array.isArray(taskContract.requiredProjectCommands) &&
2222
+ taskContract.requiredProjectCommands.length > 0)
2223
+ ) {
2224
+ contractRequiredTools.push("run_command");
2225
+ }
2226
+ if (
2227
+ Array.isArray(taskContract.exactInputPaths) &&
2228
+ taskContract.exactInputPaths.length > 0
2229
+ ) {
2230
+ contractRequiredTools.push("read_file");
2231
+ }
2232
+ for (const name of taskContract.requiredToolCalls || []) {
2233
+ if (COMPACT_ELIGIBLE_TOOL_NAMES.has(String(name || "").trim())) {
2234
+ contractRequiredTools.push(String(name).trim());
2235
+ }
2236
+ }
2207
2237
  const bundleOrder = [];
2208
2238
  for (const bundle of explicitBundles) bundleOrder.push(bundle);
2209
2239
  for (const bundle of inferred) {
@@ -2232,7 +2262,10 @@ function compactToolNames({ config, goal, profile, messages }) {
2232
2262
  for (const name of COMPACT_TOOL_BUNDLES[bundle] || []) append(name);
2233
2263
  }
2234
2264
  for (const name of explicitProfileTools) append(name);
2235
- return names;
2265
+ const missingContractTools = [
2266
+ ...new Set(contractRequiredTools.filter((name) => !seen.has(name))),
2267
+ ];
2268
+ return [...missingContractTools, ...names];
2236
2269
  }
2237
2270
 
2238
2271
  /**
@@ -2739,21 +2772,9 @@ export function selectProgressiveTools(
2739
2772
  if (config.scopedArtifactTask === true && config.scopedArtifactRoot) {
2740
2773
  const available = new Map(enabled.map(({ name, tool }) => [name, tool]));
2741
2774
  const scopedWorkspaceTools = [
2742
- constrainScopedArtifactPath(
2743
- available.get("list_files"),
2744
- config.scopedArtifactRoot,
2745
- { commandCwd: config.commandCwd }
2746
- ),
2747
- constrainScopedArtifactPath(
2748
- available.get("read_file"),
2749
- config.scopedArtifactRoot,
2750
- { commandCwd: config.commandCwd }
2751
- ),
2752
- constrainScopedArtifactPath(
2753
- available.get("search_files"),
2754
- config.scopedArtifactRoot,
2755
- { commandCwd: config.commandCwd }
2756
- ),
2775
+ available.get("list_files"),
2776
+ available.get("read_file"),
2777
+ available.get("search_files"),
2757
2778
  constrainScopedArtifactPath(
2758
2779
  available.get("write_file"),
2759
2780
  config.scopedArtifactRoot,
@@ -2767,9 +2788,9 @@ export function selectProgressiveTools(
2767
2788
  available.get("run_command"),
2768
2789
  ].filter(Boolean);
2769
2790
 
2770
- // Artifact isolation limits where the task may read or write; it must not
2771
- // erase the task's research, browser, or specialist capabilities. Keep
2772
- // inferred non-file tools while the workspace tools remain root-scoped.
2791
+ // A host-owned artifact root confines artifact writes, but it must not hide
2792
+ // the workspace evidence needed to produce those artifacts. Keep safe reads
2793
+ // and inferred non-file tools available while write tools remain root-scoped.
2773
2794
  const scopedWorkspaceToolNames = new Set(
2774
2795
  scopedWorkspaceTools.map((tool) => openAiFunctionName(tool)).filter(Boolean)
2775
2796
  );
@@ -1275,8 +1275,19 @@ function goalRequestsExplicitTestMutation(text = "") {
1275
1275
  );
1276
1276
  }
1277
1277
 
1278
+ function stripHostManagedResponseNarration(goal = "") {
1279
+ return String(goal || "").replace(
1280
+ /\b(?:and\s+)?(?:write|return|provide)\s+(?:(?:a|the)\s+)?(?:(?:concise|final|normal|structured)\s+)?(?:(?:agent|task)\s+)?(?:answer|response|result)\b(?!\s+(?:as|at|file|in|into|json|markdown|pdf|text|to|under)\b)/gi,
1281
+ ""
1282
+ );
1283
+ }
1284
+
1278
1285
  function goalRequestsWorkspaceMutation(goal = "", taskProfile = "") {
1279
- const text = normalizedText(stripCompletedWorkNarration(stripForbiddenLanguage(goal)));
1286
+ const text = normalizedText(
1287
+ stripHostManagedResponseNarration(
1288
+ stripCompletedWorkNarration(stripForbiddenLanguage(goal))
1289
+ )
1290
+ );
1280
1291
  const explicitAddMutation =
1281
1292
  goalRequestsExplicitTestMutation(text) ||
1282
1293
  /\badd\b(?:\s+(?:a|an|the|new|additional|specific))*\s+(?:code|documents?|files?|notes?|readme|scripts?|source|workspace)\b/.test(
@@ -1330,6 +1341,20 @@ function goalRequestsScopedArtifactDeliverable(goal = "") {
1330
1341
  return !projectSourceMutation;
1331
1342
  }
1332
1343
 
1344
+ function goalRequestsScopedArtifactOperation(goal = "") {
1345
+ const text = normalizedText(stripCompletedWorkNarration(stripForbiddenLanguage(goal)));
1346
+ const artifactOperation =
1347
+ /\b(?:build|compile|create|draft|edit|export|generate|make|prepare|produce|render|repair|revise|save|update|write)\b[^.\n;]{0,180}\b(?:artifact|brief|cad|diagram|document|figure|image|markdown|memo|model|note|paper|pcb|pdf|presentation|prompt|report|research note|slide deck|slides?|spreadsheet|story|summary|transcript|video)\b/.test(text) ||
1348
+ /\b(?:artifact|brief|cad|diagram|document|figure|image|markdown|memo|model|note|paper|pcb|pdf|presentation|prompt|report|research note|slide deck|slides?|spreadsheet|story|summary|transcript|video)\b[^.\n;]{0,120}\b(?:build|compile|create|draft|edit|export|generate|make|prepare|produce|render|repair|revise|save|update|write)\b/.test(text) ||
1349
+ /(?:创建|生成|保存|编写|撰写|制作|导出|准备|修改|修订|修复|更新)[^。;\n]{0,100}(?:报告|笔记|摘要|文档|论文|图表|图片|模型|演示文稿|幻灯片|表格|故事|提示词|转录|视频)/.test(text);
1350
+ if (!artifactOperation) return false;
1351
+ const projectSourceMutation =
1352
+ /\b(?:change|debug|edit|fix|implement|improve|modify|patch|refactor|repair|replace|rewrite|update)\b[^.\n;]{0,140}\b(?:app|application|code|codebase|implementation|library|package|repo|repository|runtime|source\s+code|src\/)\b/.test(text) ||
1353
+ /\b(?:app|application|code|codebase|implementation|library|package|repo|repository|runtime|source\s+code|src\/)\b[^.\n;]{0,140}\b(?:change|debug|edit|fix|implement|improve|modify|patch|refactor|repair|replace|rewrite|update)\b/.test(text) ||
1354
+ /(?:修改|修复|实现|改进|重构|调试|替换|更新)[^。;\n]{0,100}(?:代码|代码库|实现|运行时|应用|仓库|源码)/.test(text);
1355
+ return !projectSourceMutation;
1356
+ }
1357
+
1333
1358
  function goalRequestsTestExecution(goal = "") {
1334
1359
  const text = normalizedText(stripCompletedWorkNarration(stripForbiddenLanguage(goal)));
1335
1360
  return (
@@ -1448,7 +1473,12 @@ function inferRequirementCategories(goal = "", taskProfile = "", acceptanceCrite
1448
1473
  if (goalRequestsTestExecution(positiveGoal)) {
1449
1474
  categories.add("test");
1450
1475
  }
1451
- if (textHas(mandatoryEvidenceText, /\b(artifact|canvas|pdf|image|video|screenshot|cover|plot|chart|figure|docx|archive|copy to|export|generated|generate|draft)\b/) || /输出|产物|图片|视频|截图|封面|生成/.test(mandatoryEvidenceText)) {
1476
+ if (
1477
+ textHas(
1478
+ mandatoryEvidenceText,
1479
+ /\b(artifact|canvas|pdf|image|video|screenshot|plot|chart|figure|docx|archive|copy to|export|generated|generate|draft)\b|\b(?:album|book|paper|report|video)\s+cover\b|\bcover\s+(?:art|design|image)\b/
1480
+ ) || /输出|产物|图片|视频|截图|封面|生成/.test(mandatoryEvidenceText)
1481
+ ) {
1452
1482
  categories.add("artifact");
1453
1483
  }
1454
1484
  if (
@@ -1513,14 +1543,14 @@ function inferForbiddenActions(goal = "") {
1513
1543
  const text = String(goal || "");
1514
1544
  const forbidden = [];
1515
1545
  const isAction = (value = "") =>
1516
- /\b(use|open|click|browse|browser|upload|attach|submit|publish|deploy|run|execute|install|delete|remove|commit|push|call|api|alter|change|edit|fix|modify|patch|repair|rewrite|touch|write)\b/i.test(
1546
+ /\b(use|open|click|browse|browser|upload|attach|submit|publish|deploy|run|execute|install|delete|remove|commit|push|call|api|alter|chang(?:e|ing)|edit(?:ing)?|fix(?:ing)?|modif(?:y|ying)|patch(?:ing)?|repair(?:ing)?|rewrit(?:e|ing)|send(?:ing)?|touch(?:ing)?|writ(?:e|ing))\b/i.test(
1517
1547
  value
1518
1548
  ) || /浏览器|网页|打开|点击|上传|提交|发布|部署|运行|执行|安装|删除|复制|移动|修改|编辑|修复|改写|写入|提交代码|推送|调用|API/.test(value);
1519
1549
  const patterns = [
1520
1550
  { re: /\b(do not|don't|dont|never|no need to)\s+([^.\n;]+)/gi, prefix: "User forbids" },
1521
1551
  // A sentence such as "without changing X, run tests and commit" starts a
1522
1552
  // positive instruction after the comma. Keep only the local without-clause.
1523
- { re: /\bwithout\s+([^.,\n;]+)/gi, prefix: "User forbids" },
1553
+ { re: /\bwithout\s+([^.,:\uFF1A\n;]+)/gi, prefix: "User forbids" },
1524
1554
  { re: /不要([^。\n;]+)/g, prefix: "User forbids" },
1525
1555
  { re: /禁止([^。\n;]+)/g, prefix: "User forbids" },
1526
1556
  ];
@@ -2046,6 +2076,9 @@ export function deriveScsTaskContract({ goal = "", taskProfile = "", acceptanceC
2046
2076
  const scopedArtifactDeliverable = Boolean(
2047
2077
  artifactRoot && goalRequestsScopedArtifactDeliverable(evidenceGoal)
2048
2078
  );
2079
+ const scopedArtifactOperation = Boolean(
2080
+ artifactRoot && goalRequestsScopedArtifactOperation(evidenceGoal)
2081
+ );
2049
2082
  return {
2050
2083
  version: 1,
2051
2084
  outcome: compact(evidenceGoal || "Complete the requested task.", 500),
@@ -2056,6 +2089,7 @@ export function deriveScsTaskContract({ goal = "", taskProfile = "", acceptanceC
2056
2089
  exactOutputPaths,
2057
2090
  requiredArtifactKinds,
2058
2091
  scopedArtifactDeliverable,
2092
+ scopedArtifactOperation,
2059
2093
  excludedOutputPaths,
2060
2094
  artifactRoot,
2061
2095
  exactInputPaths,
@@ -117,10 +117,15 @@ function matchingReadOnlyRoot(config, target) {
117
117
  .find((root) => isInside(root, target));
118
118
  }
119
119
 
120
- function configuredWorkspaceScopeRoots(config, root) {
121
- const values = Array.isArray(config.workspacePathScopeRoots)
122
- ? config.workspacePathScopeRoots
123
- : [];
120
+ function configuredWorkspaceScopeRoots(config, root, { writeAccess = false } = {}) {
121
+ const values = [
122
+ ...(Array.isArray(config.workspacePathScopeRoots)
123
+ ? config.workspacePathScopeRoots
124
+ : []),
125
+ ...(writeAccess && Array.isArray(config.workspaceWritePathScopeRoots)
126
+ ? config.workspaceWritePathScopeRoots
127
+ : []),
128
+ ];
124
129
  return values
125
130
  .map((item) => String(item || "").trim())
126
131
  .filter(Boolean)
@@ -267,12 +272,16 @@ export function normalizeWorkspaceInputPath(rawPath) {
267
272
  return rawPath;
268
273
  }
269
274
 
270
- export function resolveWorkspacePath(config, inputPath = ".", { allowReadOnlyRoots = false } = {}) {
275
+ export function resolveWorkspacePath(
276
+ config,
277
+ inputPath = ".",
278
+ { allowReadOnlyRoots = false, writeAccess = false } = {}
279
+ ) {
271
280
  const root = workspaceRoot(config);
272
281
  const rawPath = sanitizePathInput(inputPath);
273
282
  const workspacePath = normalizeWorkspaceInputPath(rawPath);
274
283
  const absolutePath = path.resolve(root, workspacePath);
275
- const scopedRoots = configuredWorkspaceScopeRoots(config, root);
284
+ const scopedRoots = configuredWorkspaceScopeRoots(config, root, { writeAccess });
276
285
 
277
286
  if (scopedRoots.length && !scopedRoots.some((scopeRoot) => isInside(scopeRoot, absolutePath))) {
278
287
  throw new Error(`Path is outside the active task artifact scope: ${rawPath}`);
@@ -451,7 +460,7 @@ export function checkWorkspaceToolUse(toolName, args, config) {
451
460
  const operations = parsePatchDocument(args.patch);
452
461
  for (const operation of operations) {
453
462
  for (const candidate of [operation.path, operation.newPath].filter(Boolean)) {
454
- const target = resolveWorkspacePath(config, candidate);
463
+ const target = resolveWorkspacePath(config, candidate, { writeAccess: true });
455
464
  const policy = pathPolicy(toolName, target.relativePath);
456
465
  if (!policy.allowed) return policy;
457
466
  }
@@ -462,6 +471,7 @@ export function checkWorkspaceToolUse(toolName, args, config) {
462
471
  }
463
472
  const target = resolveWorkspacePath(config, args.path || ".", {
464
473
  allowReadOnlyRoots: !WORKSPACE_WRITE_TOOL_NAMES.includes(toolName),
474
+ writeAccess: WORKSPACE_WRITE_TOOL_NAMES.includes(toolName),
465
475
  });
466
476
  const policy = pathPolicy(toolName, target.relativePath);
467
477
  if (!policy.allowed) return policy;
@@ -1006,7 +1016,7 @@ async function deleteChange(target, action = "delete_file", details = {}) {
1006
1016
  }
1007
1017
 
1008
1018
  async function writeFile(config, args) {
1009
- const target = resolveWorkspacePath(config, args.path);
1019
+ const target = resolveWorkspacePath(config, args.path, { writeAccess: true });
1010
1020
  const mode = args.mode === "overwrite" ? "overwrite" : "create";
1011
1021
  const exists = await fs
1012
1022
  .stat(target.absolutePath)
@@ -1033,7 +1043,7 @@ async function applyPatch(config, args) {
1033
1043
  return applyPatchDocument(config, args);
1034
1044
  }
1035
1045
 
1036
- const target = resolveWorkspacePath(config, args.path);
1046
+ const target = resolveWorkspacePath(config, args.path, { writeAccess: true });
1037
1047
  const { content: beforeText, hash } = await readTextFile(target);
1038
1048
  if (args.baseHash && args.baseHash !== hash) {
1039
1049
  throw new Error(`Base hash mismatch for ${target.relativePath}; read the file again before patching.`);
@@ -1234,10 +1244,12 @@ async function applyPatchDocument(config, args) {
1234
1244
  if (seenPaths.has(operation.path)) throw new Error(`Patch contains duplicate file operations for ${operation.path}.`);
1235
1245
  seenPaths.add(operation.path);
1236
1246
 
1237
- const target = resolveWorkspacePath(config, operation.path);
1247
+ const target = resolveWorkspacePath(config, operation.path, { writeAccess: true });
1238
1248
  const policy = pathPolicy("apply_patch", target.relativePath);
1239
1249
  if (!policy.allowed) throw new Error(policy.reason);
1240
- const newTarget = operation.newPath ? resolveWorkspacePath(config, operation.newPath) : null;
1250
+ const newTarget = operation.newPath
1251
+ ? resolveWorkspacePath(config, operation.newPath, { writeAccess: true })
1252
+ : null;
1241
1253
  if (newTarget) {
1242
1254
  const newPolicy = pathPolicy("apply_patch", newTarget.relativePath);
1243
1255
  if (!newPolicy.allowed) throw new Error(newPolicy.reason);
@@ -1300,7 +1312,7 @@ async function applyPatchDocument(config, args) {
1300
1312
  continue;
1301
1313
  }
1302
1314
  if (operation.newPath) {
1303
- const newTarget = resolveWorkspacePath(config, operation.newPath);
1315
+ const newTarget = resolveWorkspacePath(config, operation.newPath, { writeAccess: true });
1304
1316
  const policy = pathPolicy("apply_patch", newTarget.relativePath);
1305
1317
  if (!policy.allowed) throw new Error(policy.reason);
1306
1318
  changes.push(await writeChange(newTarget, item.afterText, "apply_patch_move", { fromPath: target.relativePath, patchFormat: "multi-file" }));