@chainingintention/pi-web-cn 1.202608.7 → 1.202608.9

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.
@@ -1,6 +1,7 @@
1
- import { realpathSync, statSync } from "node:fs";
2
- import { readFile, writeFile } from "node:fs/promises";
1
+ import { constants, realpathSync, statSync } from "node:fs";
2
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, ModelRuntime, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
5
6
  import { projectBrowserMessage } from "../browserMessageProjection.js";
6
7
  import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
@@ -61,6 +62,25 @@ const STARTUP_PHASE_EXTENSIONS = "正在加载会话扩展";
61
62
  const STARTUP_CONCURRENT_CATALOG_REFRESH = "服务商模型列表正在刷新";
62
63
  const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
63
64
  const MAX_PENDING_UNREAD_MUTATIONS = SESSION_UNREAD_LIMIT + 1;
65
+ function auditVisibleAssistantMessage(message) {
66
+ if (!isAuditRecord(message))
67
+ return message;
68
+ const record = message;
69
+ const content = record["content"];
70
+ if (!Array.isArray(content))
71
+ return message;
72
+ return {
73
+ ...record,
74
+ content: content.filter(part => (typeof part !== "object"
75
+ || part === null
76
+ || Array.isArray(part)
77
+ || !isAuditRecord(part)
78
+ || part["type"] !== "thinking")),
79
+ };
80
+ }
81
+ function isAuditRecord(value) {
82
+ return typeof value === "object" && value !== null && !Array.isArray(value);
83
+ }
64
84
  function noop() {
65
85
  // Intentionally empty default unsubscribe callback.
66
86
  }
@@ -153,6 +173,8 @@ function parsePromptStreamingBehavior(value) {
153
173
  throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
154
174
  }
155
175
  const GLOBAL_AGENT_CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
176
+ const MANAGED_RELAY_SKILL_DIRECTORY = join(".pi", "skills", "relay");
177
+ const BUNDLED_RELAY_SKILL_PATH = fileURLToPath(new URL("../../../skills/relay/SKILL.md", import.meta.url));
156
178
  export function filterManagedGlobalContextFiles(cwd, agentDir, base) {
157
179
  return {
158
180
  agentsFiles: base.agentsFiles.filter((file) => {
@@ -168,34 +190,77 @@ export function filterManagedGlobalContextFiles(cwd, agentDir, base) {
168
190
  }
169
191
  export function filterManagedProjectSkills(cwd, base) {
170
192
  const projectRoot = realpathSync(cwd);
193
+ const pathAllowed = (path) => pathInsideRealDirectory(projectRoot, path);
171
194
  return {
172
195
  ...base,
173
- skills: base.skills.filter((skill) => {
174
- try {
175
- return pathInsideOrEqual(projectRoot, realpathSync(skill.filePath));
176
- }
177
- catch {
178
- return false;
179
- }
180
- }),
196
+ skills: base.skills.filter((skill) => pathAllowed(skill.filePath)),
197
+ diagnostics: filterManagedSkillDiagnostics(base.diagnostics, pathAllowed),
181
198
  };
182
199
  }
200
+ export async function ensureManagedRelaySkill(cwd, bundledSkillPath = BUNDLED_RELAY_SKILL_PATH) {
201
+ const projectRoot = realpathSync(cwd);
202
+ const piDirectory = resolve(projectRoot, ".pi");
203
+ const skillsDirectory = join(piDirectory, "skills");
204
+ const skillDirectory = resolve(projectRoot, MANAGED_RELAY_SKILL_DIRECTORY);
205
+ const skillPath = join(skillDirectory, "SKILL.md");
206
+ for (const directory of [piDirectory, skillsDirectory, skillDirectory]) {
207
+ await ensureManagedSkillDirectory(projectRoot, directory);
208
+ }
209
+ try {
210
+ await copyFile(bundledSkillPath, skillPath, constants.COPYFILE_EXCL);
211
+ }
212
+ catch (error) {
213
+ if (!isNodeError(error, "EEXIST"))
214
+ throw error;
215
+ }
216
+ assertManagedSkillPath(projectRoot, skillPath, "file");
217
+ return skillPath;
218
+ }
183
219
  export function filterManagedWorkbenchSkills(cwd, base, receipt) {
184
220
  const projectRoot = realpathSync(cwd);
185
- const allowedDirectories = new Set(receipt.skills.map((skill) => resolve(projectRoot, ".pi", "skills", skill.directory)));
221
+ const allowedDirectories = new Set([
222
+ resolve(projectRoot, MANAGED_RELAY_SKILL_DIRECTORY),
223
+ ...receipt.skills.map((skill) => resolve(projectRoot, ".pi", "skills", skill.directory)),
224
+ ]);
225
+ const pathAllowed = (path) => [...allowedDirectories].some((directory) => pathInsideRealDirectory(directory, path));
186
226
  return {
187
227
  ...base,
188
- skills: base.skills.filter((skill) => {
189
- try {
190
- const path = realpathSync(skill.filePath);
191
- return [...allowedDirectories].some((directory) => pathInsideOrEqual(directory, path));
192
- }
193
- catch {
194
- return false;
195
- }
196
- }),
228
+ skills: base.skills.filter((skill) => pathAllowed(skill.filePath)),
229
+ diagnostics: filterManagedSkillDiagnostics(base.diagnostics, pathAllowed),
197
230
  };
198
231
  }
232
+ function filterManagedSkillDiagnostics(diagnostics, pathAllowed) {
233
+ return diagnostics.filter((diagnostic) => diagnostic.path === undefined || pathAllowed(diagnostic.path));
234
+ }
235
+ function pathInsideRealDirectory(directory, path) {
236
+ try {
237
+ return pathInsideOrEqual(directory, realpathSync(path));
238
+ }
239
+ catch {
240
+ return false;
241
+ }
242
+ }
243
+ async function ensureManagedSkillDirectory(projectRoot, path) {
244
+ try {
245
+ await mkdir(path);
246
+ }
247
+ catch (error) {
248
+ if (!isNodeError(error, "EEXIST"))
249
+ throw error;
250
+ }
251
+ assertManagedSkillPath(projectRoot, path, "directory");
252
+ }
253
+ function assertManagedSkillPath(projectRoot, path, expectedType) {
254
+ const realPath = realpathSync(path);
255
+ const stats = statSync(realPath);
256
+ const hasExpectedType = expectedType === "directory" ? stats.isDirectory() : stats.isFile();
257
+ if (!pathInsideOrEqual(projectRoot, realPath) || !hasExpectedType) {
258
+ throw new Error(`Managed skill path is invalid: ${path}`);
259
+ }
260
+ }
261
+ function isNodeError(error, code) {
262
+ return error instanceof Error && "code" in error && error.code === code;
263
+ }
199
264
  function pathInsideOrEqual(parent, child) {
200
265
  const normalizedParent = resolve(parent);
201
266
  const normalizedChild = resolve(child);
@@ -371,6 +436,7 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
371
436
  }
372
437
  function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd) {
373
438
  return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled = true }) => {
439
+ await ensureManagedRelaySkill(cwd);
374
440
  const auditIdentity = await resolveManagementAuditIdentity(cwd, managementContext, managementProjectIdForCwd, logger);
375
441
  const accessHandle = workbenchAccessHandle(managementContext);
376
442
  const state = workbench === undefined ? undefined : workbench.accessStates.require(accessHandle);
@@ -402,8 +468,23 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
402
468
  cwd,
403
469
  },
404
470
  });
405
- const workbenchToolNames = workbenchTools.map((tool) => tool.name);
406
- const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext, workbenchToolNames);
471
+ const scopedSpawn = spawn === undefined
472
+ ? undefined
473
+ : (input) => spawn({ ...input, managementContext });
474
+ const scopedSubsessions = subsessions === undefined
475
+ ? undefined
476
+ : {
477
+ ...subsessions,
478
+ spawn: (input) => subsessions.spawn({ ...input, managementContext }),
479
+ };
480
+ const controlledManagementTools = [
481
+ ...(delegationToolsEnabled && scopedSpawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn: scopedSpawn }))] : []),
482
+ ...(delegationToolsEnabled && scopedSubsessions !== undefined ? createSubsessionToolDefinitions(cwd, scopedSubsessions).map((tool) => defineTool(tool)) : []),
483
+ ...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
484
+ ...workbenchTools,
485
+ ];
486
+ const controlledManagementToolNames = controlledManagementTools.map((tool) => tool.name);
487
+ const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext, controlledManagementToolNames);
407
488
  return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
408
489
  const settingsManager = await createScopedSettingsManager({
409
490
  cwd,
@@ -421,29 +502,17 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
421
502
  },
422
503
  });
423
504
  const managedToolOptions = createManagedAgentToolOptions(cwd);
424
- const scopedSpawn = spawn === undefined
425
- ? undefined
426
- : (input) => spawn({ ...input, managementContext });
427
- const scopedSubsessions = subsessions === undefined
428
- ? undefined
429
- : {
430
- ...subsessions,
431
- spawn: (input) => subsessions.spawn({ ...input, managementContext }),
432
- };
433
505
  const customTools = [
434
506
  ...managedAgentToolDefinitions(cwd, managedToolOptions),
435
507
  createManagedPythonToolDefinition(cwd, managementContext),
436
- ...(delegationToolsEnabled && scopedSpawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn: scopedSpawn }))] : []),
437
- ...(delegationToolsEnabled && scopedSubsessions !== undefined ? createSubsessionToolDefinitions(cwd, scopedSubsessions).map((tool) => defineTool(tool)) : []),
438
- ...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
439
- ...workbenchTools,
508
+ ...controlledManagementTools,
440
509
  ];
441
510
  const options = {
442
511
  services,
443
512
  sessionManager,
444
513
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
445
514
  customTools,
446
- tools: managementAgentToolNames(managementContext, workbenchToolNames),
515
+ tools: managementAgentToolNames(managementContext, controlledManagementToolNames),
447
516
  ...(initialModel === undefined ? {} : { model: initialModel }),
448
517
  ...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
449
518
  };
@@ -1654,6 +1723,7 @@ export class PiSessionService {
1654
1723
  void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
1655
1724
  }
1656
1725
  submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
1726
+ this.auditUserPrompt(session, text);
1657
1727
  this.publishActivity(session, behavior === "steer" ? "插队消息已排队" : behavior === "followUp" ? "消息已排队" : "消息已接收", "active");
1658
1728
  const eventScope = this.eventScopeForSession(session);
1659
1729
  if (behavior === undefined && echoUserMessage)
@@ -2491,6 +2561,7 @@ export class PiSessionService {
2491
2561
  active.unsubscribe = session.subscribe((event) => {
2492
2562
  this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel), active.eventScope);
2493
2563
  this.auditToolExecutionEvent(active, event);
2564
+ this.auditAssistantResponseEvent(active, event);
2494
2565
  this.publishActivityForEvent(session, event);
2495
2566
  const eventType = getString(event, "type");
2496
2567
  if (eventType === "agent_end")
@@ -2839,7 +2910,12 @@ export class PiSessionService {
2839
2910
  }, "Pi tool execution audit");
2840
2911
  if (managementIdentity !== undefined) {
2841
2912
  try {
2842
- this.managementAudit?.record({ action: "tool_execution", ...managementIdentity, ...auditEvent });
2913
+ this.managementAudit?.record({
2914
+ action: "tool_execution",
2915
+ ...managementIdentity,
2916
+ ...auditEvent,
2917
+ content: isEnd ? getProperty(event, "result") : getProperty(event, "args"),
2918
+ });
2843
2919
  }
2844
2920
  catch (error) {
2845
2921
  this.logger.info({ sessionId: auditEvent.sessionId, toolCallId: auditEvent.toolCallId, error: error instanceof Error ? error.message : String(error) }, "failed to enqueue management-mode tool audit");
@@ -2859,6 +2935,43 @@ export class PiSessionService {
2859
2935
  }, "failed to persist ordinary-mode tool audit");
2860
2936
  }
2861
2937
  }
2938
+ auditAssistantResponseEvent(active, event) {
2939
+ if (getString(event, "type") !== "message_end")
2940
+ return;
2941
+ const message = getProperty(event, "message");
2942
+ if (getString(message, "role") !== "assistant")
2943
+ return;
2944
+ this.recordManagementContentAudit(active, "assistant_response", auditVisibleAssistantMessage(message));
2945
+ }
2946
+ auditUserPrompt(session, text) {
2947
+ const active = [...new Set(this.active.values())]
2948
+ .find(candidate => candidate.runtime.session === session);
2949
+ if (active === undefined)
2950
+ return;
2951
+ this.recordManagementContentAudit(active, "user_prompt", text);
2952
+ }
2953
+ recordManagementContentAudit(active, action, content) {
2954
+ const identity = active.managementAuditIdentity;
2955
+ if (identity === undefined || this.managementAudit === undefined)
2956
+ return;
2957
+ try {
2958
+ this.managementAudit.record({
2959
+ action,
2960
+ status: "completed",
2961
+ ...identity,
2962
+ sessionId: active.runtime.session.sessionId,
2963
+ cwd: active.runtime.cwd,
2964
+ content,
2965
+ });
2966
+ }
2967
+ catch (error) {
2968
+ this.logger.info({
2969
+ sessionId: active.runtime.session.sessionId,
2970
+ action,
2971
+ error: error instanceof Error ? error.message : String(error),
2972
+ }, "failed to enqueue management-mode content audit");
2973
+ }
2974
+ }
2862
2975
  /**
2863
2976
  * Build the reporter for one session construction.
2864
2977
  *
@@ -3817,6 +3930,11 @@ async function resolveManagementAuditIdentity(cwd, context, resolveProjectId, lo
3817
3930
  logger.info({ userId: context.user.id, cwd, error: error instanceof Error ? error.message : String(error) }, "failed to resolve management audit project; using the authorized default");
3818
3931
  }
3819
3932
  }
3820
- return { userId: context.user.id, rootUserId: context.user.rootUserId, projectId };
3933
+ return {
3934
+ userId: context.user.id,
3935
+ rootUserId: context.user.rootUserId,
3936
+ ...(context.user.displayName === undefined ? {} : { userDisplayName: context.user.displayName }),
3937
+ projectId,
3938
+ };
3821
3939
  }
3822
3940
  //# sourceMappingURL=piSessionService.js.map