@nestm/storage 0.1.0-alpha.8 → 0.1.0-alpha.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 +25 -0
- package/README.md +86 -1
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +9 -2
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -1
- package/dist/ai-sdk/ai-sdk-workspace-tools.js +117 -58
- package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -1
- package/dist/ai-sdk/index.d.ts +1 -1
- package/dist/ai-sdk/index.d.ts.map +1 -1
- package/dist/ai-sdk/index.js.map +1 -1
- package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
- package/dist/files-sdk/files-sdk.driver.js +51 -4
- package/dist/files-sdk/files-sdk.driver.js.map +1 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/storage-workspace.d.ts.map +1 -1
- package/dist/workspace/storage-workspace.js +116 -24
- package/dist/workspace/storage-workspace.js.map +1 -1
- package/dist/workspace/storage-workspace.types.d.ts +22 -4
- package/dist/workspace/storage-workspace.types.d.ts.map +1 -1
- package/dist/workspace/storage-workspace.types.js +1 -0
- package/dist/workspace/storage-workspace.types.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @nestm/storage
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.9
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- aec25d6: Add explicit last-write-wins workspace write, copy, and unconditional-delete
|
|
8
|
+
operations that traverse the ordinary Files SDK plugin, hook, and receipt
|
|
9
|
+
pipeline while retaining the existing native conditional create, replace,
|
|
10
|
+
copy, move, and delete variants. Unconditional delete requires both `write` and
|
|
11
|
+
`delete`; move remains conditional-only because a non-atomic
|
|
12
|
+
download/upload/delete sequence could delete a newer source generation. Add a
|
|
13
|
+
separate `write` permission and an AI tool factory mutation-mode switch whose
|
|
14
|
+
default remains conditional.
|
|
15
|
+
|
|
16
|
+
Add bounded binary workspace reads through `readBytes`, alongside the existing
|
|
17
|
+
UTF-8 `readText` API. `readBytes` is a required `StorageWorkspace` member, so
|
|
18
|
+
custom interface implementations and typed test doubles must add it when
|
|
19
|
+
upgrading; workspaces returned by `mountStorageWorkspace` need no changes.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 37e0d8d: Fail conditional storage operations closed when caller-configured Files SDK
|
|
24
|
+
plugins, hooks, or receipts would be bypassed by native adapter extensions.
|
|
25
|
+
Ordinary operations continue through Files SDK while incompatible conditional
|
|
26
|
+
capabilities are hidden until Files SDK exposes one shared interception boundary.
|
|
27
|
+
|
|
3
28
|
## 0.1.0-alpha.8
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -167,6 +167,7 @@ const workspace = mountStorageWorkspace(agentFiles, {
|
|
|
167
167
|
'list',
|
|
168
168
|
'read',
|
|
169
169
|
'search',
|
|
170
|
+
'write',
|
|
170
171
|
'create',
|
|
171
172
|
'replace',
|
|
172
173
|
'copy',
|
|
@@ -199,8 +200,16 @@ await workspace.writeFile('src/main.ts', 'export const ready = false;\n', {
|
|
|
199
200
|
etag: created.etag,
|
|
200
201
|
contentType: 'text/typescript',
|
|
201
202
|
});
|
|
203
|
+
|
|
204
|
+
const image = await workspace.readBytes('assets/logo.png');
|
|
205
|
+
console.log(image.bytes.byteLength);
|
|
202
206
|
```
|
|
203
207
|
|
|
208
|
+
`readBytes` is now a required member of the exported `StorageWorkspace`
|
|
209
|
+
interface. Workspaces returned by `mountStorageWorkspace` provide it
|
|
210
|
+
automatically; custom implementations and typed test doubles must add the
|
|
211
|
+
method when adopting this alpha minor.
|
|
212
|
+
|
|
204
213
|
Create, replace, and delete are conditional operations. A driver that cannot
|
|
205
214
|
enforce the requested not-exists or ETag precondition fails with
|
|
206
215
|
`NOT_SUPPORTED`; the workspace never substitutes an `exists()`/`head()` check
|
|
@@ -215,6 +224,32 @@ the call returns `CONFLICT`; inspect both logical paths before retrying. This
|
|
|
215
224
|
preserves at least one copy across provider timeouts and post-operation hook
|
|
216
225
|
failures, but does not pretend a multi-object move is transactionally atomic.
|
|
217
226
|
|
|
227
|
+
Callers that prefer the ordinary Files pipeline can opt into explicit
|
|
228
|
+
last-write-wins variants. The `write` permission is separate from conditional
|
|
229
|
+
`create` and `replace` authority:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
await workspace.writeFile('notes.txt', 'latest contents', {
|
|
233
|
+
mode: 'overwrite',
|
|
234
|
+
});
|
|
235
|
+
await workspace.copyFile('notes.txt', 'backup.txt', { mode: 'overwrite' });
|
|
236
|
+
await workspace.deleteFile('backup.txt', { mode: 'unconditional' });
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Overwrite copy reads the latest source through the ordinary download pipeline,
|
|
240
|
+
enforces `maxWriteBytes` while collecting it, and uploads it through the
|
|
241
|
+
ordinary upload pipeline. It never substitutes the provider's server-side
|
|
242
|
+
copy. These paths compose with Files SDK plugins, hooks, and receipts, including
|
|
243
|
+
the built-in `encryption()` plugin. That plugin is useful compatibility
|
|
244
|
+
evidence, not an Artifact-specific security policy: strict encrypted-only
|
|
245
|
+
reads, tenant/path-bound AAD, key custody and rotation, and copy/move rules
|
|
246
|
+
remain application-owned.
|
|
247
|
+
|
|
248
|
+
Move remains conditional-only. A last-write-wins download/upload/delete
|
|
249
|
+
sequence could copy one source generation and then delete a newer generation
|
|
250
|
+
written during the transfer. Use the ETag-conditional `moveFile` variant when a
|
|
251
|
+
move is required.
|
|
252
|
+
|
|
218
253
|
A child mount may further restrict a directory, permissions, or limits, but it
|
|
219
254
|
cannot widen any of them:
|
|
220
255
|
|
|
@@ -275,6 +310,7 @@ import type { ToolSet } from 'ai';
|
|
|
275
310
|
'list',
|
|
276
311
|
'read',
|
|
277
312
|
'search',
|
|
313
|
+
'write',
|
|
278
314
|
'create',
|
|
279
315
|
'replace',
|
|
280
316
|
'copy',
|
|
@@ -312,6 +348,13 @@ workspace capability remains the authorization boundary even when approval is
|
|
|
312
348
|
disabled. The module's `AiSdkService.files()` API is the model provider's file
|
|
313
349
|
upload facility and is unrelated to storage workspaces.
|
|
314
350
|
|
|
351
|
+
`mutationMode` defaults to `'conditional'`. A trusted composition can instead
|
|
352
|
+
select `{ mutationMode: 'last-write-wins' }`; generated mutation schemas then
|
|
353
|
+
omit ETags and modes, hardcode the explicit overwrite/unconditional workspace
|
|
354
|
+
variants, and require `write` permission for destination mutations.
|
|
355
|
+
Unconditional delete requires both `write` and `delete`. The move tool is
|
|
356
|
+
omitted in last-write-wins mode because Workspace move remains conditional-only.
|
|
357
|
+
|
|
315
358
|
Atomic create collisions remain sanitized tool errors by default. Applications
|
|
316
359
|
that model an existing destination as a normal tool result can map that one
|
|
317
360
|
case while preserving replace/ETag conflicts as failures:
|
|
@@ -328,7 +371,8 @@ const tools = createAiSdkWorkspaceTools({
|
|
|
328
371
|
```
|
|
329
372
|
|
|
330
373
|
The mapper receives only the logical workspace path; provider errors, object
|
|
331
|
-
keys, and mount coordinates are never exposed.
|
|
374
|
+
keys, and mount coordinates are never exposed. `mapCreateConflict` is valid
|
|
375
|
+
only in conditional mode and is rejected with last-write-wins mode.
|
|
332
376
|
|
|
333
377
|
This logical confinement is sufficient for a `ToolLoopAgent` whose only file
|
|
334
378
|
capabilities are these tools. It cannot constrain a coding harness that already
|
|
@@ -555,6 +599,47 @@ run the reusable
|
|
|
555
599
|
against dedicated test credentials. Unknown endpoints are forced read-only and
|
|
556
600
|
receive no inferred conditional capabilities.
|
|
557
601
|
|
|
602
|
+
## Files SDK responsibility boundary
|
|
603
|
+
|
|
604
|
+
Files SDK is the upstream authority for the generic storage data plane:
|
|
605
|
+
provider adapters, generic CRUD, bulk and list operations, retries, transfers
|
|
606
|
+
and sync, its plugin pipeline, and framework-neutral gateway mechanics.
|
|
607
|
+
`@nestm/storage` retains the guarantees that Files SDK does not currently
|
|
608
|
+
provide: NestJS 12 named stores, exact native conditional/CAS capabilities,
|
|
609
|
+
`StorageWorkspace` permissions and limits, bounded storage errors, and
|
|
610
|
+
capability-scoped AI tools.
|
|
611
|
+
|
|
612
|
+
On the alpha.8 base, ordinary `FilesSdkStorageDriver` operations already
|
|
613
|
+
delegate to the Files SDK pipeline. The exception is the native conditional
|
|
614
|
+
adapter extensions: the current Files SDK operation union does not include
|
|
615
|
+
them, so they cannot run through caller-configured Files plugins, hooks, or
|
|
616
|
+
receipts. Until Files SDK provides one interception boundary for ordinary and
|
|
617
|
+
conditional operations, the driver applies this interim fail-closed
|
|
618
|
+
compatibility rule:
|
|
619
|
+
|
|
620
|
+
| Caller Files configuration | Ordinary operations | Conditional operations |
|
|
621
|
+
| ------------------------------------------------- | ------------------------------------ | -------------------------------------------------------- |
|
|
622
|
+
| No plugins, active hooks, or receipts | Files pipeline | Advertised when the adapter supports the exact primitive |
|
|
623
|
+
| One or more plugins | Files pipeline, including transforms | Hidden; direct invocation returns `NOT_SUPPORTED` |
|
|
624
|
+
| Any active hook | Files pipeline and hook callbacks | Hidden; direct invocation returns `NOT_SUPPORTED` |
|
|
625
|
+
| Receipts enabled with `true` or an options object | Files pipeline and receipts | Hidden; direct invocation returns `NOT_SUPPORTED` |
|
|
626
|
+
|
|
627
|
+
An empty plugin list, an empty hooks object, and `receipts: false` do not trigger
|
|
628
|
+
the gate. NestM's internal physical-key guard does not trigger it either. When
|
|
629
|
+
available, direct conditional paths independently apply prefixing, the
|
|
630
|
+
physical-key budget, mutation read-only restrictions, default
|
|
631
|
+
retry/signal/timeout options, and bounded error mapping. `StoragePlugin` remains
|
|
632
|
+
a separate veto/observation boundary; it is not a substitute for Files body or
|
|
633
|
+
result transforms. This compatibility gate is intended to be removed once
|
|
634
|
+
native CAS can traverse the upstream operation and plugin pipeline rather than
|
|
635
|
+
becoming a second generic CRUD facade here.
|
|
636
|
+
|
|
637
|
+
`StorageWorkspace` therefore exposes both contracts without weakening either:
|
|
638
|
+
its existing create/replace, exact-read copy/move, and conditional-delete paths
|
|
639
|
+
retain native CAS and this fail-closed gate, while explicit overwrite and
|
|
640
|
+
unconditional-delete variants use the ordinary Files pipeline. Lower-level
|
|
641
|
+
conditional client and driver APIs remain available to callers that need them.
|
|
642
|
+
|
|
558
643
|
## Storage API
|
|
559
644
|
|
|
560
645
|
`StorageClient` exposes:
|
|
@@ -9,6 +9,7 @@ export type AiSdkWorkspaceMutationToolName = Extract<AiSdkWorkspaceToolName, 'wo
|
|
|
9
9
|
* approval, matching an omitted policy.
|
|
10
10
|
*/
|
|
11
11
|
export type AiSdkWorkspaceApprovalConfig = boolean | Partial<Record<AiSdkWorkspaceMutationToolName, boolean>>;
|
|
12
|
+
export type AiSdkWorkspaceMutationMode = 'conditional' | 'last-write-wins';
|
|
12
13
|
export interface AiSdkWorkspaceCreateConflict {
|
|
13
14
|
/** The logical destination inside the mounted workspace. */
|
|
14
15
|
readonly path: string;
|
|
@@ -24,10 +25,16 @@ export interface CreateAiSdkWorkspaceToolsOptions<CreateConflictResult extends J
|
|
|
24
25
|
maxReadBytes?: number;
|
|
25
26
|
/** Mutation tools require approval by default. */
|
|
26
27
|
requireApproval?: AiSdkWorkspaceApprovalConfig;
|
|
28
|
+
/**
|
|
29
|
+
* Selects the mutation contract exposed to the model. Conditional mode is
|
|
30
|
+
* the default; last-write-wins uses the workspace's ordinary Files path.
|
|
31
|
+
*/
|
|
32
|
+
mutationMode?: AiSdkWorkspaceMutationMode;
|
|
27
33
|
/**
|
|
28
34
|
* Maps an atomic create collision to an application result. When omitted,
|
|
29
35
|
* the collision remains an AiSdkWorkspaceToolError like every other storage
|
|
30
|
-
* failure. Replace conflicts are never mapped by this hook.
|
|
36
|
+
* failure. Replace conflicts are never mapped by this hook. This option is
|
|
37
|
+
* valid only when mutationMode is conditional.
|
|
31
38
|
*/
|
|
32
39
|
mapCreateConflict?: AiSdkWorkspaceCreateConflictMapper<CreateConflictResult>;
|
|
33
40
|
}
|
|
@@ -69,6 +76,6 @@ export interface AiSdkWorkspacePageResult {
|
|
|
69
76
|
* The workspace remains the enforcing boundary if a retained tool reference
|
|
70
77
|
* is invoked after further narrowing.
|
|
71
78
|
*/
|
|
72
|
-
export declare function createAiSdkWorkspaceTools<CreateConflictResult extends JSONValue = never>({ workspace, maxReadBytes: requestedMaxReadBytes, requireApproval, mapCreateConflict, }: CreateAiSdkWorkspaceToolsOptions<CreateConflictResult>): ToolSet;
|
|
79
|
+
export declare function createAiSdkWorkspaceTools<CreateConflictResult extends JSONValue = never>({ workspace, maxReadBytes: requestedMaxReadBytes, mutationMode, requireApproval, mapCreateConflict, }: CreateAiSdkWorkspaceToolsOptions<CreateConflictResult>): ToolSet;
|
|
73
80
|
export declare function isAiSdkWorkspaceMutationToolName(value: string): value is AiSdkWorkspaceMutationToolName;
|
|
74
81
|
//# sourceMappingURL=ai-sdk-workspace-tools.d.ts.map
|
|
@@ -1 +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,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,IAAI,CAAC;AAGxD,OAAO,EAGL,KAAK,gBAAgB,EAItB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAGL,KAAK,gBAAgB,IAAI,qBAAqB,EAC/C,MAAM,qBAAqB,CAAC;AAG7B,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,4BAA4B;IAC3C,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,kCAAkC,CAAC,MAAM,SAAS,SAAS,IAAI,CACzE,QAAQ,EAAE,4BAA4B,KACnC,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;AAElC,MAAM,WAAW,gCAAgC,CAC/C,oBAAoB,SAAS,SAAS,GAAG,KAAK;IAE9C,4EAA4E;IAC5E,SAAS,EAAE,gBAAgB,CAAC;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C
|
|
1
|
+
{"version":3,"file":"ai-sdk-workspace-tools.d.ts","sourceRoot":"","sources":["../../src/ai-sdk/ai-sdk-workspace-tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,7 +16,7 @@ export const AI_SDK_WORKSPACE_TOOL_NAMES = [
|
|
|
16
16
|
const SAFE_ERROR_MESSAGES = {
|
|
17
17
|
[StorageErrorCode.NOT_FOUND]: 'The requested workspace path was not found.',
|
|
18
18
|
[StorageErrorCode.UNAUTHORIZED]: 'This operation is not permitted in the workspace.',
|
|
19
|
-
[StorageErrorCode.CONFLICT]: 'The operation conflicts with current workspace state.
|
|
19
|
+
[StorageErrorCode.CONFLICT]: 'The operation conflicts with current workspace state. Inspect the affected paths before retrying.',
|
|
20
20
|
[StorageErrorCode.READ_ONLY]: 'This operation is not permitted in the workspace.',
|
|
21
21
|
[StorageErrorCode.INVALID_ARGUMENT]: 'The workspace tool input was rejected.',
|
|
22
22
|
[StorageErrorCode.NOT_SUPPORTED]: 'This workspace operation is not supported by the configured storage.',
|
|
@@ -210,7 +210,13 @@ function serializePage(page) {
|
|
|
210
210
|
* The workspace remains the enforcing boundary if a retained tool reference
|
|
211
211
|
* is invoked after further narrowing.
|
|
212
212
|
*/
|
|
213
|
-
export function createAiSdkWorkspaceTools({ workspace, maxReadBytes: requestedMaxReadBytes, requireApproval = true, mapCreateConflict, }) {
|
|
213
|
+
export function createAiSdkWorkspaceTools({ workspace, maxReadBytes: requestedMaxReadBytes, mutationMode = 'conditional', requireApproval = true, mapCreateConflict, }) {
|
|
214
|
+
if (mutationMode !== 'conditional' && mutationMode !== 'last-write-wins') {
|
|
215
|
+
throw new RangeError('mutationMode must be "conditional" or "last-write-wins".');
|
|
216
|
+
}
|
|
217
|
+
if (mutationMode === 'last-write-wins' && mapCreateConflict !== undefined) {
|
|
218
|
+
throw new RangeError('mapCreateConflict is available only in conditional mutation mode.');
|
|
219
|
+
}
|
|
214
220
|
const maxReadBytes = resolveReadLimit(workspace, requestedMaxReadBytes);
|
|
215
221
|
const tools = {};
|
|
216
222
|
const pathSchema = logicalPath('File path', workspace.limits.maxPathBytes);
|
|
@@ -248,7 +254,9 @@ export function createAiSdkWorkspaceTools({ workspace, maxReadBytes: requestedMa
|
|
|
248
254
|
}
|
|
249
255
|
if (workspace.allows('read')) {
|
|
250
256
|
tools.workspace_stat = tool({
|
|
251
|
-
description:
|
|
257
|
+
description: mutationMode === 'conditional'
|
|
258
|
+
? 'Inspect a file inside the mounted workspace without reading its contents. Returns the ETag required for safe replace, move, and delete operations.'
|
|
259
|
+
: 'Inspect a file inside the mounted workspace without reading its contents. Any returned ETag is informational in last-write-wins mode.',
|
|
252
260
|
strict: true,
|
|
253
261
|
inputSchema: z.object({ path: pathSchema }).strict(),
|
|
254
262
|
execute: ({ path }, { abortSignal }) => executeSafely(abortSignal, async () => serializeFile(await workspace.stat(path, operationOptions(abortSignal)))),
|
|
@@ -291,62 +299,96 @@ export function createAiSdkWorkspaceTools({ workspace, maxReadBytes: requestedMa
|
|
|
291
299
|
}))),
|
|
292
300
|
});
|
|
293
301
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
.refine((value) => utf8Encoder.encode(value).byteLength <=
|
|
302
|
-
workspace.limits.maxWriteBytes, {
|
|
303
|
-
message: `Content exceeds the ${workspace.limits.maxWriteBytes}-byte workspace write limit.`,
|
|
304
|
-
})
|
|
305
|
-
.describe(`UTF-8 text to write. The workspace enforces its ${workspace.limits.maxWriteBytes}-byte write limit.`),
|
|
306
|
-
};
|
|
307
|
-
const createSchema = z
|
|
308
|
-
.object({ ...commonWriteShape, mode: z.literal('create') })
|
|
309
|
-
.strict();
|
|
310
|
-
const replaceSchema = z
|
|
311
|
-
.object({
|
|
312
|
-
...commonWriteShape,
|
|
313
|
-
mode: z.literal('replace'),
|
|
314
|
-
etag: etagSchema,
|
|
302
|
+
const commonWriteShape = {
|
|
303
|
+
path: pathSchema,
|
|
304
|
+
content: z
|
|
305
|
+
.string()
|
|
306
|
+
.refine((value) => utf8Encoder.encode(value).byteLength <=
|
|
307
|
+
workspace.limits.maxWriteBytes, {
|
|
308
|
+
message: `Content exceeds the ${workspace.limits.maxWriteBytes}-byte workspace write limit.`,
|
|
315
309
|
})
|
|
316
|
-
.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
? createSchema
|
|
321
|
-
: replaceSchema;
|
|
310
|
+
.describe(`UTF-8 text to write. The workspace enforces its ${workspace.limits.maxWriteBytes}-byte write limit.`),
|
|
311
|
+
};
|
|
312
|
+
if (mutationMode === 'last-write-wins' && workspace.allows('write')) {
|
|
313
|
+
const inputSchema = z.object(commonWriteShape).strict();
|
|
322
314
|
tools.workspace_write_file = tool({
|
|
323
|
-
description:
|
|
324
|
-
|
|
325
|
-
: canCreate
|
|
326
|
-
? 'Create a new UTF-8 text file inside the mounted workspace. The operation fails if the destination already exists.'
|
|
327
|
-
: 'Replace an existing UTF-8 text file inside the mounted workspace using its current ETag.',
|
|
328
|
-
// The combined create/replace schema is a discriminated union. OpenAI
|
|
329
|
-
// strict function tools reject its root-level oneOf, while the runtime
|
|
330
|
-
// Zod schema continues to validate every tool call in non-strict mode.
|
|
331
|
-
strict: !(canCreate && canReplace),
|
|
315
|
+
description: 'Write a UTF-8 text file inside the mounted workspace. An existing destination is overwritten; the last successful writer wins.',
|
|
316
|
+
strict: true,
|
|
332
317
|
inputSchema,
|
|
333
318
|
needsApproval: resolveApproval('workspace_write_file', requireApproval),
|
|
334
|
-
execute: (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
})
|
|
339
|
-
: await workspace.writeFile(input.path, input.content, {
|
|
340
|
-
mode: 'replace',
|
|
341
|
-
etag: input.etag,
|
|
342
|
-
...operationOptions(abortSignal),
|
|
343
|
-
})), mapCreateConflict),
|
|
319
|
+
execute: ({ path, content }, { abortSignal }) => executeSafely(abortSignal, async () => serializeFile(await workspace.writeFile(path, content, {
|
|
320
|
+
mode: 'overwrite',
|
|
321
|
+
...operationOptions(abortSignal),
|
|
322
|
+
}))),
|
|
344
323
|
});
|
|
345
324
|
}
|
|
346
|
-
|
|
347
|
-
workspace.allows('
|
|
348
|
-
workspace.allows('
|
|
349
|
-
|
|
325
|
+
else if (mutationMode === 'conditional') {
|
|
326
|
+
const canCreate = workspace.allows('create');
|
|
327
|
+
const canReplace = workspace.allows('replace');
|
|
328
|
+
if (canCreate || canReplace) {
|
|
329
|
+
const createSchema = z
|
|
330
|
+
.object({ ...commonWriteShape, mode: z.literal('create') })
|
|
331
|
+
.strict();
|
|
332
|
+
const replaceSchema = z
|
|
333
|
+
.object({
|
|
334
|
+
...commonWriteShape,
|
|
335
|
+
mode: z.literal('replace'),
|
|
336
|
+
etag: etagSchema,
|
|
337
|
+
})
|
|
338
|
+
.strict();
|
|
339
|
+
const inputSchema = canCreate && canReplace
|
|
340
|
+
? z.discriminatedUnion('mode', [createSchema, replaceSchema])
|
|
341
|
+
: canCreate
|
|
342
|
+
? createSchema
|
|
343
|
+
: replaceSchema;
|
|
344
|
+
tools.workspace_write_file = tool({
|
|
345
|
+
description: canCreate && canReplace
|
|
346
|
+
? 'Create a new UTF-8 text file or replace an existing file inside the mounted workspace. Create fails if the destination exists; replace requires its current ETag.'
|
|
347
|
+
: canCreate
|
|
348
|
+
? 'Create a new UTF-8 text file inside the mounted workspace. The operation fails if the destination already exists.'
|
|
349
|
+
: 'Replace an existing UTF-8 text file inside the mounted workspace using its current ETag.',
|
|
350
|
+
// The combined create/replace schema is a discriminated union. OpenAI
|
|
351
|
+
// strict function tools reject its root-level oneOf, while the runtime
|
|
352
|
+
// Zod schema continues to validate every tool call in non-strict mode.
|
|
353
|
+
strict: !(canCreate && canReplace),
|
|
354
|
+
inputSchema,
|
|
355
|
+
needsApproval: resolveApproval('workspace_write_file', requireApproval),
|
|
356
|
+
execute: (input, { abortSignal }) => executeCreateAware(abortSignal, input, async () => serializeFile(input.mode === 'create'
|
|
357
|
+
? await workspace.writeFile(input.path, input.content, {
|
|
358
|
+
mode: 'create',
|
|
359
|
+
...operationOptions(abortSignal),
|
|
360
|
+
})
|
|
361
|
+
: await workspace.writeFile(input.path, input.content, {
|
|
362
|
+
mode: 'replace',
|
|
363
|
+
etag: input.etag,
|
|
364
|
+
...operationOptions(abortSignal),
|
|
365
|
+
})), mapCreateConflict),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const canCopy = workspace.allows('copy') && workspace.allows('read');
|
|
370
|
+
if (mutationMode === 'last-write-wins' &&
|
|
371
|
+
canCopy &&
|
|
372
|
+
workspace.allows('write')) {
|
|
373
|
+
tools.workspace_copy_file = tool({
|
|
374
|
+
description: 'Copy the latest readable contents of a file inside the mounted workspace. The source remains intact, and an existing destination is overwritten.',
|
|
375
|
+
strict: true,
|
|
376
|
+
inputSchema: z
|
|
377
|
+
.object({
|
|
378
|
+
source: logicalPath('Source file path', workspace.limits.maxPathBytes),
|
|
379
|
+
destination: logicalPath('Destination file path', workspace.limits.maxPathBytes),
|
|
380
|
+
})
|
|
381
|
+
.strict(),
|
|
382
|
+
needsApproval: resolveApproval('workspace_copy_file', requireApproval),
|
|
383
|
+
execute: ({ source, destination }, { abortSignal }) => executeSafely(abortSignal, async () => serializeFile(await workspace.copyFile(source, destination, {
|
|
384
|
+
mode: 'overwrite',
|
|
385
|
+
...operationOptions(abortSignal),
|
|
386
|
+
}))),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
else if (mutationMode === 'conditional' &&
|
|
390
|
+
canCopy &&
|
|
391
|
+
workspace.allows('create')) {
|
|
350
392
|
tools.workspace_copy_file = tool({
|
|
351
393
|
description: 'Copy an exact observed version of a file inside the mounted workspace. The source remains intact, and the operation fails if the source changed or the destination already exists.',
|
|
352
394
|
strict: true,
|
|
@@ -364,11 +406,11 @@ export function createAiSdkWorkspaceTools({ workspace, maxReadBytes: requestedMa
|
|
|
364
406
|
}))),
|
|
365
407
|
});
|
|
366
408
|
}
|
|
367
|
-
|
|
409
|
+
if (mutationMode === 'conditional' &&
|
|
410
|
+
workspace.allows('move') &&
|
|
368
411
|
workspace.allows('read') &&
|
|
369
|
-
workspace.allows('
|
|
370
|
-
workspace.allows('
|
|
371
|
-
if (canMove) {
|
|
412
|
+
workspace.allows('delete') &&
|
|
413
|
+
workspace.allows('create')) {
|
|
372
414
|
tools.workspace_move_file = tool({
|
|
373
415
|
description: "Move a file inside the mounted workspace using the source's current ETag. The operation fails if the destination already exists. If source deletion cannot be confirmed, the destination is retained and the tool reports a conflict; inspect both paths before retrying.",
|
|
374
416
|
strict: true,
|
|
@@ -386,7 +428,24 @@ export function createAiSdkWorkspaceTools({ workspace, maxReadBytes: requestedMa
|
|
|
386
428
|
}))),
|
|
387
429
|
});
|
|
388
430
|
}
|
|
389
|
-
if (
|
|
431
|
+
if (mutationMode === 'last-write-wins' &&
|
|
432
|
+
workspace.allows('delete') &&
|
|
433
|
+
workspace.allows('write')) {
|
|
434
|
+
tools.workspace_delete_file = tool({
|
|
435
|
+
description: 'Unconditionally delete the current file at a path inside the mounted workspace.',
|
|
436
|
+
strict: true,
|
|
437
|
+
inputSchema: z.object({ path: pathSchema }).strict(),
|
|
438
|
+
needsApproval: resolveApproval('workspace_delete_file', requireApproval),
|
|
439
|
+
execute: ({ path }, { abortSignal }) => executeSafely(abortSignal, async () => {
|
|
440
|
+
await workspace.deleteFile(path, {
|
|
441
|
+
mode: 'unconditional',
|
|
442
|
+
...operationOptions(abortSignal),
|
|
443
|
+
});
|
|
444
|
+
return { deleted: true, path };
|
|
445
|
+
}),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else if (mutationMode === 'conditional' && workspace.allows('delete')) {
|
|
390
449
|
tools.workspace_delete_file = tool({
|
|
391
450
|
description: 'Delete a file inside the mounted workspace using its current ETag.',
|
|
392
451
|
strict: true,
|