@aigne/afs 1.4.0-beta.7 → 1.4.0-beta.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0-beta.9](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.8...afs-v1.4.0-beta.9) (2026-01-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * **afs:** add module access control and schema validation support ([#904](https://github.com/AIGNE-io/aigne-framework/issues/904)) ([d0b279a](https://github.com/AIGNE-io/aigne-framework/commit/d0b279aac07ebe2bcc1fd4148498fc3f6bbcd561))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * improve test coverage tracking and reporting ([#903](https://github.com/AIGNE-io/aigne-framework/issues/903)) ([031144e](https://github.com/AIGNE-io/aigne-framework/commit/031144e74f29e882cffe52ffda8f7a18c76ace7f))
14
+
15
+
16
+ ### Dependencies
17
+
18
+ * The following workspace dependencies were updated
19
+ * dependencies
20
+ * @aigne/platform-helpers bumped to 0.6.7-beta.2
21
+
22
+ ## [1.4.0-beta.8](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.7...afs-v1.4.0-beta.8) (2026-01-12)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **afs:** show gitignored files with marker instead of filtering ([c2bdea1](https://github.com/AIGNE-io/aigne-framework/commit/c2bdea155f47c9420f2fe810cdfed79ef70ef899))
28
+
3
29
  ## [1.4.0-beta.7](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.6...afs-v1.4.0-beta.7) (2026-01-08)
4
30
 
5
31
 
package/lib/cjs/afs.d.ts CHANGED
@@ -9,6 +9,11 @@ export declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
9
9
  name: string;
10
10
  constructor(options?: AFSOptions);
11
11
  private modules;
12
+ /**
13
+ * Check if write operations are allowed for the given module.
14
+ * Throws AFSReadonlyError if not allowed.
15
+ */
16
+ private checkWritePermission;
12
17
  mount(module: AFSModule): this;
13
18
  listModules(): Promise<{
14
19
  name: string;
package/lib/cjs/afs.js CHANGED
@@ -6,6 +6,7 @@ const uuid_1 = require("@aigne/uuid");
6
6
  const strict_event_emitter_1 = require("strict-event-emitter");
7
7
  const ufo_1 = require("ufo");
8
8
  const zod_1 = require("zod");
9
+ const error_js_1 = require("./error.js");
9
10
  const type_js_1 = require("./type.js");
10
11
  const DEFAULT_MAX_DEPTH = 1;
11
12
  const MODULES_ROOT_DIR = "/modules";
@@ -20,6 +21,16 @@ class AFS extends strict_event_emitter_1.Emitter {
20
21
  }
21
22
  }
22
23
  modules = new Map();
24
+ /**
25
+ * Check if write operations are allowed for the given module.
26
+ * Throws AFSReadonlyError if not allowed.
27
+ */
28
+ checkWritePermission(module, operation, path) {
29
+ // Module-level readonly (undefined means readonly by default)
30
+ if (module.accessMode !== "readwrite") {
31
+ throw new error_js_1.AFSReadonlyError(`Module '${module.name}' is readonly, cannot perform ${operation} to ${path}`);
32
+ }
33
+ }
23
34
  mount(module) {
24
35
  let path = (0, ufo_1.joinURL)("/", module.name);
25
36
  if (!/^\/[^/]+$/.test(path)) {
@@ -109,6 +120,7 @@ class AFS extends strict_event_emitter_1.Emitter {
109
120
  const module = this.findModules(path, { exactMatch: true })[0];
110
121
  if (!module?.module.write)
111
122
  throw new Error(`No module found for path: ${path}`);
123
+ this.checkWritePermission(module.module, "write", path);
112
124
  const res = await module.module.write(module.subpath, content, options);
113
125
  return {
114
126
  ...res,
@@ -122,6 +134,7 @@ class AFS extends strict_event_emitter_1.Emitter {
122
134
  const module = this.findModules(path, { exactMatch: true })[0];
123
135
  if (!module?.module.delete)
124
136
  throw new Error(`No module found for path: ${path}`);
137
+ this.checkWritePermission(module.module, "delete", path);
125
138
  return await module.module.delete(module.subpath, options);
126
139
  }
127
140
  async rename(oldPath, newPath, options) {
@@ -134,6 +147,7 @@ class AFS extends strict_event_emitter_1.Emitter {
134
147
  if (!oldModule.module.rename) {
135
148
  throw new Error(`Module does not support rename operation: ${oldModule.modulePath}`);
136
149
  }
150
+ this.checkWritePermission(oldModule.module, "rename", oldPath);
137
151
  return await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
138
152
  }
139
153
  async search(path, query, options = {}) {
@@ -281,6 +295,10 @@ class AFS extends strict_event_emitter_1.Emitter {
281
295
  if (entry?.metadata?.childrenTruncated) {
282
296
  metadataParts.push("truncated");
283
297
  }
298
+ // Gitignored
299
+ if (entry?.metadata?.gitignored) {
300
+ metadataParts.push("gitignored");
301
+ }
284
302
  // Executable
285
303
  if (entry?.metadata?.execute) {
286
304
  metadataParts.push("executable");
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Base error class for all AFS errors.
3
+ */
4
+ export declare class AFSError extends Error {
5
+ readonly code: string;
6
+ constructor(message: string, code: string);
7
+ }
8
+ /**
9
+ * Error thrown when attempting write operations on a readonly AFS or module.
10
+ */
11
+ export declare class AFSReadonlyError extends AFSError {
12
+ constructor(message: string);
13
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AFSReadonlyError = exports.AFSError = void 0;
4
+ /**
5
+ * Base error class for all AFS errors.
6
+ */
7
+ class AFSError extends Error {
8
+ code;
9
+ constructor(message, code) {
10
+ super(message);
11
+ this.name = "AFSError";
12
+ this.code = code;
13
+ }
14
+ }
15
+ exports.AFSError = AFSError;
16
+ /**
17
+ * Error thrown when attempting write operations on a readonly AFS or module.
18
+ */
19
+ class AFSReadonlyError extends AFSError {
20
+ constructor(message) {
21
+ super(message, "AFS_READONLY");
22
+ this.name = "AFSReadonlyError";
23
+ }
24
+ }
25
+ exports.AFSReadonlyError = AFSReadonlyError;
@@ -1,2 +1,3 @@
1
1
  export * from "./afs.js";
2
+ export * from "./error.js";
2
3
  export * from "./type.js";
package/lib/cjs/index.js CHANGED
@@ -15,4 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./afs.js"), exports);
18
+ __exportStar(require("./error.js"), exports);
18
19
  __exportStar(require("./type.js"), exports);
package/lib/cjs/type.d.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  import type { Emitter } from "strict-event-emitter";
2
- import { type ZodType } from "zod";
3
- export interface AFSListOptions {
2
+ import { type ZodType, z } from "zod";
3
+ /**
4
+ * Access mode for AFS modules and root.
5
+ * - "readonly": Only read operations are allowed (list, read, search)
6
+ * - "readwrite": All operations are allowed
7
+ */
8
+ export type AFSAccessMode = "readonly" | "readwrite";
9
+ /**
10
+ * Zod schema for access mode validation.
11
+ * Can be reused across modules that support access mode configuration.
12
+ */
13
+ export declare const accessModeSchema: z.ZodOptional<z.ZodEnum<["readonly", "readwrite"]>>;
14
+ export interface AFSOperationOptions {
15
+ context?: any;
16
+ }
17
+ export interface AFSListOptions extends AFSOperationOptions {
4
18
  filter?: {
5
19
  agentId?: string;
6
20
  userId?: string;
@@ -23,47 +37,40 @@ export interface AFSListOptions {
23
37
  * Examples: "*.ts", "**\/*.js", "src/**\/*.{ts,tsx}"
24
38
  */
25
39
  pattern?: string;
26
- context?: any;
27
40
  }
28
41
  export interface AFSListResult {
29
42
  data: AFSEntry[];
30
43
  message?: string;
31
- context?: any;
32
44
  }
33
- export interface AFSSearchOptions {
45
+ export interface AFSSearchOptions extends AFSOperationOptions {
34
46
  limit?: number;
35
47
  caseSensitive?: boolean;
36
- context?: any;
37
48
  }
38
49
  export interface AFSSearchResult {
39
50
  data: AFSEntry[];
40
51
  message?: string;
41
52
  }
42
- export interface AFSReadOptions {
53
+ export interface AFSReadOptions extends AFSOperationOptions {
43
54
  filter?: AFSListOptions["filter"];
44
- context?: any;
45
55
  }
46
56
  export interface AFSReadResult {
47
57
  data?: AFSEntry;
48
58
  message?: string;
49
59
  }
50
- export interface AFSDeleteOptions {
60
+ export interface AFSDeleteOptions extends AFSOperationOptions {
51
61
  recursive?: boolean;
52
- context?: any;
53
62
  }
54
63
  export interface AFSDeleteResult {
55
64
  message?: string;
56
65
  }
57
- export interface AFSRenameOptions {
66
+ export interface AFSRenameOptions extends AFSOperationOptions {
58
67
  overwrite?: boolean;
59
- context?: any;
60
68
  }
61
69
  export interface AFSRenameResult {
62
70
  message?: string;
63
71
  }
64
- export interface AFSWriteOptions {
72
+ export interface AFSWriteOptions extends AFSOperationOptions {
65
73
  append?: boolean;
66
- context?: any;
67
74
  }
68
75
  export interface AFSWriteResult {
69
76
  data: AFSEntry;
@@ -72,8 +79,7 @@ export interface AFSWriteResult {
72
79
  }
73
80
  export interface AFSWriteEntryPayload extends Omit<AFSEntry, "id" | "path"> {
74
81
  }
75
- export interface AFSExecOptions {
76
- context: any;
82
+ export interface AFSExecOptions extends AFSOperationOptions {
77
83
  }
78
84
  export interface AFSExecResult {
79
85
  data: Record<string, any>;
@@ -81,6 +87,19 @@ export interface AFSExecResult {
81
87
  export interface AFSModule {
82
88
  readonly name: string;
83
89
  readonly description?: string;
90
+ /**
91
+ * Access mode for this module.
92
+ * - "readonly": Only read operations are allowed
93
+ * - "readwrite": All operations are allowed
94
+ * Default behavior is implementation-specific.
95
+ */
96
+ readonly accessMode?: AFSAccessMode;
97
+ /**
98
+ * Enable automatic agent skill scanning for this module.
99
+ * When set to true, the system will scan this module for agent skills.
100
+ * @default false
101
+ */
102
+ readonly agentSkills?: boolean;
84
103
  onMount?(root: AFSRoot): void;
85
104
  symlinkToPhysical?(path: string): Promise<void>;
86
105
  list?(path: string, options?: AFSListOptions): Promise<AFSListResult>;
@@ -91,6 +110,39 @@ export interface AFSModule {
91
110
  search?(path: string, query: string, options?: AFSSearchOptions): Promise<AFSSearchResult>;
92
111
  exec?(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
93
112
  }
113
+ /**
114
+ * Parameters for loading a module from configuration.
115
+ */
116
+ export interface AFSModuleLoadParams {
117
+ /** Path to the configuration file */
118
+ filepath: string;
119
+ /** Parsed configuration object */
120
+ parsed?: object;
121
+ }
122
+ /**
123
+ * Interface for module classes that support schema validation and loading from configuration.
124
+ * This describes the static part of a module class.
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * class MyModule implements AFSModule {
129
+ * static schema() { return mySchema; }
130
+ * static async load(params: AFSModuleLoadParams) { ... }
131
+ * // ...
132
+ * }
133
+ *
134
+ * // Type check
135
+ * const _check: AFSModuleClass<MyModule, MyModuleOptions> = MyModule;
136
+ * ```
137
+ */
138
+ export interface AFSModuleClass<T extends AFSModule = AFSModule, O extends object = object> {
139
+ /** Returns the Zod schema for validating module configuration */
140
+ schema(): ZodType<O>;
141
+ /** Loads a module instance from configuration file path and parsed config */
142
+ load(params: AFSModuleLoadParams): Promise<T>;
143
+ /** Constructor */
144
+ new (options: O): T;
145
+ }
94
146
  export type AFSRootEvents = {
95
147
  agentSucceed: [
96
148
  {
@@ -104,7 +156,7 @@ export type AFSRootEvents = {
104
156
  ];
105
157
  historyCreated: [{
106
158
  entry: AFSEntry;
107
- }];
159
+ }, options: AFSOperationOptions];
108
160
  };
109
161
  export interface AFSRootListOptions extends AFSListOptions, AFSContextPreset {
110
162
  preset?: string;
@@ -133,6 +185,7 @@ export interface AFSEntryMetadata extends Record<string, any> {
133
185
  };
134
186
  childrenCount?: number;
135
187
  childrenTruncated?: boolean;
188
+ gitignored?: boolean;
136
189
  }
137
190
  export interface AFSEntry<T = any> {
138
191
  id: string;
package/lib/cjs/type.js CHANGED
@@ -1,7 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.afsEntrySchema = void 0;
3
+ exports.afsEntrySchema = exports.accessModeSchema = void 0;
4
4
  const zod_1 = require("zod");
5
+ /**
6
+ * Zod schema for access mode validation.
7
+ * Can be reused across modules that support access mode configuration.
8
+ */
9
+ exports.accessModeSchema = zod_1.z
10
+ .enum(["readonly", "readwrite"])
11
+ .describe("Access mode for this module")
12
+ .optional();
5
13
  exports.afsEntrySchema = zod_1.z.object({
6
14
  id: zod_1.z.string(),
7
15
  createdAt: zod_1.z.date().optional(),
package/lib/dts/afs.d.ts CHANGED
@@ -9,6 +9,11 @@ export declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
9
9
  name: string;
10
10
  constructor(options?: AFSOptions);
11
11
  private modules;
12
+ /**
13
+ * Check if write operations are allowed for the given module.
14
+ * Throws AFSReadonlyError if not allowed.
15
+ */
16
+ private checkWritePermission;
12
17
  mount(module: AFSModule): this;
13
18
  listModules(): Promise<{
14
19
  name: string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Base error class for all AFS errors.
3
+ */
4
+ export declare class AFSError extends Error {
5
+ readonly code: string;
6
+ constructor(message: string, code: string);
7
+ }
8
+ /**
9
+ * Error thrown when attempting write operations on a readonly AFS or module.
10
+ */
11
+ export declare class AFSReadonlyError extends AFSError {
12
+ constructor(message: string);
13
+ }
@@ -1,2 +1,3 @@
1
1
  export * from "./afs.js";
2
+ export * from "./error.js";
2
3
  export * from "./type.js";
package/lib/dts/type.d.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  import type { Emitter } from "strict-event-emitter";
2
- import { type ZodType } from "zod";
3
- export interface AFSListOptions {
2
+ import { type ZodType, z } from "zod";
3
+ /**
4
+ * Access mode for AFS modules and root.
5
+ * - "readonly": Only read operations are allowed (list, read, search)
6
+ * - "readwrite": All operations are allowed
7
+ */
8
+ export type AFSAccessMode = "readonly" | "readwrite";
9
+ /**
10
+ * Zod schema for access mode validation.
11
+ * Can be reused across modules that support access mode configuration.
12
+ */
13
+ export declare const accessModeSchema: z.ZodOptional<z.ZodEnum<["readonly", "readwrite"]>>;
14
+ export interface AFSOperationOptions {
15
+ context?: any;
16
+ }
17
+ export interface AFSListOptions extends AFSOperationOptions {
4
18
  filter?: {
5
19
  agentId?: string;
6
20
  userId?: string;
@@ -23,47 +37,40 @@ export interface AFSListOptions {
23
37
  * Examples: "*.ts", "**\/*.js", "src/**\/*.{ts,tsx}"
24
38
  */
25
39
  pattern?: string;
26
- context?: any;
27
40
  }
28
41
  export interface AFSListResult {
29
42
  data: AFSEntry[];
30
43
  message?: string;
31
- context?: any;
32
44
  }
33
- export interface AFSSearchOptions {
45
+ export interface AFSSearchOptions extends AFSOperationOptions {
34
46
  limit?: number;
35
47
  caseSensitive?: boolean;
36
- context?: any;
37
48
  }
38
49
  export interface AFSSearchResult {
39
50
  data: AFSEntry[];
40
51
  message?: string;
41
52
  }
42
- export interface AFSReadOptions {
53
+ export interface AFSReadOptions extends AFSOperationOptions {
43
54
  filter?: AFSListOptions["filter"];
44
- context?: any;
45
55
  }
46
56
  export interface AFSReadResult {
47
57
  data?: AFSEntry;
48
58
  message?: string;
49
59
  }
50
- export interface AFSDeleteOptions {
60
+ export interface AFSDeleteOptions extends AFSOperationOptions {
51
61
  recursive?: boolean;
52
- context?: any;
53
62
  }
54
63
  export interface AFSDeleteResult {
55
64
  message?: string;
56
65
  }
57
- export interface AFSRenameOptions {
66
+ export interface AFSRenameOptions extends AFSOperationOptions {
58
67
  overwrite?: boolean;
59
- context?: any;
60
68
  }
61
69
  export interface AFSRenameResult {
62
70
  message?: string;
63
71
  }
64
- export interface AFSWriteOptions {
72
+ export interface AFSWriteOptions extends AFSOperationOptions {
65
73
  append?: boolean;
66
- context?: any;
67
74
  }
68
75
  export interface AFSWriteResult {
69
76
  data: AFSEntry;
@@ -72,8 +79,7 @@ export interface AFSWriteResult {
72
79
  }
73
80
  export interface AFSWriteEntryPayload extends Omit<AFSEntry, "id" | "path"> {
74
81
  }
75
- export interface AFSExecOptions {
76
- context: any;
82
+ export interface AFSExecOptions extends AFSOperationOptions {
77
83
  }
78
84
  export interface AFSExecResult {
79
85
  data: Record<string, any>;
@@ -81,6 +87,19 @@ export interface AFSExecResult {
81
87
  export interface AFSModule {
82
88
  readonly name: string;
83
89
  readonly description?: string;
90
+ /**
91
+ * Access mode for this module.
92
+ * - "readonly": Only read operations are allowed
93
+ * - "readwrite": All operations are allowed
94
+ * Default behavior is implementation-specific.
95
+ */
96
+ readonly accessMode?: AFSAccessMode;
97
+ /**
98
+ * Enable automatic agent skill scanning for this module.
99
+ * When set to true, the system will scan this module for agent skills.
100
+ * @default false
101
+ */
102
+ readonly agentSkills?: boolean;
84
103
  onMount?(root: AFSRoot): void;
85
104
  symlinkToPhysical?(path: string): Promise<void>;
86
105
  list?(path: string, options?: AFSListOptions): Promise<AFSListResult>;
@@ -91,6 +110,39 @@ export interface AFSModule {
91
110
  search?(path: string, query: string, options?: AFSSearchOptions): Promise<AFSSearchResult>;
92
111
  exec?(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
93
112
  }
113
+ /**
114
+ * Parameters for loading a module from configuration.
115
+ */
116
+ export interface AFSModuleLoadParams {
117
+ /** Path to the configuration file */
118
+ filepath: string;
119
+ /** Parsed configuration object */
120
+ parsed?: object;
121
+ }
122
+ /**
123
+ * Interface for module classes that support schema validation and loading from configuration.
124
+ * This describes the static part of a module class.
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * class MyModule implements AFSModule {
129
+ * static schema() { return mySchema; }
130
+ * static async load(params: AFSModuleLoadParams) { ... }
131
+ * // ...
132
+ * }
133
+ *
134
+ * // Type check
135
+ * const _check: AFSModuleClass<MyModule, MyModuleOptions> = MyModule;
136
+ * ```
137
+ */
138
+ export interface AFSModuleClass<T extends AFSModule = AFSModule, O extends object = object> {
139
+ /** Returns the Zod schema for validating module configuration */
140
+ schema(): ZodType<O>;
141
+ /** Loads a module instance from configuration file path and parsed config */
142
+ load(params: AFSModuleLoadParams): Promise<T>;
143
+ /** Constructor */
144
+ new (options: O): T;
145
+ }
94
146
  export type AFSRootEvents = {
95
147
  agentSucceed: [
96
148
  {
@@ -104,7 +156,7 @@ export type AFSRootEvents = {
104
156
  ];
105
157
  historyCreated: [{
106
158
  entry: AFSEntry;
107
- }];
159
+ }, options: AFSOperationOptions];
108
160
  };
109
161
  export interface AFSRootListOptions extends AFSListOptions, AFSContextPreset {
110
162
  preset?: string;
@@ -133,6 +185,7 @@ export interface AFSEntryMetadata extends Record<string, any> {
133
185
  };
134
186
  childrenCount?: number;
135
187
  childrenTruncated?: boolean;
188
+ gitignored?: boolean;
136
189
  }
137
190
  export interface AFSEntry<T = any> {
138
191
  id: string;
package/lib/esm/afs.d.ts CHANGED
@@ -9,6 +9,11 @@ export declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
9
9
  name: string;
10
10
  constructor(options?: AFSOptions);
11
11
  private modules;
12
+ /**
13
+ * Check if write operations are allowed for the given module.
14
+ * Throws AFSReadonlyError if not allowed.
15
+ */
16
+ private checkWritePermission;
12
17
  mount(module: AFSModule): this;
13
18
  listModules(): Promise<{
14
19
  name: string;
package/lib/esm/afs.js CHANGED
@@ -3,6 +3,7 @@ import { v7 } from "@aigne/uuid";
3
3
  import { Emitter } from "strict-event-emitter";
4
4
  import { joinURL } from "ufo";
5
5
  import { z } from "zod";
6
+ import { AFSReadonlyError } from "./error.js";
6
7
  import { afsEntrySchema, } from "./type.js";
7
8
  const DEFAULT_MAX_DEPTH = 1;
8
9
  const MODULES_ROOT_DIR = "/modules";
@@ -17,6 +18,16 @@ export class AFS extends Emitter {
17
18
  }
18
19
  }
19
20
  modules = new Map();
21
+ /**
22
+ * Check if write operations are allowed for the given module.
23
+ * Throws AFSReadonlyError if not allowed.
24
+ */
25
+ checkWritePermission(module, operation, path) {
26
+ // Module-level readonly (undefined means readonly by default)
27
+ if (module.accessMode !== "readwrite") {
28
+ throw new AFSReadonlyError(`Module '${module.name}' is readonly, cannot perform ${operation} to ${path}`);
29
+ }
30
+ }
20
31
  mount(module) {
21
32
  let path = joinURL("/", module.name);
22
33
  if (!/^\/[^/]+$/.test(path)) {
@@ -106,6 +117,7 @@ export class AFS extends Emitter {
106
117
  const module = this.findModules(path, { exactMatch: true })[0];
107
118
  if (!module?.module.write)
108
119
  throw new Error(`No module found for path: ${path}`);
120
+ this.checkWritePermission(module.module, "write", path);
109
121
  const res = await module.module.write(module.subpath, content, options);
110
122
  return {
111
123
  ...res,
@@ -119,6 +131,7 @@ export class AFS extends Emitter {
119
131
  const module = this.findModules(path, { exactMatch: true })[0];
120
132
  if (!module?.module.delete)
121
133
  throw new Error(`No module found for path: ${path}`);
134
+ this.checkWritePermission(module.module, "delete", path);
122
135
  return await module.module.delete(module.subpath, options);
123
136
  }
124
137
  async rename(oldPath, newPath, options) {
@@ -131,6 +144,7 @@ export class AFS extends Emitter {
131
144
  if (!oldModule.module.rename) {
132
145
  throw new Error(`Module does not support rename operation: ${oldModule.modulePath}`);
133
146
  }
147
+ this.checkWritePermission(oldModule.module, "rename", oldPath);
134
148
  return await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
135
149
  }
136
150
  async search(path, query, options = {}) {
@@ -278,6 +292,10 @@ export class AFS extends Emitter {
278
292
  if (entry?.metadata?.childrenTruncated) {
279
293
  metadataParts.push("truncated");
280
294
  }
295
+ // Gitignored
296
+ if (entry?.metadata?.gitignored) {
297
+ metadataParts.push("gitignored");
298
+ }
281
299
  // Executable
282
300
  if (entry?.metadata?.execute) {
283
301
  metadataParts.push("executable");
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Base error class for all AFS errors.
3
+ */
4
+ export declare class AFSError extends Error {
5
+ readonly code: string;
6
+ constructor(message: string, code: string);
7
+ }
8
+ /**
9
+ * Error thrown when attempting write operations on a readonly AFS or module.
10
+ */
11
+ export declare class AFSReadonlyError extends AFSError {
12
+ constructor(message: string);
13
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Base error class for all AFS errors.
3
+ */
4
+ export class AFSError extends Error {
5
+ code;
6
+ constructor(message, code) {
7
+ super(message);
8
+ this.name = "AFSError";
9
+ this.code = code;
10
+ }
11
+ }
12
+ /**
13
+ * Error thrown when attempting write operations on a readonly AFS or module.
14
+ */
15
+ export class AFSReadonlyError extends AFSError {
16
+ constructor(message) {
17
+ super(message, "AFS_READONLY");
18
+ this.name = "AFSReadonlyError";
19
+ }
20
+ }
@@ -1,2 +1,3 @@
1
1
  export * from "./afs.js";
2
+ export * from "./error.js";
2
3
  export * from "./type.js";
package/lib/esm/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./afs.js";
2
+ export * from "./error.js";
2
3
  export * from "./type.js";
package/lib/esm/type.d.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  import type { Emitter } from "strict-event-emitter";
2
- import { type ZodType } from "zod";
3
- export interface AFSListOptions {
2
+ import { type ZodType, z } from "zod";
3
+ /**
4
+ * Access mode for AFS modules and root.
5
+ * - "readonly": Only read operations are allowed (list, read, search)
6
+ * - "readwrite": All operations are allowed
7
+ */
8
+ export type AFSAccessMode = "readonly" | "readwrite";
9
+ /**
10
+ * Zod schema for access mode validation.
11
+ * Can be reused across modules that support access mode configuration.
12
+ */
13
+ export declare const accessModeSchema: z.ZodOptional<z.ZodEnum<["readonly", "readwrite"]>>;
14
+ export interface AFSOperationOptions {
15
+ context?: any;
16
+ }
17
+ export interface AFSListOptions extends AFSOperationOptions {
4
18
  filter?: {
5
19
  agentId?: string;
6
20
  userId?: string;
@@ -23,47 +37,40 @@ export interface AFSListOptions {
23
37
  * Examples: "*.ts", "**\/*.js", "src/**\/*.{ts,tsx}"
24
38
  */
25
39
  pattern?: string;
26
- context?: any;
27
40
  }
28
41
  export interface AFSListResult {
29
42
  data: AFSEntry[];
30
43
  message?: string;
31
- context?: any;
32
44
  }
33
- export interface AFSSearchOptions {
45
+ export interface AFSSearchOptions extends AFSOperationOptions {
34
46
  limit?: number;
35
47
  caseSensitive?: boolean;
36
- context?: any;
37
48
  }
38
49
  export interface AFSSearchResult {
39
50
  data: AFSEntry[];
40
51
  message?: string;
41
52
  }
42
- export interface AFSReadOptions {
53
+ export interface AFSReadOptions extends AFSOperationOptions {
43
54
  filter?: AFSListOptions["filter"];
44
- context?: any;
45
55
  }
46
56
  export interface AFSReadResult {
47
57
  data?: AFSEntry;
48
58
  message?: string;
49
59
  }
50
- export interface AFSDeleteOptions {
60
+ export interface AFSDeleteOptions extends AFSOperationOptions {
51
61
  recursive?: boolean;
52
- context?: any;
53
62
  }
54
63
  export interface AFSDeleteResult {
55
64
  message?: string;
56
65
  }
57
- export interface AFSRenameOptions {
66
+ export interface AFSRenameOptions extends AFSOperationOptions {
58
67
  overwrite?: boolean;
59
- context?: any;
60
68
  }
61
69
  export interface AFSRenameResult {
62
70
  message?: string;
63
71
  }
64
- export interface AFSWriteOptions {
72
+ export interface AFSWriteOptions extends AFSOperationOptions {
65
73
  append?: boolean;
66
- context?: any;
67
74
  }
68
75
  export interface AFSWriteResult {
69
76
  data: AFSEntry;
@@ -72,8 +79,7 @@ export interface AFSWriteResult {
72
79
  }
73
80
  export interface AFSWriteEntryPayload extends Omit<AFSEntry, "id" | "path"> {
74
81
  }
75
- export interface AFSExecOptions {
76
- context: any;
82
+ export interface AFSExecOptions extends AFSOperationOptions {
77
83
  }
78
84
  export interface AFSExecResult {
79
85
  data: Record<string, any>;
@@ -81,6 +87,19 @@ export interface AFSExecResult {
81
87
  export interface AFSModule {
82
88
  readonly name: string;
83
89
  readonly description?: string;
90
+ /**
91
+ * Access mode for this module.
92
+ * - "readonly": Only read operations are allowed
93
+ * - "readwrite": All operations are allowed
94
+ * Default behavior is implementation-specific.
95
+ */
96
+ readonly accessMode?: AFSAccessMode;
97
+ /**
98
+ * Enable automatic agent skill scanning for this module.
99
+ * When set to true, the system will scan this module for agent skills.
100
+ * @default false
101
+ */
102
+ readonly agentSkills?: boolean;
84
103
  onMount?(root: AFSRoot): void;
85
104
  symlinkToPhysical?(path: string): Promise<void>;
86
105
  list?(path: string, options?: AFSListOptions): Promise<AFSListResult>;
@@ -91,6 +110,39 @@ export interface AFSModule {
91
110
  search?(path: string, query: string, options?: AFSSearchOptions): Promise<AFSSearchResult>;
92
111
  exec?(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
93
112
  }
113
+ /**
114
+ * Parameters for loading a module from configuration.
115
+ */
116
+ export interface AFSModuleLoadParams {
117
+ /** Path to the configuration file */
118
+ filepath: string;
119
+ /** Parsed configuration object */
120
+ parsed?: object;
121
+ }
122
+ /**
123
+ * Interface for module classes that support schema validation and loading from configuration.
124
+ * This describes the static part of a module class.
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * class MyModule implements AFSModule {
129
+ * static schema() { return mySchema; }
130
+ * static async load(params: AFSModuleLoadParams) { ... }
131
+ * // ...
132
+ * }
133
+ *
134
+ * // Type check
135
+ * const _check: AFSModuleClass<MyModule, MyModuleOptions> = MyModule;
136
+ * ```
137
+ */
138
+ export interface AFSModuleClass<T extends AFSModule = AFSModule, O extends object = object> {
139
+ /** Returns the Zod schema for validating module configuration */
140
+ schema(): ZodType<O>;
141
+ /** Loads a module instance from configuration file path and parsed config */
142
+ load(params: AFSModuleLoadParams): Promise<T>;
143
+ /** Constructor */
144
+ new (options: O): T;
145
+ }
94
146
  export type AFSRootEvents = {
95
147
  agentSucceed: [
96
148
  {
@@ -104,7 +156,7 @@ export type AFSRootEvents = {
104
156
  ];
105
157
  historyCreated: [{
106
158
  entry: AFSEntry;
107
- }];
159
+ }, options: AFSOperationOptions];
108
160
  };
109
161
  export interface AFSRootListOptions extends AFSListOptions, AFSContextPreset {
110
162
  preset?: string;
@@ -133,6 +185,7 @@ export interface AFSEntryMetadata extends Record<string, any> {
133
185
  };
134
186
  childrenCount?: number;
135
187
  childrenTruncated?: boolean;
188
+ gitignored?: boolean;
136
189
  }
137
190
  export interface AFSEntry<T = any> {
138
191
  id: string;
package/lib/esm/type.js CHANGED
@@ -1,4 +1,12 @@
1
1
  import { z } from "zod";
2
+ /**
3
+ * Zod schema for access mode validation.
4
+ * Can be reused across modules that support access mode configuration.
5
+ */
6
+ export const accessModeSchema = z
7
+ .enum(["readonly", "readwrite"])
8
+ .describe("Access mode for this module")
9
+ .optional();
2
10
  export const afsEntrySchema = z.object({
3
11
  id: z.string(),
4
12
  createdAt: z.date().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/afs",
3
- "version": "1.4.0-beta.7",
3
+ "version": "1.4.0-beta.9",
4
4
  "description": "Agentic File System (AFS) is a virtual file system that supports various storage backends and provides a unified API for file operations.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -51,7 +51,7 @@
51
51
  "strict-event-emitter": "^0.5.1",
52
52
  "ufo": "^1.6.1",
53
53
  "zod": "^3.25.67",
54
- "@aigne/platform-helpers": "^0.6.7-beta.1"
54
+ "@aigne/platform-helpers": "^0.6.7-beta.2"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/bun": "^1.2.22",