@oyasmi/pipiclaw 0.5.5 → 0.5.7

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 (44) hide show
  1. package/README.md +54 -3
  2. package/dist/agent/channel-runner.d.ts +1 -0
  3. package/dist/agent/channel-runner.js +3 -0
  4. package/dist/agent/command-extension.js +6 -6
  5. package/dist/agent/commands.js +1 -1
  6. package/dist/agent/runner-factory.d.ts +2 -0
  7. package/dist/agent/runner-factory.js +6 -0
  8. package/dist/agent/types.d.ts +1 -0
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/memory/lifecycle.d.ts +2 -1
  12. package/dist/memory/lifecycle.js +19 -1
  13. package/dist/models/utils.d.ts +4 -0
  14. package/dist/models/utils.js +15 -0
  15. package/dist/paths.js +1 -1
  16. package/dist/runtime/bootstrap.d.ts +22 -1
  17. package/dist/runtime/bootstrap.js +72 -26
  18. package/dist/security/command-guard.d.ts +16 -0
  19. package/dist/security/command-guard.js +447 -0
  20. package/dist/security/config.d.ts +4 -0
  21. package/dist/security/config.js +82 -0
  22. package/dist/security/logger.d.ts +2 -0
  23. package/dist/security/logger.js +18 -0
  24. package/dist/security/path-guard.d.ts +2 -0
  25. package/dist/security/path-guard.js +237 -0
  26. package/dist/security/types.d.ts +66 -0
  27. package/dist/security/types.js +1 -0
  28. package/dist/subagents/tool.d.ts +2 -0
  29. package/dist/subagents/tool.js +31 -7
  30. package/dist/tools/attach.d.ts +7 -1
  31. package/dist/tools/attach.js +36 -1
  32. package/dist/tools/bash.d.ts +4 -0
  33. package/dist/tools/bash.js +38 -0
  34. package/dist/tools/edit.d.ts +7 -1
  35. package/dist/tools/edit.js +42 -2
  36. package/dist/tools/index.d.ts +7 -1
  37. package/dist/tools/index.js +29 -3
  38. package/dist/tools/read.d.ts +7 -1
  39. package/dist/tools/read.js +36 -1
  40. package/dist/tools/write-content.d.ts +5 -0
  41. package/dist/tools/write-content.js +32 -11
  42. package/dist/tools/write.d.ts +7 -1
  43. package/dist/tools/write.js +10 -3
  44. package/package.json +2 -1
@@ -1,13 +1,38 @@
1
+ import { APP_HOME_DIR } from "../paths.js";
2
+ import { loadSecurityConfig } from "../security/config.js";
1
3
  import { createSubAgentTool } from "../subagents/tool.js";
2
4
  import { createBashTool } from "./bash.js";
3
5
  import { createEditTool } from "./edit.js";
4
6
  import { createReadTool } from "./read.js";
5
7
  import { createWriteTool } from "./write.js";
6
- export function createPipiclawBaseTools(executor) {
7
- return [createReadTool(executor), createBashTool(executor), createEditTool(executor), createWriteTool(executor)];
8
+ export function createPipiclawBaseTools(executor, options = {}) {
9
+ const hasSecurityOptions = options.securityConfig || options.securityContext || options.channelId;
10
+ const toolOptions = hasSecurityOptions
11
+ ? {
12
+ securityConfig: options.securityConfig,
13
+ securityContext: options.securityContext,
14
+ channelId: options.channelId,
15
+ }
16
+ : undefined;
17
+ return [
18
+ createReadTool(executor, toolOptions),
19
+ createBashTool(executor, toolOptions),
20
+ createEditTool(executor, toolOptions),
21
+ createWriteTool(executor, toolOptions),
22
+ ];
8
23
  }
9
24
  export function createPipiclawTools(options) {
10
- const baseTools = createPipiclawBaseTools(options.executor);
25
+ const securityConfig = loadSecurityConfig(APP_HOME_DIR);
26
+ const securityContext = {
27
+ workspaceDir: options.workspaceDir,
28
+ workspacePath: options.workspacePath,
29
+ cwd: process.cwd(),
30
+ };
31
+ const baseTools = createPipiclawBaseTools(options.executor, {
32
+ securityConfig,
33
+ securityContext,
34
+ channelId: options.channelId,
35
+ });
11
36
  return [
12
37
  ...baseTools,
13
38
  createSubAgentTool({
@@ -19,6 +44,7 @@ export function createPipiclawTools(options) {
19
44
  channelDir: options.channelDir,
20
45
  getSubAgentDiscovery: options.getSubAgentDiscovery,
21
46
  getMemoryRecallSettings: options.getMemoryRecallSettings,
47
+ securityConfig,
22
48
  runtimeContext: {
23
49
  workspacePath: options.workspacePath,
24
50
  channelId: options.channelId,
@@ -1,10 +1,16 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
2
  import type { Executor } from "../sandbox.js";
3
+ import type { SecurityConfig, SecurityRuntimeContext } from "../security/types.js";
3
4
  declare const readSchema: import("@sinclair/typebox").TObject<{
4
5
  label: import("@sinclair/typebox").TString;
5
6
  path: import("@sinclair/typebox").TString;
6
7
  offset: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
7
8
  limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
8
9
  }>;
9
- export declare function createReadTool(executor: Executor): AgentTool<typeof readSchema>;
10
+ export interface ReadToolOptions {
11
+ securityConfig?: SecurityConfig;
12
+ securityContext?: SecurityRuntimeContext;
13
+ channelId?: string;
14
+ }
15
+ export declare function createReadTool(executor: Executor, options?: ReadToolOptions): AgentTool<typeof readSchema>;
10
16
  export {};
@@ -1,5 +1,8 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { extname } from "path";
3
+ import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
4
+ import { logSecurityEvent } from "../security/logger.js";
5
+ import { guardPath } from "../security/path-guard.js";
3
6
  import { shellEscape } from "../shared/shell-escape.js";
4
7
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
5
8
  /**
@@ -25,13 +28,45 @@ const readSchema = Type.Object({
25
28
  offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
26
29
  limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
27
30
  });
28
- export function createReadTool(executor) {
31
+ function formatPathBlockMessage(resolvedPath, category, reason) {
32
+ const lines = [`Path blocked${category ? ` [${category}]` : ""}`];
33
+ if (reason) {
34
+ lines.push(`Reason: ${reason}`);
35
+ }
36
+ if (resolvedPath) {
37
+ lines.push(`Resolved path: ${resolvedPath}`);
38
+ }
39
+ return lines.join("\n");
40
+ }
41
+ export function createReadTool(executor, options = {}) {
42
+ const securityConfig = options.securityConfig ?? DEFAULT_SECURITY_CONFIG;
43
+ const securityContext = options.securityContext ?? {
44
+ workspaceDir: process.cwd(),
45
+ workspacePath: process.cwd(),
46
+ cwd: process.cwd(),
47
+ };
29
48
  return {
30
49
  name: "read",
31
50
  label: "read",
32
51
  description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,
33
52
  parameters: readSchema,
34
53
  execute: async (_toolCallId, { path, offset, limit }, signal) => {
54
+ if (securityConfig.enabled && securityConfig.pathGuard.enabled) {
55
+ const guardResult = guardPath(path, "read", { ...securityContext, config: securityConfig.pathGuard });
56
+ if (!guardResult.allowed) {
57
+ logSecurityEvent(securityContext.workspaceDir, securityConfig, {
58
+ type: "path",
59
+ tool: "read",
60
+ channelId: options.channelId,
61
+ rawPath: path,
62
+ operation: "read",
63
+ resolvedPath: guardResult.resolvedPath,
64
+ category: guardResult.category,
65
+ reason: guardResult.reason,
66
+ });
67
+ throw new Error(formatPathBlockMessage(guardResult.resolvedPath, guardResult.category, guardResult.reason));
68
+ }
69
+ }
35
70
  const mimeType = isImageFile(path);
36
71
  if (mimeType) {
37
72
  // Read as image (binary) - use base64
@@ -1,4 +1,9 @@
1
1
  import type { Executor } from "../sandbox.js";
2
+ import type { SecurityConfig, SecurityRuntimeContext } from "../security/types.js";
2
3
  export declare function writeContent(executor: Executor, path: string, content: string, signal: AbortSignal | undefined, options?: {
3
4
  createParentDir?: boolean;
5
+ securityConfig?: SecurityConfig;
6
+ securityContext?: SecurityRuntimeContext;
7
+ channelId?: string;
8
+ toolName?: string;
4
9
  }): Promise<void>;
@@ -1,11 +1,10 @@
1
+ import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
2
+ import { logSecurityEvent } from "../security/logger.js";
3
+ import { guardPath } from "../security/path-guard.js";
1
4
  import { shellEscape } from "../shared/shell-escape.js";
2
- const INLINE_WRITE_MAX_BYTES = 64 * 1024;
3
5
  function getDir(path) {
4
6
  return path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : ".";
5
7
  }
6
- function isInlineSafe(content) {
7
- return Buffer.byteLength(content, "utf-8") <= INLINE_WRITE_MAX_BYTES;
8
- }
9
8
  function ensureSuccess(result, path) {
10
9
  if (result.code !== 0) {
11
10
  throw new Error(result.stderr || `Failed to write file: ${path}`);
@@ -13,14 +12,36 @@ function ensureSuccess(result, path) {
13
12
  }
14
13
  export async function writeContent(executor, path, content, signal, options) {
15
14
  const createParentDir = options?.createParentDir ?? false;
16
- const dirPrefix = createParentDir ? `mkdir -p ${shellEscape(getDir(path))} && ` : "";
17
- if (isInlineSafe(content)) {
18
- const result = await executor.exec(`${dirPrefix}printf '%s' ${shellEscape(content)} > ${shellEscape(path)}`, {
19
- signal,
20
- });
21
- ensureSuccess(result, path);
22
- return;
15
+ const securityConfig = options?.securityConfig ?? DEFAULT_SECURITY_CONFIG;
16
+ const securityContext = options?.securityContext ?? {
17
+ workspaceDir: process.cwd(),
18
+ workspacePath: process.cwd(),
19
+ cwd: process.cwd(),
20
+ };
21
+ if (securityConfig.enabled && securityConfig.pathGuard.enabled) {
22
+ const guardResult = guardPath(path, "write", { ...securityContext, config: securityConfig.pathGuard });
23
+ if (!guardResult.allowed) {
24
+ logSecurityEvent(securityContext.workspaceDir, securityConfig, {
25
+ type: "path",
26
+ tool: options?.toolName ?? "write",
27
+ channelId: options?.channelId,
28
+ rawPath: path,
29
+ operation: "write",
30
+ resolvedPath: guardResult.resolvedPath,
31
+ category: guardResult.category,
32
+ reason: guardResult.reason,
33
+ });
34
+ const lines = [`Path blocked${guardResult.category ? ` [${guardResult.category}]` : ""}`];
35
+ if (guardResult.reason) {
36
+ lines.push(`Reason: ${guardResult.reason}`);
37
+ }
38
+ if (guardResult.resolvedPath) {
39
+ lines.push(`Resolved path: ${guardResult.resolvedPath}`);
40
+ }
41
+ throw new Error(lines.join("\n"));
42
+ }
23
43
  }
44
+ const dirPrefix = createParentDir ? `mkdir -p ${shellEscape(getDir(path))} && ` : "";
24
45
  const result = await executor.exec(`${dirPrefix}cat > ${shellEscape(path)}`, {
25
46
  signal,
26
47
  stdin: content,
@@ -1,9 +1,15 @@
1
1
  import type { AgentTool } from "@mariozechner/pi-agent-core";
2
2
  import type { Executor } from "../sandbox.js";
3
+ import type { SecurityConfig, SecurityRuntimeContext } from "../security/types.js";
3
4
  declare const writeSchema: import("@sinclair/typebox").TObject<{
4
5
  label: import("@sinclair/typebox").TString;
5
6
  path: import("@sinclair/typebox").TString;
6
7
  content: import("@sinclair/typebox").TString;
7
8
  }>;
8
- export declare function createWriteTool(executor: Executor): AgentTool<typeof writeSchema>;
9
+ export interface WriteToolOptions {
10
+ securityConfig?: SecurityConfig;
11
+ securityContext?: SecurityRuntimeContext;
12
+ channelId?: string;
13
+ }
14
+ export declare function createWriteTool(executor: Executor, options?: WriteToolOptions): AgentTool<typeof writeSchema>;
9
15
  export {};
@@ -5,16 +5,23 @@ const writeSchema = Type.Object({
5
5
  path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
6
6
  content: Type.String({ description: "Content to write to the file" }),
7
7
  });
8
- export function createWriteTool(executor) {
8
+ export function createWriteTool(executor, options = {}) {
9
9
  return {
10
10
  name: "write",
11
11
  label: "write",
12
12
  description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
13
13
  parameters: writeSchema,
14
14
  execute: async (_toolCallId, { path, content }, signal) => {
15
- await writeContent(executor, path, content, signal, { createParentDir: true });
15
+ await writeContent(executor, path, content, signal, {
16
+ createParentDir: true,
17
+ securityConfig: options.securityConfig,
18
+ securityContext: options.securityContext,
19
+ channelId: options.channelId,
20
+ toolName: "write",
21
+ });
22
+ const bytesWritten = Buffer.byteLength(content, "utf-8");
16
23
  return {
17
- content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${path}` }],
24
+ content: [{ type: "text", text: `Successfully wrote ${bytesWritten} bytes to ${path}` }],
18
25
  details: undefined,
19
26
  };
20
27
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oyasmi/pipiclaw",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "An AI assistant runtime for coding and team workflows, with DingTalk AI Cards, sub-agents, memory, and scheduled events.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,7 @@
27
27
  "lint": "biome check .",
28
28
  "typecheck": "tsc --noEmit -p tsconfig.json",
29
29
  "test": "vitest --run",
30
+ "test:e2e": "vitest --run --config vitest.config.e2e.ts",
30
31
  "test:coverage": "vitest --run --coverage",
31
32
  "check": "npm run lint && npm run typecheck && npm run test",
32
33
  "prepublishOnly": "npm run clean && npm run build && npm run check"