@diff-review-system/drs 4.0.1 → 5.0.0-rc.1

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 (218) hide show
  1. package/.pi/agents/review/unified-reviewer.md +2 -6
  2. package/.pi/agents/task/okf-wiki-maintainer.md +72 -0
  3. package/.pi/agents/task/review-assistant.md +22 -0
  4. package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +12 -4
  5. package/.pi/workflows/github-pr-review-post.yaml +29 -20
  6. package/.pi/workflows/github-pr-review.yaml +37 -8
  7. package/.pi/workflows/github-pr-update-agents-md-stacked.yaml +2 -0
  8. package/.pi/workflows/github-pr-visual-explain.yaml +46 -1
  9. package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +12 -4
  10. package/.pi/workflows/gitlab-mr-review.yaml +20 -8
  11. package/.pi/workflows/gitlab-mr-update-agents-md-stacked.yaml +2 -0
  12. package/.pi/workflows/gitlab-mr-visual-explain.yaml +46 -1
  13. package/.pi/workflows/local-fix-review-issues.yaml +13 -6
  14. package/.pi/workflows/local-review.yaml +7 -1
  15. package/.pi/workflows/local-visual-explain.yaml +46 -1
  16. package/.pi/workflows/release-changelog-finalize.yaml +10 -0
  17. package/.pi/workflows/repository-wiki-check.yaml +40 -0
  18. package/.pi/workflows/repository-wiki-sync.yaml +187 -0
  19. package/.pi/workflows/temporal-control-smoke.yaml +63 -0
  20. package/.wiki-site/.vitepress/config.mts +452 -0
  21. package/.wiki-site/.vitepress/theme/PageLead.vue +122 -0
  22. package/.wiki-site/.vitepress/theme/index.ts +12 -0
  23. package/.wiki-site/.vitepress/theme/style.css +270 -0
  24. package/README.md +77 -4
  25. package/dist/cli/index.js +87 -4
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/init.d.ts +6 -1
  28. package/dist/cli/init.d.ts.map +1 -1
  29. package/dist/cli/init.js +26 -10
  30. package/dist/cli/init.js.map +1 -1
  31. package/dist/cli/run-agent.d.ts +5 -7
  32. package/dist/cli/run-agent.d.ts.map +1 -1
  33. package/dist/cli/run-agent.js +56 -5
  34. package/dist/cli/run-agent.js.map +1 -1
  35. package/dist/cli/wiki.d.ts +3 -0
  36. package/dist/cli/wiki.d.ts.map +1 -0
  37. package/dist/cli/wiki.js +174 -0
  38. package/dist/cli/wiki.js.map +1 -0
  39. package/dist/cli/workflow-executor-selection.d.ts +3 -0
  40. package/dist/cli/workflow-executor-selection.d.ts.map +1 -0
  41. package/dist/cli/workflow-executor-selection.js +15 -0
  42. package/dist/cli/workflow-executor-selection.js.map +1 -0
  43. package/dist/cli/workflow.d.ts +40 -42
  44. package/dist/cli/workflow.d.ts.map +1 -1
  45. package/dist/cli/workflow.js +533 -868
  46. package/dist/cli/workflow.js.map +1 -1
  47. package/dist/github/client.d.ts.map +1 -1
  48. package/dist/github/client.js +2 -2
  49. package/dist/github/client.js.map +1 -1
  50. package/dist/github/platform-adapter.d.ts.map +1 -1
  51. package/dist/github/platform-adapter.js +15 -1
  52. package/dist/github/platform-adapter.js.map +1 -1
  53. package/dist/gitlab/client.d.ts +4 -0
  54. package/dist/gitlab/client.d.ts.map +1 -1
  55. package/dist/gitlab/client.js +17 -1
  56. package/dist/gitlab/client.js.map +1 -1
  57. package/dist/gitlab/platform-adapter.d.ts.map +1 -1
  58. package/dist/gitlab/platform-adapter.js +17 -1
  59. package/dist/gitlab/platform-adapter.js.map +1 -1
  60. package/dist/lib/agent-permissions.d.ts +44 -0
  61. package/dist/lib/agent-permissions.d.ts.map +1 -0
  62. package/dist/lib/agent-permissions.js +358 -0
  63. package/dist/lib/agent-permissions.js.map +1 -0
  64. package/dist/lib/agent-result.d.ts +10 -0
  65. package/dist/lib/agent-result.d.ts.map +1 -0
  66. package/dist/lib/agent-result.js +2 -0
  67. package/dist/lib/agent-result.js.map +1 -0
  68. package/dist/lib/comment-poster.d.ts +1 -1
  69. package/dist/lib/comment-poster.d.ts.map +1 -1
  70. package/dist/lib/comment-poster.js +38 -16
  71. package/dist/lib/comment-poster.js.map +1 -1
  72. package/dist/lib/config.d.ts +27 -1
  73. package/dist/lib/config.d.ts.map +1 -1
  74. package/dist/lib/config.js +17 -0
  75. package/dist/lib/config.js.map +1 -1
  76. package/dist/lib/error-comment-poster.d.ts +1 -1
  77. package/dist/lib/error-comment-poster.d.ts.map +1 -1
  78. package/dist/lib/error-comment-poster.js +15 -7
  79. package/dist/lib/error-comment-poster.js.map +1 -1
  80. package/dist/lib/okf-wiki.d.ts +65 -0
  81. package/dist/lib/okf-wiki.d.ts.map +1 -0
  82. package/dist/lib/okf-wiki.js +647 -0
  83. package/dist/lib/okf-wiki.js.map +1 -0
  84. package/dist/lib/output-paths.d.ts +0 -1
  85. package/dist/lib/output-paths.d.ts.map +1 -1
  86. package/dist/lib/output-paths.js +0 -1
  87. package/dist/lib/output-paths.js.map +1 -1
  88. package/dist/lib/platform-client.d.ts +2 -0
  89. package/dist/lib/platform-client.d.ts.map +1 -1
  90. package/dist/lib/project-setup.d.ts +7 -0
  91. package/dist/lib/project-setup.d.ts.map +1 -0
  92. package/dist/lib/project-setup.js +11 -0
  93. package/dist/lib/project-setup.js.map +1 -0
  94. package/dist/lib/review-artifact-store.d.ts +16 -0
  95. package/dist/lib/review-artifact-store.d.ts.map +1 -0
  96. package/dist/lib/review-artifact-store.js +84 -0
  97. package/dist/lib/review-artifact-store.js.map +1 -0
  98. package/dist/lib/review-artifact.d.ts +11 -1
  99. package/dist/lib/review-artifact.d.ts.map +1 -1
  100. package/dist/lib/review-artifact.js +203 -0
  101. package/dist/lib/review-artifact.js.map +1 -1
  102. package/dist/lib/review-core.d.ts +1 -0
  103. package/dist/lib/review-core.d.ts.map +1 -1
  104. package/dist/lib/review-core.js +11 -14
  105. package/dist/lib/review-core.js.map +1 -1
  106. package/dist/lib/review-orchestrator.d.ts +6 -1
  107. package/dist/lib/review-orchestrator.d.ts.map +1 -1
  108. package/dist/lib/review-orchestrator.js +3 -1
  109. package/dist/lib/review-orchestrator.js.map +1 -1
  110. package/dist/lib/review-parser.d.ts.map +1 -1
  111. package/dist/lib/review-parser.js +15 -12
  112. package/dist/lib/review-parser.js.map +1 -1
  113. package/dist/lib/wiki-delta.d.ts +71 -0
  114. package/dist/lib/wiki-delta.d.ts.map +1 -0
  115. package/dist/lib/wiki-delta.js +524 -0
  116. package/dist/lib/wiki-delta.js.map +1 -0
  117. package/dist/lib/wiki-run-summary.d.ts +45 -0
  118. package/dist/lib/wiki-run-summary.d.ts.map +1 -0
  119. package/dist/lib/wiki-run-summary.js +168 -0
  120. package/dist/lib/wiki-run-summary.js.map +1 -0
  121. package/dist/lib/wiki-search.d.ts +24 -0
  122. package/dist/lib/wiki-search.d.ts.map +1 -0
  123. package/dist/lib/wiki-search.js +200 -0
  124. package/dist/lib/wiki-search.js.map +1 -0
  125. package/dist/lib/wiki-site-graph.d.ts +33 -0
  126. package/dist/lib/wiki-site-graph.d.ts.map +1 -0
  127. package/dist/lib/wiki-site-graph.js +446 -0
  128. package/dist/lib/wiki-site-graph.js.map +1 -0
  129. package/dist/lib/wiki-site-safety.d.ts +6 -0
  130. package/dist/lib/wiki-site-safety.d.ts.map +1 -0
  131. package/dist/lib/wiki-site-safety.js +83 -0
  132. package/dist/lib/wiki-site-safety.js.map +1 -0
  133. package/dist/lib/wiki-site-smoke.d.ts +16 -0
  134. package/dist/lib/wiki-site-smoke.d.ts.map +1 -0
  135. package/dist/lib/wiki-site-smoke.js +270 -0
  136. package/dist/lib/wiki-site-smoke.js.map +1 -0
  137. package/dist/lib/wiki-site.d.ts +26 -0
  138. package/dist/lib/wiki-site.d.ts.map +1 -0
  139. package/dist/lib/wiki-site.js +221 -0
  140. package/dist/lib/wiki-site.js.map +1 -0
  141. package/dist/lib/workflow/artifact-store.d.ts +71 -0
  142. package/dist/lib/workflow/artifact-store.d.ts.map +1 -0
  143. package/dist/lib/workflow/artifact-store.js +121 -0
  144. package/dist/lib/workflow/artifact-store.js.map +1 -0
  145. package/dist/lib/workflow/compiled-plan.d.ts +87 -0
  146. package/dist/lib/workflow/compiled-plan.d.ts.map +1 -0
  147. package/dist/lib/workflow/compiled-plan.js +62 -0
  148. package/dist/lib/workflow/compiled-plan.js.map +1 -0
  149. package/dist/lib/workflow/executor.d.ts +18 -0
  150. package/dist/lib/workflow/executor.d.ts.map +1 -0
  151. package/dist/lib/workflow/executor.js +2 -0
  152. package/dist/lib/workflow/executor.js.map +1 -0
  153. package/dist/lib/workflow/graph.d.ts +30 -0
  154. package/dist/lib/workflow/graph.d.ts.map +1 -0
  155. package/dist/lib/workflow/graph.js +139 -0
  156. package/dist/lib/workflow/graph.js.map +1 -0
  157. package/dist/lib/workflow/input.d.ts +4 -0
  158. package/dist/lib/workflow/input.d.ts.map +1 -0
  159. package/dist/lib/workflow/input.js +4 -0
  160. package/dist/lib/workflow/input.js.map +1 -0
  161. package/dist/lib/workflow/node-executor.d.ts +19 -0
  162. package/dist/lib/workflow/node-executor.d.ts.map +1 -0
  163. package/dist/lib/workflow/node-executor.js +2 -0
  164. package/dist/lib/workflow/node-executor.js.map +1 -0
  165. package/dist/lib/workflow/planning.d.ts +52 -0
  166. package/dist/lib/workflow/planning.d.ts.map +1 -0
  167. package/dist/lib/workflow/planning.js +865 -0
  168. package/dist/lib/workflow/planning.js.map +1 -0
  169. package/dist/lib/workflow/types.d.ts +71 -0
  170. package/dist/lib/workflow/types.d.ts.map +1 -0
  171. package/dist/lib/workflow/types.js +2 -0
  172. package/dist/lib/workflow/types.js.map +1 -0
  173. package/dist/lib/workflow-artifacts.d.ts.map +1 -1
  174. package/dist/lib/workflow-artifacts.js +23 -1
  175. package/dist/lib/workflow-artifacts.js.map +1 -1
  176. package/dist/lib/write-json-output.d.ts.map +1 -1
  177. package/dist/lib/write-json-output.js +3 -8
  178. package/dist/lib/write-json-output.js.map +1 -1
  179. package/dist/pi/sdk.d.ts.map +1 -1
  180. package/dist/pi/sdk.js +276 -19
  181. package/dist/pi/sdk.js.map +1 -1
  182. package/dist/runtime/client.d.ts +3 -0
  183. package/dist/runtime/client.d.ts.map +1 -1
  184. package/dist/runtime/client.js +4 -0
  185. package/dist/runtime/client.js.map +1 -1
  186. package/dist/temporal/activities.d.ts +26 -0
  187. package/dist/temporal/activities.d.ts.map +1 -0
  188. package/dist/temporal/activities.js +219 -0
  189. package/dist/temporal/activities.js.map +1 -0
  190. package/dist/temporal/config.d.ts +4 -0
  191. package/dist/temporal/config.d.ts.map +1 -0
  192. package/dist/temporal/config.js +36 -0
  193. package/dist/temporal/config.js.map +1 -0
  194. package/dist/temporal/executor.d.ts +7 -0
  195. package/dist/temporal/executor.d.ts.map +1 -0
  196. package/dist/temporal/executor.js +240 -0
  197. package/dist/temporal/executor.js.map +1 -0
  198. package/dist/temporal/retry-policy.d.ts +28 -0
  199. package/dist/temporal/retry-policy.d.ts.map +1 -0
  200. package/dist/temporal/retry-policy.js +61 -0
  201. package/dist/temporal/retry-policy.js.map +1 -0
  202. package/dist/temporal/types.d.ts +86 -0
  203. package/dist/temporal/types.d.ts.map +1 -0
  204. package/dist/temporal/types.js +2 -0
  205. package/dist/temporal/types.js.map +1 -0
  206. package/dist/temporal/worker.d.ts +3 -0
  207. package/dist/temporal/worker.d.ts.map +1 -0
  208. package/dist/temporal/worker.js +18 -0
  209. package/dist/temporal/worker.js.map +1 -0
  210. package/dist/temporal/workflow-id.d.ts +27 -0
  211. package/dist/temporal/workflow-id.d.ts.map +1 -0
  212. package/dist/temporal/workflow-id.js +53 -0
  213. package/dist/temporal/workflow-id.js.map +1 -0
  214. package/dist/temporal/workflows.d.ts +6 -0
  215. package/dist/temporal/workflows.d.ts.map +1 -0
  216. package/dist/temporal/workflows.js +355 -0
  217. package/dist/temporal/workflows.js.map +1 -0
  218. package/package.json +24 -9
package/dist/pi/sdk.js CHANGED
@@ -1,12 +1,19 @@
1
1
  import { randomUUID } from 'crypto';
2
2
  import { spawn } from 'child_process';
3
- import { isAbsolute, join, resolve } from 'path';
3
+ import { constants } from 'fs';
4
+ import { access, mkdir, readFile as readFsFile, unlink, writeFile as writeFsFile, } from 'fs/promises';
5
+ import { homedir } from 'os';
6
+ import { isAbsolute, join, relative, resolve, sep } from 'path';
7
+ import { fileURLToPath } from 'url';
4
8
  import { Type } from '@sinclair/typebox';
5
- import { AuthStorage, createAgentSession, DefaultResourceLoader, getAgentDir, ModelRegistry, SettingsManager, SessionManager, } from '@earendil-works/pi-coding-agent';
9
+ import { AuthStorage, createAgentSession, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, DefaultResourceLoader, getAgentDir, ModelRegistry, SettingsManager, SessionManager, } from '@earendil-works/pi-coding-agent';
6
10
  import { writeJsonOutput } from '../lib/write-json-output.js';
7
- import { writeArtifactOutput } from '../lib/html-artifact.js';
11
+ import { extractHtmlDocument, writeArtifactOutput } from '../lib/html-artifact.js';
8
12
  import { isReviewArtifactPayload } from '../lib/review-artifact.js';
9
13
  import { resolveWithinWorkingDir } from '../lib/path-utils.js';
14
+ import { AgentFilesystemAuthorizer, validateAgentPermissions, validateAgentValidation, } from '../lib/agent-permissions.js';
15
+ import { formatOkfValidationErrors, validateOkfBundle, validateOkfDocument, } from '../lib/okf-wiki.js';
16
+ import { OUTPUT_PATHS } from '../lib/output-paths.js';
10
17
  const DEFAULT_GIT_DIFF_MAX_BYTES = 120_000;
11
18
  const HARD_GIT_DIFF_MAX_BYTES = 500_000;
12
19
  const GIT_DIFF_STDERR_MAX_BYTES = 16_384;
@@ -15,6 +22,44 @@ function asRecord(value) {
15
22
  ? value
16
23
  : {};
17
24
  }
25
+ function parseAgentPermissions(value) {
26
+ if (value === undefined)
27
+ return undefined;
28
+ validateAgentPermissions(value, 'Pi runtime permissions');
29
+ return value;
30
+ }
31
+ function parseAgentValidation(value) {
32
+ if (value === undefined)
33
+ return undefined;
34
+ validateAgentValidation(value, 'Pi runtime validation');
35
+ return value;
36
+ }
37
+ function toPosixPath(value) {
38
+ return sep === '/' ? value : value.split(sep).join('/');
39
+ }
40
+ function resolvePiToolPath(workingDir, requestedPath) {
41
+ let normalized = requestedPath.replace(/[\u00A0\u2000-\u200A\u202F\u205F\u3000]/gu, ' ');
42
+ if (normalized.startsWith('@'))
43
+ normalized = normalized.slice(1);
44
+ if (normalized === '~')
45
+ normalized = homedir();
46
+ else if (normalized.startsWith('~/') ||
47
+ (process.platform === 'win32' && normalized.startsWith('~\\'))) {
48
+ normalized = join(homedir(), normalized.slice(2));
49
+ }
50
+ if (/^file:\/\//u.test(normalized))
51
+ normalized = fileURLToPath(normalized);
52
+ return isAbsolute(normalized) ? resolve(normalized) : resolve(workingDir, normalized);
53
+ }
54
+ function allowsAggregateReadTools(permissions) {
55
+ return Boolean(permissions &&
56
+ (permissions.deny?.length ?? 0) === 0 &&
57
+ permissions.allow.some((pattern) => pattern.replaceAll('\\', '/').replace(/^\.\//u, '') === '**'));
58
+ }
59
+ function allowsFullRepositoryRead(permissions) {
60
+ return Boolean(allowsAggregateReadTools(permissions) &&
61
+ permissions?.roots.some((root) => root.replaceAll('\\', '/').replace(/\/$/u, '') === '.'));
62
+ }
18
63
  function asString(value) {
19
64
  return typeof value === 'string' ? value : undefined;
20
65
  }
@@ -593,6 +638,11 @@ class PiSessionRuntime {
593
638
  sessions = new Map();
594
639
  sessionApi;
595
640
  constructor(config) {
641
+ const permissions = parseAgentPermissions(config.permissions);
642
+ const validation = parseAgentValidation(config.validation);
643
+ if (validation && !permissions?.filesystem?.write && !permissions?.filesystem?.delete) {
644
+ throw new Error('Pi runtime validation requires filesystem write or delete permissions.');
645
+ }
596
646
  this.runtimeConfig = {
597
647
  tools: asRecord(config.tools),
598
648
  agent: asRecord(config.agent),
@@ -602,6 +652,8 @@ class PiSessionRuntime {
602
652
  thinkingLevel: asString(config.thinkingLevel),
603
653
  fixChecks: parseFixChecks(config.fixChecks),
604
654
  traceCollector: config.traceCollector,
655
+ permissions,
656
+ validation,
605
657
  retry: asRecord(config.retry),
606
658
  };
607
659
  this.authStorage = AuthStorage.create();
@@ -729,34 +781,211 @@ class PiSessionRuntime {
729
781
  }
730
782
  resolveTools(_cwd, agentTools) {
731
783
  const tools = [];
784
+ const filesystemPermissions = this.runtimeConfig.permissions?.filesystem;
785
+ const aggregateReadsAllowed = !filesystemPermissions?.read || allowsAggregateReadTools(filesystemPermissions.read);
732
786
  if (this.isToolEnabled('Read', true, agentTools)) {
733
787
  tools.push('read');
734
788
  }
735
- if (this.isToolEnabled('Bash', true, agentTools)) {
789
+ if (this.runtimeConfig.permissions?.shell !== false &&
790
+ this.isToolEnabled('Bash', true, agentTools)) {
736
791
  tools.push('bash');
737
792
  }
738
- if (this.isToolEnabled('Edit', false, agentTools)) {
793
+ if ((!filesystemPermissions || filesystemPermissions.write !== undefined) &&
794
+ this.isToolEnabled('Edit', false, agentTools)) {
739
795
  tools.push('edit');
740
796
  }
741
- if (this.isToolEnabled('Write', false, agentTools)) {
797
+ if ((!filesystemPermissions || filesystemPermissions.write !== undefined) &&
798
+ this.isToolEnabled('Write', false, agentTools)) {
742
799
  tools.push('write');
743
800
  }
744
- if (this.isToolEnabled('Grep', true, agentTools)) {
801
+ if (aggregateReadsAllowed && this.isToolEnabled('Grep', true, agentTools)) {
745
802
  tools.push('grep');
746
803
  }
747
- if (this.isToolEnabled('Glob', true, agentTools)) {
804
+ if (aggregateReadsAllowed && this.isToolEnabled('Glob', true, agentTools)) {
748
805
  tools.push('find');
749
806
  tools.push('ls');
750
807
  }
751
- if (tools.length === 0) {
752
- tools.push('read', 'bash');
808
+ return tools;
809
+ }
810
+ resolvePermissionTools(workingDir, agentTools) {
811
+ const filesystemPermissions = this.runtimeConfig.permissions?.filesystem;
812
+ if (!filesystemPermissions)
813
+ return [];
814
+ const authorizer = new AgentFilesystemAuthorizer(workingDir, filesystemPermissions);
815
+ const tools = [];
816
+ if (filesystemPermissions.read && this.isToolEnabled('Read', true, agentTools)) {
817
+ const readTool = createReadToolDefinition(workingDir, {
818
+ operations: {
819
+ access: async (filePath) => {
820
+ await authorizer.authorize('read', filePath);
821
+ await access(filePath, constants.R_OK);
822
+ },
823
+ readFile: async (filePath) => {
824
+ await authorizer.authorize('read', filePath);
825
+ return readFsFile(filePath);
826
+ },
827
+ },
828
+ });
829
+ const executeRead = readTool.execute.bind(readTool);
830
+ readTool.execute = async (toolCallId, params, signal, onUpdate, context) => {
831
+ await authorizer.authorize('read', resolvePiToolPath(workingDir, params.path));
832
+ return executeRead(toolCallId, params, signal, onUpdate, context);
833
+ };
834
+ tools.push(readTool);
835
+ }
836
+ if (filesystemPermissions.read && allowsAggregateReadTools(filesystemPermissions.read)) {
837
+ if (this.isToolEnabled('Grep', true, agentTools)) {
838
+ const grepTool = createGrepToolDefinition(workingDir);
839
+ const executeGrep = grepTool.execute.bind(grepTool);
840
+ grepTool.execute = async (toolCallId, params, signal, onUpdate, context) => {
841
+ await authorizer.authorize('read', resolvePiToolPath(workingDir, params.path ?? '.'));
842
+ return executeGrep(toolCallId, params, signal, onUpdate, context);
843
+ };
844
+ tools.push(grepTool);
845
+ }
846
+ if (this.isToolEnabled('Glob', true, agentTools)) {
847
+ const findTool = createFindToolDefinition(workingDir);
848
+ const executeFind = findTool.execute.bind(findTool);
849
+ findTool.execute = async (toolCallId, params, signal, onUpdate, context) => {
850
+ if (isAbsolute(params.pattern) ||
851
+ params.pattern.replaceAll('\\', '/').split('/').includes('..')) {
852
+ throw new Error(`Agent find pattern must stay within its search root: ${params.pattern}`);
853
+ }
854
+ await authorizer.authorize('read', resolvePiToolPath(workingDir, params.path ?? '.'));
855
+ return executeFind(toolCallId, params, signal, onUpdate, context);
856
+ };
857
+ tools.push(findTool);
858
+ const lsTool = createLsToolDefinition(workingDir);
859
+ const executeLs = lsTool.execute.bind(lsTool);
860
+ lsTool.execute = async (toolCallId, params, signal, onUpdate, context) => {
861
+ await authorizer.authorize('read', resolvePiToolPath(workingDir, params.path ?? '.'));
862
+ return executeLs(toolCallId, params, signal, onUpdate, context);
863
+ };
864
+ tools.push(lsTool);
865
+ }
866
+ }
867
+ if (filesystemPermissions.write && this.isToolEnabled('Write', false, agentTools)) {
868
+ const writeTool = createWriteToolDefinition(workingDir, {
869
+ operations: {
870
+ mkdir: async (directory) => mkdir(directory, { recursive: true }).then(() => undefined),
871
+ writeFile: async (filePath, content) => {
872
+ await authorizer.authorize('write', filePath);
873
+ this.validateProposedWrite(workingDir, filePath, content);
874
+ await writeFsFile(filePath, content, 'utf-8');
875
+ },
876
+ },
877
+ });
878
+ const executeWrite = writeTool.execute.bind(writeTool);
879
+ writeTool.execute = async (toolCallId, params, signal, onUpdate, context) => {
880
+ const filePath = await authorizer.authorize('write', resolvePiToolPath(workingDir, params.path));
881
+ this.validateProposedWrite(workingDir, filePath, params.content);
882
+ const result = await executeWrite(toolCallId, params, signal, onUpdate, context);
883
+ return this.appendAfterMutationValidation(workingDir, filePath, result);
884
+ };
885
+ tools.push(writeTool);
886
+ }
887
+ if (filesystemPermissions.write && this.isToolEnabled('Edit', false, agentTools)) {
888
+ const editTool = createEditToolDefinition(workingDir, {
889
+ operations: {
890
+ access: async (filePath) => {
891
+ if (filesystemPermissions.read)
892
+ await authorizer.authorize('read', filePath);
893
+ await access(filePath, constants.R_OK | constants.W_OK);
894
+ },
895
+ readFile: async (filePath) => {
896
+ if (filesystemPermissions.read)
897
+ await authorizer.authorize('read', filePath);
898
+ return readFsFile(filePath);
899
+ },
900
+ writeFile: async (filePath, content) => {
901
+ await authorizer.authorize('write', filePath);
902
+ this.validateProposedWrite(workingDir, filePath, content);
903
+ await writeFsFile(filePath, content, 'utf-8');
904
+ },
905
+ },
906
+ });
907
+ const executeEdit = editTool.execute.bind(editTool);
908
+ editTool.execute = async (toolCallId, params, signal, onUpdate, context) => {
909
+ const resolvedPath = resolvePiToolPath(workingDir, params.path);
910
+ const filePath = await authorizer.authorize('write', resolvedPath);
911
+ if (filesystemPermissions.read)
912
+ await authorizer.authorize('read', resolvedPath);
913
+ const result = await executeEdit(toolCallId, params, signal, onUpdate, context);
914
+ return this.appendAfterMutationValidation(workingDir, filePath, result);
915
+ };
916
+ tools.push(editTool);
917
+ }
918
+ if (filesystemPermissions.delete && this.isToolEnabled('delete_file', false, agentTools)) {
919
+ tools.push({
920
+ name: 'delete_file',
921
+ label: 'delete_file',
922
+ description: 'Delete one file permitted by the current workflow policy.',
923
+ parameters: Type.Object({ path: Type.String({ minLength: 1 }) }),
924
+ execute: async (_toolCallId, params) => {
925
+ const filePath = await authorizer.authorize('delete', params.path);
926
+ await unlink(filePath);
927
+ const result = {
928
+ content: [{ type: 'text', text: `Deleted ${params.path}` }],
929
+ details: { path: params.path },
930
+ };
931
+ return this.appendAfterMutationValidation(workingDir, filePath, result);
932
+ },
933
+ });
753
934
  }
754
935
  return tools;
755
936
  }
937
+ validateProposedWrite(workingDir, filePath, content) {
938
+ for (const validator of this.runtimeConfig.validation?.afterMutation ?? []) {
939
+ const root = resolveWithinWorkingDir(workingDir, validator.root, 'access');
940
+ const relativePath = relative(root, filePath);
941
+ if (relativePath === '' ||
942
+ relativePath === '..' ||
943
+ relativePath.startsWith(`..${sep}`) ||
944
+ isAbsolute(relativePath))
945
+ continue;
946
+ if (validator.name === 'okf-document') {
947
+ const validation = validateOkfDocument(content, toPosixPath(relativePath));
948
+ if (!validation.valid) {
949
+ const errors = validation.errors
950
+ .map((issue) => `- ${issue.path ?? relativePath}: [${issue.code}] ${issue.message}`)
951
+ .join('\n');
952
+ throw new Error(`Proposed write failed ${validator.name} validation:\n${errors}`);
953
+ }
954
+ }
955
+ }
956
+ }
957
+ async appendAfterMutationValidation(workingDir, filePath, result) {
958
+ const feedback = [];
959
+ for (const validator of this.runtimeConfig.validation?.afterMutation ?? []) {
960
+ const root = resolveWithinWorkingDir(workingDir, validator.root, 'access');
961
+ const relativePath = relative(root, filePath);
962
+ if (relativePath === '' ||
963
+ relativePath === '..' ||
964
+ relativePath.startsWith(`..${sep}`) ||
965
+ isAbsolute(relativePath))
966
+ continue;
967
+ if (validator.name === 'okf-document') {
968
+ const validation = await validateOkfBundle(workingDir, validator.root);
969
+ feedback.push(validation.valid
970
+ ? `OKF bundle validation passed with ${validation.warnings.length} warning(s).`
971
+ : `OKF bundle validation found ${validation.errors.length} error(s):\n${formatOkfValidationErrors(validation)}`);
972
+ }
973
+ }
974
+ if (feedback.length === 0)
975
+ return result;
976
+ return {
977
+ ...result,
978
+ content: [...result.content, { type: 'text', text: feedback.join('\n') }],
979
+ };
980
+ }
756
981
  resolveSkillSearchPaths(cwd) {
757
982
  const configuredPaths = this.runtimeConfig.skillSearchPaths ?? [];
758
983
  if (configuredPaths.length === 0) {
759
- return [join(cwd, '.drs', 'skills'), join(cwd, '.pi', 'skills')];
984
+ return [
985
+ join(cwd, '.agents', 'skills'),
986
+ join(cwd, '.drs', 'skills'),
987
+ join(cwd, '.pi', 'skills'),
988
+ ];
760
989
  }
761
990
  return configuredPaths.map((skillPath) => normalizeSkillPath(cwd, skillPath));
762
991
  }
@@ -765,18 +994,30 @@ class PiSessionRuntime {
765
994
  }
766
995
  resolveCustomTools(workingDir, agentTools) {
767
996
  const customTools = [];
768
- if (this.isToolEnabled('write_json_output', true, agentTools)) {
997
+ const filesystemPermissions = this.runtimeConfig.permissions?.filesystem;
998
+ const authorizer = filesystemPermissions
999
+ ? new AgentFilesystemAuthorizer(workingDir, filesystemPermissions)
1000
+ : undefined;
1001
+ if ((!filesystemPermissions || filesystemPermissions.write !== undefined) &&
1002
+ this.isToolEnabled('write_json_output', true, agentTools)) {
769
1003
  customTools.push({
770
1004
  name: 'write_json_output',
771
1005
  label: 'write_json_output',
772
1006
  description: 'Validate and write structured JSON output for DRS agents.',
773
1007
  parameters: Type.Object({
774
- outputType: Type.Union([Type.Literal('describe_output'), Type.Literal('review_output')]),
1008
+ outputType: Type.Literal('describe_output'),
775
1009
  payload: Type.Any(),
776
1010
  pretty: Type.Optional(Type.Boolean()),
777
1011
  indent: Type.Optional(Type.Number({ minimum: 2, maximum: 8 })),
778
1012
  }),
779
1013
  execute: async (_toolCallId, params) => {
1014
+ const outputPath = OUTPUT_PATHS[params.outputType];
1015
+ const filePath = authorizer
1016
+ ? await authorizer.authorize('write', outputPath)
1017
+ : resolveWithinWorkingDir(workingDir, outputPath, 'write');
1018
+ const jsonValue = typeof params.payload === 'string' ? JSON.parse(params.payload) : params.payload;
1019
+ const spacing = params.pretty === false ? undefined : (params.indent ?? 2);
1020
+ this.validateProposedWrite(workingDir, filePath, JSON.stringify(jsonValue, null, spacing));
780
1021
  const pointer = await writeJsonOutput({
781
1022
  outputType: params.outputType,
782
1023
  payload: params.payload,
@@ -784,14 +1025,16 @@ class PiSessionRuntime {
784
1025
  indent: params.indent,
785
1026
  workingDir,
786
1027
  });
787
- return {
1028
+ const result = {
788
1029
  content: [{ type: 'text', text: JSON.stringify(pointer) }],
789
1030
  details: pointer,
790
1031
  };
1032
+ return this.appendAfterMutationValidation(workingDir, filePath, result);
791
1033
  },
792
1034
  });
793
1035
  }
794
- if (this.isToolEnabled('write_artifact_output', false, agentTools)) {
1036
+ if ((!filesystemPermissions || filesystemPermissions.write !== undefined) &&
1037
+ this.isToolEnabled('write_artifact_output', false, agentTools)) {
795
1038
  customTools.push({
796
1039
  name: 'write_artifact_output',
797
1040
  label: 'write_artifact_output',
@@ -801,19 +1044,26 @@ class PiSessionRuntime {
801
1044
  content: Type.String({ minLength: 1 }),
802
1045
  }),
803
1046
  execute: async (_toolCallId, params) => {
1047
+ const filePath = authorizer
1048
+ ? await authorizer.authorize('write', params.outputPath)
1049
+ : resolveWithinWorkingDir(workingDir, params.outputPath, 'write');
1050
+ const html = extractHtmlDocument(params.content);
1051
+ this.validateProposedWrite(workingDir, filePath, html);
804
1052
  const pointer = await writeArtifactOutput({
805
1053
  outputPath: params.outputPath,
806
1054
  content: params.content,
807
1055
  workingDir,
808
1056
  });
809
- return {
1057
+ const result = {
810
1058
  content: [{ type: 'text', text: JSON.stringify(pointer) }],
811
1059
  details: pointer,
812
1060
  };
1061
+ return this.appendAfterMutationValidation(workingDir, filePath, result);
813
1062
  },
814
1063
  });
815
1064
  }
816
- if (this.isToolEnabled('git_diff', false, agentTools)) {
1065
+ if ((!filesystemPermissions?.read || allowsFullRepositoryRead(filesystemPermissions.read)) &&
1066
+ this.isToolEnabled('git_diff', false, agentTools)) {
817
1067
  customTools.push({
818
1068
  name: 'git_diff',
819
1069
  label: 'git_diff',
@@ -883,6 +1133,7 @@ class PiSessionRuntime {
883
1133
  execute: async (_toolCallId, params) => {
884
1134
  const { readFile } = await import('fs/promises');
885
1135
  try {
1136
+ await authorizer?.authorize('read', params.artifactPath);
886
1137
  const fullPath = resolveWithinWorkingDir(workingDir, params.artifactPath, 'read');
887
1138
  const raw = JSON.parse(await readFile(fullPath, 'utf-8'));
888
1139
  if (findingIdParamProvided(params)) {
@@ -932,7 +1183,9 @@ class PiSessionRuntime {
932
1183
  },
933
1184
  });
934
1185
  }
935
- if (this.isToolEnabled('drs_check', false, agentTools) && this.runtimeConfig.fixChecks) {
1186
+ if (this.runtimeConfig.permissions?.shell !== false &&
1187
+ this.isToolEnabled('drs_check', false, agentTools) &&
1188
+ this.runtimeConfig.fixChecks) {
936
1189
  const configuredChecks = this.runtimeConfig.fixChecks;
937
1190
  customTools.push({
938
1191
  name: 'drs_check',
@@ -1004,6 +1257,7 @@ class PiSessionRuntime {
1004
1257
  const resourceLoader = new DefaultResourceLoader({
1005
1258
  cwd,
1006
1259
  agentDir: getAgentDir(),
1260
+ noExtensions: this.runtimeConfig.permissions !== undefined,
1007
1261
  noSkills: true,
1008
1262
  additionalSkillPaths: skillSearchPaths,
1009
1263
  skillsOverride: configuredSkillNames.size > 0
@@ -1038,7 +1292,10 @@ class PiSessionRuntime {
1038
1292
  },
1039
1293
  })
1040
1294
  : undefined;
1041
- const customTools = this.resolveCustomTools(cwd, settings.tools);
1295
+ const customTools = [
1296
+ ...this.resolveCustomTools(cwd, settings.tools),
1297
+ ...this.resolvePermissionTools(cwd, settings.tools),
1298
+ ];
1042
1299
  const tools = [
1043
1300
  ...this.resolveTools(cwd, settings.tools),
1044
1301
  ...customTools.map((tool) => tool.name),