@nestm/storage 0.1.0-alpha.6 → 0.1.0-alpha.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 (67) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +182 -2
  3. package/SECURITY.md +40 -0
  4. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +63 -0
  5. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -0
  6. package/dist/ai-sdk/ai-sdk-workspace-tools.js +383 -0
  7. package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -0
  8. package/dist/ai-sdk/index.d.ts +2 -0
  9. package/dist/ai-sdk/index.d.ts.map +1 -0
  10. package/dist/ai-sdk/index.js +2 -0
  11. package/dist/ai-sdk/index.js.map +1 -0
  12. package/dist/files-sdk/files-sdk.driver.d.ts +16 -2
  13. package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
  14. package/dist/files-sdk/files-sdk.driver.js +159 -0
  15. package/dist/files-sdk/files-sdk.driver.js.map +1 -1
  16. package/dist/files-sdk/fs/index.d.ts +21 -2
  17. package/dist/files-sdk/fs/index.d.ts.map +1 -1
  18. package/dist/files-sdk/fs/index.js +600 -1
  19. package/dist/files-sdk/fs/index.js.map +1 -1
  20. package/dist/files-sdk/index.d.ts +1 -1
  21. package/dist/files-sdk/index.d.ts.map +1 -1
  22. package/dist/files-sdk/index.js.map +1 -1
  23. package/dist/files-sdk/provider/index.d.ts.map +1 -1
  24. package/dist/files-sdk/provider/index.js +7 -0
  25. package/dist/files-sdk/provider/index.js.map +1 -1
  26. package/dist/files-sdk/s3/index.d.ts +12 -3
  27. package/dist/files-sdk/s3/index.d.ts.map +1 -1
  28. package/dist/files-sdk/s3/index.js +203 -37
  29. package/dist/files-sdk/s3/index.js.map +1 -1
  30. package/dist/storage.client.d.ts +5 -1
  31. package/dist/storage.client.d.ts.map +1 -1
  32. package/dist/storage.client.js +49 -0
  33. package/dist/storage.client.js.map +1 -1
  34. package/dist/storage.driver.d.ts +3 -1
  35. package/dist/storage.driver.d.ts.map +1 -1
  36. package/dist/storage.driver.js.map +1 -1
  37. package/dist/storage.service.d.ts.map +1 -1
  38. package/dist/storage.service.js +28 -3
  39. package/dist/storage.service.js.map +1 -1
  40. package/dist/storage.types.d.ts +36 -0
  41. package/dist/storage.types.d.ts.map +1 -1
  42. package/dist/storage.types.js.map +1 -1
  43. package/dist/workspace/index.d.ts +4 -0
  44. package/dist/workspace/index.d.ts.map +1 -0
  45. package/dist/workspace/index.js +4 -0
  46. package/dist/workspace/index.js.map +1 -0
  47. package/dist/workspace/storage-workspace.cursor.d.ts +6 -0
  48. package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -0
  49. package/dist/workspace/storage-workspace.cursor.js +45 -0
  50. package/dist/workspace/storage-workspace.cursor.js.map +1 -0
  51. package/dist/workspace/storage-workspace.d.ts +6 -0
  52. package/dist/workspace/storage-workspace.d.ts.map +1 -0
  53. package/dist/workspace/storage-workspace.error.d.ts +28 -0
  54. package/dist/workspace/storage-workspace.error.d.ts.map +1 -0
  55. package/dist/workspace/storage-workspace.error.js +67 -0
  56. package/dist/workspace/storage-workspace.error.js.map +1 -0
  57. package/dist/workspace/storage-workspace.js +747 -0
  58. package/dist/workspace/storage-workspace.js.map +1 -0
  59. package/dist/workspace/storage-workspace.path.d.ts +9 -0
  60. package/dist/workspace/storage-workspace.path.d.ts.map +1 -0
  61. package/dist/workspace/storage-workspace.path.js +68 -0
  62. package/dist/workspace/storage-workspace.path.js.map +1 -0
  63. package/dist/workspace/storage-workspace.types.d.ts +97 -0
  64. package/dist/workspace/storage-workspace.types.d.ts.map +1 -0
  65. package/dist/workspace/storage-workspace.types.js +20 -0
  66. package/dist/workspace/storage-workspace.types.js.map +1 -0
  67. package/package.json +23 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @nestm/storage
2
2
 
3
+ ## 0.1.0-alpha.7
4
+
5
+ ### Minor Changes
6
+
7
+ - 842ff34: Add a backend-neutral `StorageWorkspace` capability and optional AI SDK 7 tool
8
+ adapter. Workspaces expose only canonical mount-relative paths, enforce
9
+ permissions and resource limits, hide provider coordinates and cursors, and use
10
+ atomic create/ETag mutation preconditions. S3 now advertises and implements the
11
+ conditional mutation primitives used by writable workspaces.
12
+
13
+ Harden local filesystem workspace reads and conditional mutations against
14
+ symlink aliases. Moves retain their create-only destination whenever source
15
+ deletion cannot be confirmed, avoiding data loss after provider or
16
+ post-operation hook ambiguity.
17
+
18
+ Fix cross-store sync so `destinationPrefix` is applied to uploaded keys as well
19
+ as pruning, keeping every mutation inside the selected destination scope.
20
+
3
21
  ## 0.1.0-alpha.6
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @nestm/storage
2
2
 
3
3
  Framework-neutral storage clients with NestJS 12 integration, named stores,
4
- explicit streaming I/O, cross-store workflows, and an optional guarded HTTP
5
- gateway.
4
+ explicit streaming I/O, capability-scoped agent workspaces, cross-store
5
+ workflows, and an optional guarded HTTP gateway.
6
6
 
7
7
  The package uses [`files-sdk`](https://github.com/haydenbleasel/files-sdk) as
8
8
  its provider engine, but owns the API injected into Nest applications. Provider
@@ -88,6 +88,186 @@ storage errors and operation types, and `StorageUploadControl`. It has no NestJS
88
88
  runtime or declaration imports. Provider adapters remain available through
89
89
  `@nestm/storage/files-sdk`.
90
90
 
91
+ ## Mounted agent workspaces
92
+
93
+ `@nestm/storage/workspace` turns a `StorageClient` into a narrow capability for
94
+ one logical directory. The mount is virtual: the same API works over S3, a
95
+ filesystem driver, or another storage backend without exposing the provider,
96
+ bucket, filesystem root, raw cursor, or internal prefix to its caller.
97
+
98
+ ```mermaid
99
+ flowchart LR
100
+ A["Trusted application context"] -->|"store + opaque prefix + policy"| W["StorageWorkspace"]
101
+ W --> C["StorageClient"]
102
+ C --> D["S3 / filesystem / other driver"]
103
+ W --> T["AI SDK workspace tools"]
104
+ T --> G["ToolLoopAgent"]
105
+ ```
106
+
107
+ Only trusted application code chooses the mount prefix. Every path accepted by
108
+ the workspace is a canonical, relative POSIX path. Absolute paths, backslashes,
109
+ control characters, repeated separators, and `.` or `..` segments are rejected
110
+ rather than normalized. Keys and provider cursors returned by a driver are also
111
+ checked before they are converted back to logical paths.
112
+
113
+ ```ts
114
+ import { mountStorageWorkspace } from '@nestm/storage/workspace';
115
+
116
+ const workspace = mountStorageWorkspace(agentFiles, {
117
+ // Use an opaque server-derived run id, never a value selected by the model.
118
+ prefix: `workspaces/${runId}`,
119
+ permissions: [
120
+ 'list',
121
+ 'read',
122
+ 'search',
123
+ 'create',
124
+ 'replace',
125
+ 'copy',
126
+ 'move',
127
+ 'delete',
128
+ ],
129
+ limits: {
130
+ maxReadBytes: 1024 * 1024,
131
+ maxWriteBytes: 1024 * 1024,
132
+ maxPageSize: 100,
133
+ maxSearchResults: 100,
134
+ maxSearchScan: 1000,
135
+ },
136
+ });
137
+
138
+ const created = await workspace.writeFile(
139
+ 'src/main.ts',
140
+ 'export const ready = true;\n',
141
+ { mode: 'create', contentType: 'text/typescript' },
142
+ );
143
+
144
+ if (created.etag === undefined) {
145
+ throw new Error('This backend cannot safely replace the object.');
146
+ }
147
+
148
+ await workspace.writeFile('src/main.ts', 'export const ready = false;\n', {
149
+ mode: 'replace',
150
+ etag: created.etag,
151
+ contentType: 'text/typescript',
152
+ });
153
+ ```
154
+
155
+ Create, replace, and delete are conditional operations. A driver that cannot
156
+ enforce the requested not-exists or ETag precondition fails with
157
+ `NOT_SUPPORTED`; the workspace never substitutes an `exists()`/`head()` check
158
+ followed by an unconditional mutation. Reads enforce their byte ceiling while
159
+ consuming the stream, and list/search results are bounded. Search supports
160
+ exact, substring, and workspace-coordinate glob matching, but no caller-supplied
161
+ regular expressions.
162
+
163
+ Move is implemented as create-only copy followed by ETag-conditional source
164
+ delete. If source deletion cannot be confirmed, the destination is retained and
165
+ the call returns `CONFLICT`; inspect both logical paths before retrying. This
166
+ preserves at least one copy across provider timeouts and post-operation hook
167
+ failures, but does not pretend a multi-object move is transactionally atomic.
168
+
169
+ A child mount may further restrict a directory, permissions, or limits, but it
170
+ cannot widen any of them:
171
+
172
+ ```ts
173
+ const readOnlySource = workspace.mount('src', {
174
+ permissions: ['list', 'read', 'search'],
175
+ limits: { maxReadBytes: 256 * 1024 },
176
+ });
177
+ ```
178
+
179
+ ### AI SDK and NestJS composition
180
+
181
+ Install AI SDK 7 and Zod only in applications that use the optional adapter:
182
+
183
+ ```sh
184
+ pnpm add ai@^7 zod@^4
185
+ ```
186
+
187
+ `files-sdk` 2.2.x still declares an optional `ai@^6` peer for its own adapter,
188
+ so some package managers may print a peer warning when AI SDK 7 is installed.
189
+ This package does not import that adapter; `@nestm/storage/ai-sdk` targets AI
190
+ SDK 7 directly.
191
+
192
+ `@nestm/storage/ai-sdk` converts an already-mounted workspace to an ordinary
193
+ upstream `ToolSet`. It does not import NestJS or `@nestm/ai-sdk`; the application
194
+ composes the tool set through the AI module's existing named-toolset factory.
195
+ For a tenant or run selected per request, make both factories request-scoped and
196
+ derive the mount coordinate from authenticated host context:
197
+
198
+ ```ts
199
+ import { Module, Scope } from '@nestjs/common';
200
+ import { AiSdkModule, AiSdkService, getAiToolsetToken } from '@nestm/ai-sdk';
201
+ import { getStorageToken, type StorageClient } from '@nestm/storage';
202
+ import { createAiSdkWorkspaceTools } from '@nestm/storage/ai-sdk';
203
+ import { mountStorageWorkspace } from '@nestm/storage/workspace';
204
+ import type { ToolSet } from 'ai';
205
+
206
+ @Module({
207
+ imports: [
208
+ AppStorageModule,
209
+ WorkspaceContextModule,
210
+ AiSdkModule.forFeature({
211
+ imports: [AppStorageModule, WorkspaceContextModule],
212
+ toolsets: [
213
+ {
214
+ name: 'workspace',
215
+ scope: Scope.REQUEST,
216
+ inject: [getStorageToken('agent-files'), WorkspaceContext],
217
+ useFactory: (storage: StorageClient, context: WorkspaceContext) =>
218
+ createAiSdkWorkspaceTools({
219
+ workspace: mountStorageWorkspace(storage, {
220
+ // A validated, opaque coordinate from trusted auth/run state.
221
+ // It is never accepted from a prompt or tool input.
222
+ prefix: context.storagePrefix,
223
+ permissions: [
224
+ 'list',
225
+ 'read',
226
+ 'search',
227
+ 'create',
228
+ 'replace',
229
+ 'copy',
230
+ 'move',
231
+ 'delete',
232
+ ],
233
+ }),
234
+ }),
235
+ },
236
+ ],
237
+ agents: [
238
+ {
239
+ name: 'workspace-agent',
240
+ scope: Scope.REQUEST,
241
+ inject: [AiSdkService, getAiToolsetToken('workspace')],
242
+ useFactory: (ai: AiSdkService, tools: ToolSet) => ({
243
+ model: ai.languageModel(),
244
+ instructions:
245
+ 'Use only the mounted workspace tools for file operations.',
246
+ tools,
247
+ }),
248
+ },
249
+ ],
250
+ }),
251
+ ],
252
+ })
253
+ export class WorkspaceAgentModule {}
254
+ ```
255
+
256
+ The generated set contains only tools allowed by the workspace permissions:
257
+ bounded list, stat, UTF-8 read, and search tools plus conditional create,
258
+ replace, copy, move, and delete tools when granted. Mutation tools require AI
259
+ SDK user approval by default; approval can be configured per tool, but the
260
+ workspace capability remains the authorization boundary even when approval is
261
+ disabled. The module's `AiSdkService.files()` API is the model provider's file
262
+ upload facility and is unrelated to storage workspaces.
263
+
264
+ This logical confinement is sufficient for a `ToolLoopAgent` whose only file
265
+ capabilities are these tools. It cannot constrain a coding harness that already
266
+ has shell, `node:fs`, or subprocess access. For Codex/Claude-style harnesses,
267
+ materialize the workspace into a per-session container or VM, mount only that
268
+ directory, run the harness there, and synchronize reviewed changes back through
269
+ `StorageWorkspace`. A working directory alone is not a sandbox.
270
+
91
271
  ## Configure named stores
92
272
 
93
273
  Use the package-owned S3 factory when applicable. For other providers, create a
package/SECURITY.md CHANGED
@@ -46,3 +46,43 @@ the validated `head` or with an immutable provider version. Ordinary `copy`
46
46
  does not protect against a staging-key replay between validation and copy.
47
47
  Conditional promotion keeps the staging source; delete it only after the
48
48
  application's metadata transaction commits.
49
+
50
+ ## Workspace security boundary
51
+
52
+ `StorageWorkspace` is a narrowing capability for storage operations. Construct
53
+ it from trusted application context, keep the underlying `StorageClient` and
54
+ mount prefix private, and pass only the workspace or its AI tool set to
55
+ untrusted agent code. A tenant id, run id, prefix, provider cursor, snapshot id,
56
+ or fork id supplied by a model is not a safe mount coordinate.
57
+
58
+ The workspace accepts only canonical mount-relative POSIX paths and rechecks
59
+ every key returned by a driver before unscoping it. Its cursors are opaque and
60
+ bound to the mount and query. Permissions, byte limits, result limits, and
61
+ conditional mutation preconditions are enforced inside the capability; tool
62
+ omission and user approval are additional workflow controls, not the
63
+ authorization boundary.
64
+
65
+ Conditional mutations can still have an ambiguous outcome when a remote
66
+ provider commits and then loses or violates its response, or when a configured
67
+ post-operation plugin fails after the driver has committed. The API fails
68
+ closed in that case: inspect the logical destination and reconcile it before
69
+ retrying. Create-only and ETag preconditions prevent a blind retry from
70
+ silently overwriting a different object, but they cannot make a multi-object
71
+ move transactionally atomic.
72
+
73
+ This guarantee covers calls made through `StorageWorkspace`. It does not
74
+ confine arbitrary `node:fs`, shell, subprocess, or native-code access in the
75
+ same process. A coding harness with built-in shell or filesystem tools must run
76
+ inside an OS sandbox (container, VM, or equivalent) that exposes only a
77
+ materialized workspace. Setting `cwd` to a workspace directory is not
78
+ isolation.
79
+
80
+ For local filesystem storage, use a dedicated service-owned root and do not let
81
+ another untrusted process mutate its directory tree concurrently. High-level
82
+ Node filesystem checks reject symlinks and hard-linked object files in existing
83
+ workspace read and mutation paths, including metadata sidecars, but cannot
84
+ provide a race-proof boundary against an actor that can replace path components
85
+ between validation and use. For that threat model, mount only the workspace
86
+ into a separate UID/container/VM and synchronize approved results back through
87
+ storage. The local adapter also commits the body and metadata sidecar as two
88
+ files; a process crash between their atomic renames can require reconciliation.
@@ -0,0 +1,63 @@
1
+ import { type ToolSet } from 'ai';
2
+ import { type StorageWorkspace } from '../workspace/index.js';
3
+ import { type StorageErrorCode as StorageErrorCodeValue } from '../storage.error.js';
4
+ export declare const AI_SDK_WORKSPACE_TOOL_NAMES: readonly ['workspace_list', 'workspace_stat', 'workspace_read_file', 'workspace_search', 'workspace_write_file', 'workspace_copy_file', 'workspace_move_file', 'workspace_delete_file'];
5
+ export type AiSdkWorkspaceToolName = (typeof AI_SDK_WORKSPACE_TOOL_NAMES)[number];
6
+ export type AiSdkWorkspaceMutationToolName = Extract<AiSdkWorkspaceToolName, 'workspace_write_file' | 'workspace_copy_file' | 'workspace_move_file' | 'workspace_delete_file'>;
7
+ /**
8
+ * Approval policy for mutation tools. An omitted entry defaults to requiring
9
+ * approval, matching an omitted policy.
10
+ */
11
+ export type AiSdkWorkspaceApprovalConfig = boolean | Partial<Record<AiSdkWorkspaceMutationToolName, boolean>>;
12
+ export interface CreateAiSdkWorkspaceToolsOptions {
13
+ /** The already-mounted, policy-enforcing workspace exposed to the tools. */
14
+ workspace: StorageWorkspace;
15
+ /**
16
+ * Optional tighter read ceiling. Values above the workspace ceiling are
17
+ * clamped; the model cannot choose or raise this value.
18
+ */
19
+ maxReadBytes?: number;
20
+ /** Mutation tools require approval by default. */
21
+ requireApproval?: AiSdkWorkspaceApprovalConfig;
22
+ }
23
+ export type AiSdkWorkspaceToolErrorCode = StorageErrorCodeValue;
24
+ /**
25
+ * Safe error exposed at the AI tool boundary. It deliberately carries no
26
+ * provider error, storage key, mount prefix, store name, or cause.
27
+ */
28
+ export declare class AiSdkWorkspaceToolError extends Error {
29
+ readonly code: AiSdkWorkspaceToolErrorCode;
30
+ constructor(code: AiSdkWorkspaceToolErrorCode);
31
+ }
32
+ export interface AiSdkWorkspaceFileResult {
33
+ kind: 'file';
34
+ path: string;
35
+ name: string;
36
+ size: number;
37
+ contentType: string;
38
+ etag?: string;
39
+ lastModified?: string;
40
+ }
41
+ export interface AiSdkWorkspaceDirectoryResult {
42
+ kind: 'directory';
43
+ path: string;
44
+ name: string;
45
+ }
46
+ export type AiSdkWorkspaceEntryResult = AiSdkWorkspaceFileResult | AiSdkWorkspaceDirectoryResult;
47
+ export interface AiSdkWorkspaceTextFileResult extends AiSdkWorkspaceFileResult {
48
+ text: string;
49
+ }
50
+ export interface AiSdkWorkspacePageResult {
51
+ entries: AiSdkWorkspaceEntryResult[];
52
+ cursor?: string;
53
+ }
54
+ /**
55
+ * Creates Vercel AI SDK tools backed only by a mounted StorageWorkspace.
56
+ *
57
+ * A tool is omitted unless the workspace grants its required permission.
58
+ * The workspace remains the enforcing boundary if a retained tool reference
59
+ * is invoked after further narrowing.
60
+ */
61
+ export declare function createAiSdkWorkspaceTools({ workspace, maxReadBytes: requestedMaxReadBytes, requireApproval, }: CreateAiSdkWorkspaceToolsOptions): ToolSet;
62
+ export declare function isAiSdkWorkspaceMutationToolName(value: string): value is AiSdkWorkspaceMutationToolName;
63
+ //# sourceMappingURL=ai-sdk-workspace-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-sdk-workspace-tools.d.ts","sourceRoot":"","sources":["../../src/ai-sdk/ai-sdk-workspace-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,IAAI,CAAC;AAGxC,OAAO,EAEL,KAAK,gBAAgB,EAItB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAGL,KAAK,gBAAgB,IAAI,qBAAqB,EAC/C,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,2BAA2B,YACtC,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,CACf,CAAC;AAEX,MAAM,MAAM,sBAAsB,GAChC,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/C,MAAM,MAAM,8BAA8B,GAAG,OAAO,CAClD,sBAAsB,EACpB,sBAAsB,GACtB,qBAAqB,GACrB,qBAAqB,GACrB,uBAAuB,CAC1B,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,4BAA4B,GACtC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,8BAA8B,EAAE,OAAO,CAAC,CAAC,CAAC;AAErE,MAAM,WAAW,gCAAgC;IAC/C,4EAA4E;IAC5E,SAAS,EAAE,gBAAgB,CAAC;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,eAAe,CAAC,EAAE,4BAA4B,CAAC;CAChD;AAED,MAAM,MAAM,2BAA2B,GAAG,qBAAqB,CAAC;AAsBhE;;;GAGG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAE3C,YAAY,IAAI,EAAE,2BAA2B,EAI5C;CACF;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,yBAAyB,GACnC,wBAAwB,GAAG,6BAA6B,CAAC;AAE3D,MAAM,WAAW,4BAA6B,SAAQ,wBAAwB;IAC5E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,yBAAyB,EAAE,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA2MD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,SAAS,EACT,YAAY,EAAE,qBAAqB,EACnC,eAAsB,GACvB,EAAE,gCAAgC,GAAG,OAAO,CA2R5C;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,8BAA8B,CAEzC"}