@zocomputer/agent-sdk 0.5.0
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/LICENSE +21 -0
- package/README.md +673 -0
- package/dist/attachments.js +52 -0
- package/dist/gateway-fetch.js +67 -0
- package/dist/index.js +4169 -0
- package/dist/initiator-auth.js +49 -0
- package/dist/platform/agent-sandbox/index.js +691 -0
- package/dist/platform/cloud-tools/image.js +498 -0
- package/dist/platform/cloud-tools/index.js +507 -0
- package/dist/platform/cloud-tools/web-search.js +87 -0
- package/dist/platform/runtime-ai/gateway.js +87 -0
- package/dist/platform/runtime-ai/index.js +91 -0
- package/dist/platform/runtime-ai/register.js +88 -0
- package/dist/platform/runtime-ai/session-fetch.js +54 -0
- package/dist/platform/runtime-auth/index.js +141 -0
- package/dist/state-files.js +287 -0
- package/dist/state-sandbox.js +383 -0
- package/dist/state.js +29 -0
- package/dist/steer-inbox.js +84 -0
- package/dist/steer.js +83 -0
- package/package.json +143 -0
- package/platform/agent-sandbox/api-client.ts +196 -0
- package/platform/agent-sandbox/index.ts +27 -0
- package/platform/agent-sandbox/pure.ts +83 -0
- package/platform/agent-sandbox/sftp.ts +141 -0
- package/platform/agent-sandbox/ssh-connection.ts +165 -0
- package/platform/agent-sandbox/ssh-exec.ts +98 -0
- package/platform/agent-sandbox/ssh-session.ts +487 -0
- package/platform/agent-sandbox/zo-backend.ts +88 -0
- package/platform/agent-sandbox/zo-sandbox.ts +39 -0
- package/platform/cloud-tools/image-path.ts +44 -0
- package/platform/cloud-tools/image.ts +225 -0
- package/platform/cloud-tools/index.ts +22 -0
- package/platform/cloud-tools/state-files.ts +368 -0
- package/platform/cloud-tools/tool-meta.ts +32 -0
- package/platform/cloud-tools/web-search.ts +15 -0
- package/platform/runtime-ai/gateway.ts +76 -0
- package/platform/runtime-ai/index.ts +20 -0
- package/platform/runtime-ai/register.ts +26 -0
- package/platform/runtime-ai/session-fetch.ts +124 -0
- package/platform/runtime-auth/index.ts +331 -0
- package/src/async-tasks.ts +273 -0
- package/src/attachments.ts +109 -0
- package/src/backgroundable.ts +88 -0
- package/src/bounded-output.ts +159 -0
- package/src/dir-conventions.ts +238 -0
- package/src/extract/cache.ts +40 -0
- package/src/extract/docx.ts +18 -0
- package/src/extract/pdf.ts +54 -0
- package/src/extract/sheet.ts +56 -0
- package/src/file-kind.ts +258 -0
- package/src/file-view.ts +80 -0
- package/src/gateway-fetch.ts +115 -0
- package/src/glob-match.ts +13 -0
- package/src/hooks.ts +213 -0
- package/src/index.ts +419 -0
- package/src/initiator-auth.ts +81 -0
- package/src/instructions.ts +224 -0
- package/src/list-files.ts +41 -0
- package/src/mock-model.ts +572 -0
- package/src/park-delivery.ts +247 -0
- package/src/path-locks.ts +52 -0
- package/src/read-file-content.ts +142 -0
- package/src/read-text.ts +40 -0
- package/src/redeliver.ts +155 -0
- package/src/run.ts +159 -0
- package/src/sandbox-io.ts +414 -0
- package/src/state-files.ts +460 -0
- package/src/state-sandbox.ts +710 -0
- package/src/state.ts +96 -0
- package/src/steer-inbox.ts +105 -0
- package/src/steer-tool.ts +83 -0
- package/src/steer.ts +146 -0
- package/src/task.ts +320 -0
- package/src/tools/bash.ts +143 -0
- package/src/tools/edit.ts +58 -0
- package/src/tools/glob.ts +56 -0
- package/src/tools/grep.ts +188 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/tasks.ts +241 -0
- package/src/tools/webfetch.ts +368 -0
- package/src/tools/write.ts +42 -0
- package/src/walk.ts +112 -0
- package/src/watch-output.ts +104 -0
- package/src/web-fetch.ts +324 -0
- package/src/web-page.ts +179 -0
- package/src/workspace-io.ts +225 -0
- package/src/workspace.ts +41 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { generateImage } from "ai";
|
|
4
|
+
import { defineTool } from "eve/tools";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { ZO_TOOL_HEADER, zoGateway } from "../runtime-ai/index.ts";
|
|
8
|
+
import { imageOutputPath } from "./image-path";
|
|
9
|
+
import { CLOUD_TOOL_META } from "./tool-meta";
|
|
10
|
+
import {
|
|
11
|
+
createRuntimeStateFilesClient,
|
|
12
|
+
DEFAULT_STATE_ASSET_DECLARATION_NAME,
|
|
13
|
+
stateAssetReference,
|
|
14
|
+
type StateAssetReference,
|
|
15
|
+
type StateFilesAssetWriter,
|
|
16
|
+
} from "./state-files";
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_IMAGE_MODEL = "bfl/flux-2-pro";
|
|
19
|
+
|
|
20
|
+
type ImageSize = `${number}x${number}`;
|
|
21
|
+
type ImageAspectRatio = `${number}:${number}`;
|
|
22
|
+
|
|
23
|
+
function isImageSize(value: unknown): value is ImageSize {
|
|
24
|
+
return (
|
|
25
|
+
typeof value === "string" &&
|
|
26
|
+
/^[1-9]\d{1,4}x[1-9]\d{1,4}$/u.test(value)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isImageAspectRatio(value: unknown): value is ImageAspectRatio {
|
|
31
|
+
return (
|
|
32
|
+
typeof value === "string" &&
|
|
33
|
+
/^[1-9]\d{0,2}:[1-9]\d{0,2}$/u.test(value)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// z.templateLiteral (never z.custom): eve converts tool input schemas to JSON Schema
|
|
38
|
+
// with zod's toJSONSchema, which throws on z.custom — an agent with this tool would
|
|
39
|
+
// fail to boot. templateLiteral emits a string+pattern schema and keeps the
|
|
40
|
+
// template-literal types; the refine keeps the stricter bounds the pattern alone
|
|
41
|
+
// can't express (runtime-only, invisible to JSON Schema).
|
|
42
|
+
const SizeSchema = z
|
|
43
|
+
.templateLiteral([z.number().int().positive(), "x", z.number().int().positive()])
|
|
44
|
+
.refine(isImageSize, { message: "Use WIDTHxHEIGHT, for example 1024x1024." });
|
|
45
|
+
|
|
46
|
+
const AspectRatioSchema = z
|
|
47
|
+
.templateLiteral([z.number().int().positive(), ":", z.number().int().positive()])
|
|
48
|
+
.refine(isImageAspectRatio, { message: "Use WIDTH:HEIGHT, for example 1:1 or 16:9." });
|
|
49
|
+
|
|
50
|
+
const ImageDimensionsSchema = z.discriminatedUnion("kind", [
|
|
51
|
+
z.object({ kind: z.literal("auto") }).strict(),
|
|
52
|
+
z.object({ kind: z.literal("size"), size: SizeSchema }).strict(),
|
|
53
|
+
z
|
|
54
|
+
.object({
|
|
55
|
+
aspectRatio: AspectRatioSchema,
|
|
56
|
+
kind: z.literal("aspectRatio"),
|
|
57
|
+
})
|
|
58
|
+
.strict(),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const OutputDirSchema = z
|
|
62
|
+
.string()
|
|
63
|
+
.trim()
|
|
64
|
+
.min(1)
|
|
65
|
+
.max(200)
|
|
66
|
+
.regex(
|
|
67
|
+
/^(?!\/)(?!.*\/$)(?!.*\/\/)(?!.*(?:^|\/)(?:\.|\.\.)(?:\/|$))[A-Za-z0-9._/-]+$/u,
|
|
68
|
+
"Use a relative state file path without empty, . or .. segments.",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export const GenerateImageInputSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
dimensions: ImageDimensionsSchema.optional(),
|
|
74
|
+
model: z.string().trim().min(1).optional(),
|
|
75
|
+
outputDir: OutputDirSchema.optional(),
|
|
76
|
+
prompt: z.string().trim().min(1).max(4000),
|
|
77
|
+
seed: z.number().int().nonnegative().optional(),
|
|
78
|
+
})
|
|
79
|
+
.strict();
|
|
80
|
+
|
|
81
|
+
const StateAssetReferenceSchema = z
|
|
82
|
+
.object({
|
|
83
|
+
bytes: z.number().int().nonnegative().optional(),
|
|
84
|
+
contentType: z.string().optional(),
|
|
85
|
+
declarationName: z.string(),
|
|
86
|
+
path: z.string(),
|
|
87
|
+
type: z.literal("state_asset"),
|
|
88
|
+
})
|
|
89
|
+
.strict();
|
|
90
|
+
|
|
91
|
+
export const GenerateImageOutputSchema = z
|
|
92
|
+
.object({
|
|
93
|
+
asset: StateAssetReferenceSchema,
|
|
94
|
+
bytes: z.number().int().nonnegative(),
|
|
95
|
+
mediaType: z.string(),
|
|
96
|
+
model: z.string(),
|
|
97
|
+
path: z.string(),
|
|
98
|
+
prompt: z.string(),
|
|
99
|
+
warnings: z.array(z.string()),
|
|
100
|
+
})
|
|
101
|
+
.strict();
|
|
102
|
+
|
|
103
|
+
export type GenerateImageDimensions = z.infer<typeof ImageDimensionsSchema>;
|
|
104
|
+
export type GenerateImageInput = z.infer<typeof GenerateImageInputSchema>;
|
|
105
|
+
export type GenerateImageOutput = z.infer<typeof GenerateImageOutputSchema>;
|
|
106
|
+
|
|
107
|
+
interface GeneratedImageResult {
|
|
108
|
+
readonly image: {
|
|
109
|
+
readonly mediaType: string;
|
|
110
|
+
readonly uint8Array: Uint8Array;
|
|
111
|
+
};
|
|
112
|
+
readonly warnings: readonly unknown[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface GenerateImageToolOptions {
|
|
116
|
+
readonly assetWriter?: StateFilesAssetWriter;
|
|
117
|
+
readonly declarationName?: string;
|
|
118
|
+
readonly generate?: (options: Parameters<typeof generateImage>[0]) => Promise<GeneratedImageResult>;
|
|
119
|
+
readonly randomId?: () => string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface ImageDimensionSettings {
|
|
123
|
+
readonly aspectRatio?: ImageAspectRatio;
|
|
124
|
+
readonly size?: ImageSize;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function assertNever(value: never): never {
|
|
128
|
+
throw new Error(`Unhandled generate_image dimensions: ${JSON.stringify(value)}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function imageDimensionSettings(
|
|
132
|
+
dimensions: GenerateImageDimensions | undefined,
|
|
133
|
+
): ImageDimensionSettings {
|
|
134
|
+
if (dimensions === undefined || dimensions.kind === "auto") {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
switch (dimensions.kind) {
|
|
139
|
+
case "aspectRatio":
|
|
140
|
+
return { aspectRatio: dimensions.aspectRatio };
|
|
141
|
+
case "size":
|
|
142
|
+
return { size: dimensions.size };
|
|
143
|
+
default:
|
|
144
|
+
return assertNever(dimensions);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function warningText(warning: unknown): string {
|
|
149
|
+
if (warning instanceof Error) {
|
|
150
|
+
return warning.message;
|
|
151
|
+
}
|
|
152
|
+
if (typeof warning === "string") {
|
|
153
|
+
return warning;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The lib overload says `JSON.stringify` returns `string`, but it really
|
|
157
|
+
// returns `undefined` for symbols/functions — widen it back.
|
|
158
|
+
const json = JSON.stringify(warning) as string | undefined;
|
|
159
|
+
return json ?? String(warning);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function randomImageId(): string {
|
|
163
|
+
return randomUUID().slice(0, 8);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function generateImageTool(options: GenerateImageToolOptions = {}) {
|
|
167
|
+
const declarationName = options.declarationName ?? DEFAULT_STATE_ASSET_DECLARATION_NAME;
|
|
168
|
+
const assetWriter =
|
|
169
|
+
options.assetWriter ?? createRuntimeStateFilesClient({ declarationName });
|
|
170
|
+
const generate = options.generate ?? generateImage;
|
|
171
|
+
const randomId = options.randomId ?? randomImageId;
|
|
172
|
+
|
|
173
|
+
return defineTool({
|
|
174
|
+
description: CLOUD_TOOL_META.image.description,
|
|
175
|
+
inputSchema: GenerateImageInputSchema,
|
|
176
|
+
outputSchema: GenerateImageOutputSchema,
|
|
177
|
+
async execute(input): Promise<GenerateImageOutput> {
|
|
178
|
+
const model = input.model ?? DEFAULT_IMAGE_MODEL;
|
|
179
|
+
const result = await generate({
|
|
180
|
+
headers: { [ZO_TOOL_HEADER]: "generate_image" },
|
|
181
|
+
model: zoGateway().imageModel(model),
|
|
182
|
+
prompt: input.prompt,
|
|
183
|
+
...imageDimensionSettings(input.dimensions),
|
|
184
|
+
...(input.seed === undefined ? {} : { seed: input.seed }),
|
|
185
|
+
});
|
|
186
|
+
const image = result.image;
|
|
187
|
+
const path = imageOutputPath({
|
|
188
|
+
id: randomId(),
|
|
189
|
+
mediaType: image.mediaType,
|
|
190
|
+
outputDir: input.outputDir,
|
|
191
|
+
prompt: input.prompt,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await assetWriter.write(path, image.uint8Array, { contentType: image.mediaType });
|
|
195
|
+
const asset: StateAssetReference = stateAssetReference({
|
|
196
|
+
type: "state_asset",
|
|
197
|
+
declarationName,
|
|
198
|
+
path,
|
|
199
|
+
contentType: image.mediaType,
|
|
200
|
+
bytes: image.uint8Array.byteLength,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
asset,
|
|
205
|
+
bytes: image.uint8Array.byteLength,
|
|
206
|
+
mediaType: image.mediaType,
|
|
207
|
+
model,
|
|
208
|
+
path,
|
|
209
|
+
prompt: input.prompt,
|
|
210
|
+
warnings: result.warnings.map(warningText),
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
toModelOutput(output) {
|
|
214
|
+
return {
|
|
215
|
+
type: "text",
|
|
216
|
+
value:
|
|
217
|
+
`Generated image saved as state asset ${output.asset.declarationName}:${output.asset.path}. ` +
|
|
218
|
+
`The asset is available to the chat UI through the state_asset reference; ` +
|
|
219
|
+
`do not invent or expose a temporary URL.`,
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default generateImageTool();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_IMAGE_MODEL,
|
|
3
|
+
GenerateImageInputSchema,
|
|
4
|
+
GenerateImageOutputSchema,
|
|
5
|
+
default as generateImage,
|
|
6
|
+
generateImageTool,
|
|
7
|
+
} from "./image";
|
|
8
|
+
export type {
|
|
9
|
+
GenerateImageDimensions,
|
|
10
|
+
GenerateImageInput,
|
|
11
|
+
GenerateImageOutput,
|
|
12
|
+
GenerateImageToolOptions,
|
|
13
|
+
} from "./image";
|
|
14
|
+
export {
|
|
15
|
+
createRuntimeStateFilesClient,
|
|
16
|
+
DEFAULT_STATE_ASSET_DECLARATION_NAME,
|
|
17
|
+
stateAssetReference,
|
|
18
|
+
} from "./state-files";
|
|
19
|
+
export type { StateAssetReference, StateFilesAssetWriter } from "./state-files";
|
|
20
|
+
|
|
21
|
+
export { webSearch } from "./web-search";
|
|
22
|
+
export type { WebSearchConfig } from "./web-search";
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { ambientEveSessionId } from "../runtime-ai/session-fetch.ts";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_STATE_ASSET_DECLARATION_NAME = "files";
|
|
4
|
+
export const STATE_FILES_HANDLE_PATH = "/state/handles";
|
|
5
|
+
export const ZO_AGENT_TOKEN_HEADER = "x-zo-agent-token";
|
|
6
|
+
export const ZO_EVE_SESSION_HEADER = "x-zo-eve-session";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_STATE_FILES_SUGGESTED_DEFAULTS: RuntimeStateFilesSuggestedDefaults = Object.freeze({
|
|
9
|
+
engine: "zo-blob-r2",
|
|
10
|
+
partition: "session",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export interface StateAssetReference {
|
|
14
|
+
readonly type: "state_asset";
|
|
15
|
+
readonly declarationName: string;
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly contentType?: string;
|
|
18
|
+
readonly bytes?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface StateFilesCredentials {
|
|
22
|
+
readonly accessKeyId: string;
|
|
23
|
+
readonly secretAccessKey: string;
|
|
24
|
+
readonly sessionToken: string;
|
|
25
|
+
readonly expiresAt: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface StateFilesHandle {
|
|
29
|
+
readonly handleId: string;
|
|
30
|
+
readonly declarationName: string;
|
|
31
|
+
readonly interface: "files";
|
|
32
|
+
readonly access: "r" | "rw";
|
|
33
|
+
readonly engine: "zo-blob-r2";
|
|
34
|
+
readonly bucketName: string;
|
|
35
|
+
readonly endpoint: string;
|
|
36
|
+
readonly credentials: StateFilesCredentials;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface StateFilesWriteOptions {
|
|
40
|
+
readonly contentType?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface StateFilesAssetWriter {
|
|
44
|
+
write(path: string, body: Uint8Array, options?: StateFilesWriteOptions): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RuntimeStateFilesClientOptions {
|
|
48
|
+
readonly apiBaseUrl?: string | URL | null;
|
|
49
|
+
readonly agentToken?: string | null;
|
|
50
|
+
readonly declarationName?: string;
|
|
51
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
52
|
+
readonly getSessionId?: () => string | undefined;
|
|
53
|
+
readonly now?: () => Date;
|
|
54
|
+
readonly suggestedDefaults?: RuntimeStateFilesSuggestedDefaults;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RuntimeStateFilesSuggestedDefaults {
|
|
58
|
+
readonly engine?: "zo-blob-r2";
|
|
59
|
+
readonly partition?: "none" | "team" | "user" | "session";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class StateFilesRuntimeError extends Error {
|
|
63
|
+
constructor(message: string) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "StateFilesRuntimeError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createRuntimeStateFilesClient(
|
|
70
|
+
options: RuntimeStateFilesClientOptions = {},
|
|
71
|
+
): StateFilesAssetWriter {
|
|
72
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
73
|
+
const declarationName = options.declarationName ?? DEFAULT_STATE_ASSET_DECLARATION_NAME;
|
|
74
|
+
const getSessionId = options.getSessionId ?? ambientEveSessionId;
|
|
75
|
+
const now = options.now ?? (() => new Date());
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
async write(path, body, writeOptions) {
|
|
79
|
+
const key = normalizeStateFilePath(path);
|
|
80
|
+
const eveSessionKey = getSessionId();
|
|
81
|
+
const handle = await requestRuntimeStateFilesHandle({
|
|
82
|
+
apiBaseUrl: resolveApiBaseUrl(options.apiBaseUrl),
|
|
83
|
+
agentToken: resolveAgentToken(options.agentToken),
|
|
84
|
+
declarationName,
|
|
85
|
+
fetch: fetchImpl,
|
|
86
|
+
suggestedDefaults: options.suggestedDefaults ?? DEFAULT_STATE_FILES_SUGGESTED_DEFAULTS,
|
|
87
|
+
...(eveSessionKey === undefined ? {} : { eveSessionKey }),
|
|
88
|
+
});
|
|
89
|
+
if (handle.access !== "rw") {
|
|
90
|
+
throw new StateFilesRuntimeError(`state files handle "${handle.handleId}" is read-only`);
|
|
91
|
+
}
|
|
92
|
+
await putStateFileObject({
|
|
93
|
+
body,
|
|
94
|
+
bucketName: handle.bucketName,
|
|
95
|
+
credentials: handle.credentials,
|
|
96
|
+
endpoint: handle.endpoint,
|
|
97
|
+
fetch: fetchImpl,
|
|
98
|
+
key,
|
|
99
|
+
now,
|
|
100
|
+
...(writeOptions?.contentType === undefined ? {} : { contentType: writeOptions.contentType }),
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function stateAssetReference(input: StateAssetReference): StateAssetReference {
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
type: "state_asset",
|
|
109
|
+
declarationName: input.declarationName,
|
|
110
|
+
path: normalizeStateFilePath(input.path),
|
|
111
|
+
...(input.contentType === undefined ? {} : { contentType: input.contentType }),
|
|
112
|
+
...(input.bytes === undefined ? {} : { bytes: input.bytes }),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function normalizeStateFilePath(path: string): string {
|
|
117
|
+
if (path.length === 0) throw new Error("state file path must not be empty");
|
|
118
|
+
if (path.startsWith("/")) throw new Error(`state file path "${path}" must be relative`);
|
|
119
|
+
const segments = path.split("/");
|
|
120
|
+
if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
121
|
+
throw new Error(`state file path "${path}" must not contain empty, . or .. segments`);
|
|
122
|
+
}
|
|
123
|
+
return path;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface RequestRuntimeStateFilesHandleOptions {
|
|
127
|
+
readonly apiBaseUrl: string;
|
|
128
|
+
readonly agentToken: string;
|
|
129
|
+
readonly declarationName: string;
|
|
130
|
+
readonly eveSessionKey?: string;
|
|
131
|
+
readonly fetch: typeof globalThis.fetch;
|
|
132
|
+
readonly suggestedDefaults: RuntimeStateFilesSuggestedDefaults;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function requestRuntimeStateFilesHandle(
|
|
136
|
+
options: RequestRuntimeStateFilesHandleOptions,
|
|
137
|
+
): Promise<StateFilesHandle> {
|
|
138
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
139
|
+
headers.set(ZO_AGENT_TOKEN_HEADER, options.agentToken);
|
|
140
|
+
if (options.eveSessionKey !== undefined && options.eveSessionKey.trim().length > 0) {
|
|
141
|
+
headers.set(ZO_EVE_SESSION_HEADER, options.eveSessionKey.trim());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const response = await options.fetch(buildStateFilesHandleUrl(options.apiBaseUrl), {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers,
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
declarationName: options.declarationName,
|
|
149
|
+
interface: "files",
|
|
150
|
+
access: "rw",
|
|
151
|
+
suggestedDefaults: options.suggestedDefaults,
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
const json: unknown = await response.json().catch(() => null);
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new StateFilesRuntimeError(readBrokerErrorMessage(json));
|
|
157
|
+
}
|
|
158
|
+
const handle = parseStateFilesHandle(json);
|
|
159
|
+
if (handle === null) {
|
|
160
|
+
throw new StateFilesRuntimeError("state files broker returned a malformed handle");
|
|
161
|
+
}
|
|
162
|
+
return handle;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resolveApiBaseUrl(apiBaseUrl: string | URL | null | undefined): string {
|
|
166
|
+
const value = String(apiBaseUrl ?? process.env.ZO_API_URL ?? "").trim();
|
|
167
|
+
if (value.length === 0) {
|
|
168
|
+
throw new StateFilesRuntimeError("ZO_API_URL is required to write generated state assets");
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolveAgentToken(agentToken: string | null | undefined): string {
|
|
174
|
+
const value = (agentToken ?? process.env.ZO_AGENT_TOKEN ?? "").trim();
|
|
175
|
+
if (value.length === 0) {
|
|
176
|
+
throw new StateFilesRuntimeError("ZO_AGENT_TOKEN is required to write generated state assets");
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildStateFilesHandleUrl(apiBaseUrl: string): string {
|
|
182
|
+
const url = new URL(apiBaseUrl);
|
|
183
|
+
url.pathname = `${url.pathname.replace(/\/+$/u, "")}/${STATE_FILES_HANDLE_PATH.replace(/^\/+/, "")}`;
|
|
184
|
+
url.search = "";
|
|
185
|
+
url.hash = "";
|
|
186
|
+
return url.toString();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parseStateFilesHandle(value: unknown): StateFilesHandle | null {
|
|
190
|
+
if (!isRecord(value)) return null;
|
|
191
|
+
if (value.interface !== "files" || value.engine !== "zo-blob-r2") return null;
|
|
192
|
+
const access = value.access === "r" || value.access === "rw" ? value.access : null;
|
|
193
|
+
const handleId = readString(value, "handleId");
|
|
194
|
+
const declarationName = readString(value, "declarationName");
|
|
195
|
+
const bucketName = readString(value, "bucketName");
|
|
196
|
+
const endpoint = readString(value, "endpoint");
|
|
197
|
+
const credentials = parseStateFilesCredentials(value.credentials);
|
|
198
|
+
if (
|
|
199
|
+
access === null ||
|
|
200
|
+
handleId === null ||
|
|
201
|
+
declarationName === null ||
|
|
202
|
+
bucketName === null ||
|
|
203
|
+
endpoint === null ||
|
|
204
|
+
credentials === null
|
|
205
|
+
) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return { handleId, declarationName, interface: "files", access, engine: "zo-blob-r2", bucketName, endpoint, credentials };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseStateFilesCredentials(value: unknown): StateFilesCredentials | null {
|
|
212
|
+
if (!isRecord(value)) return null;
|
|
213
|
+
const accessKeyId = readString(value, "accessKeyId");
|
|
214
|
+
const secretAccessKey = readString(value, "secretAccessKey");
|
|
215
|
+
const sessionToken = readString(value, "sessionToken");
|
|
216
|
+
const expiresAt = readString(value, "expiresAt");
|
|
217
|
+
if (
|
|
218
|
+
accessKeyId === null ||
|
|
219
|
+
secretAccessKey === null ||
|
|
220
|
+
sessionToken === null ||
|
|
221
|
+
expiresAt === null ||
|
|
222
|
+
!Number.isFinite(Date.parse(expiresAt))
|
|
223
|
+
) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return { accessKeyId, secretAccessKey, sessionToken, expiresAt };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function readBrokerErrorMessage(value: unknown): string {
|
|
230
|
+
if (!isRecord(value)) return "state files broker request failed";
|
|
231
|
+
const error = isRecord(value.error) ? value.error : value;
|
|
232
|
+
return readString(error, "message") ?? "state files broker request failed";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface PutStateFileObjectOptions {
|
|
236
|
+
readonly endpoint: string;
|
|
237
|
+
readonly bucketName: string;
|
|
238
|
+
readonly credentials: StateFilesCredentials;
|
|
239
|
+
readonly key: string;
|
|
240
|
+
readonly body: Uint8Array;
|
|
241
|
+
readonly contentType?: string;
|
|
242
|
+
readonly fetch: typeof globalThis.fetch;
|
|
243
|
+
readonly now: () => Date;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function putStateFileObject(options: PutStateFileObjectOptions): Promise<void> {
|
|
247
|
+
const url = stateFileObjectUrl(options.endpoint, options.bucketName, options.key);
|
|
248
|
+
const payloadHash = await sha256Hex(options.body);
|
|
249
|
+
const headers = await signedS3Headers({
|
|
250
|
+
credentials: options.credentials,
|
|
251
|
+
date: options.now(),
|
|
252
|
+
host: url.host,
|
|
253
|
+
method: "PUT",
|
|
254
|
+
path: url.pathname,
|
|
255
|
+
payloadHash,
|
|
256
|
+
...(options.contentType === undefined ? {} : { contentType: options.contentType }),
|
|
257
|
+
});
|
|
258
|
+
const response = await options.fetch(url, {
|
|
259
|
+
method: "PUT",
|
|
260
|
+
headers,
|
|
261
|
+
body: options.body,
|
|
262
|
+
});
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
throw new StateFilesRuntimeError(`state asset write failed with ${response.status}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function stateFileObjectUrl(endpoint: string, bucketName: string, key: string): URL {
|
|
269
|
+
const base = endpoint.replace(/\/+$/u, "");
|
|
270
|
+
return new URL(`${base}/${encodeS3PathSegment(bucketName)}/${encodeS3Key(key)}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface SignedS3HeadersInput {
|
|
274
|
+
readonly method: "PUT";
|
|
275
|
+
readonly path: string;
|
|
276
|
+
readonly host: string;
|
|
277
|
+
readonly payloadHash: string;
|
|
278
|
+
readonly credentials: StateFilesCredentials;
|
|
279
|
+
readonly date: Date;
|
|
280
|
+
readonly contentType?: string;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function signedS3Headers(input: SignedS3HeadersInput): Promise<Headers> {
|
|
284
|
+
const amzDate = awsAmzDate(input.date);
|
|
285
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
286
|
+
const headerEntries: [string, string][] = [
|
|
287
|
+
["host", input.host],
|
|
288
|
+
["x-amz-content-sha256", input.payloadHash],
|
|
289
|
+
["x-amz-date", amzDate],
|
|
290
|
+
["x-amz-security-token", input.credentials.sessionToken],
|
|
291
|
+
];
|
|
292
|
+
if (input.contentType !== undefined) {
|
|
293
|
+
headerEntries.push(["content-type", input.contentType]);
|
|
294
|
+
}
|
|
295
|
+
headerEntries.sort(([left], [right]) => left.localeCompare(right));
|
|
296
|
+
const canonicalHeaders = headerEntries.map(([name, value]) => `${name}:${value.trim()}\n`).join("");
|
|
297
|
+
const signedHeaders = headerEntries.map(([name]) => name).join(";");
|
|
298
|
+
const canonicalRequest = [
|
|
299
|
+
input.method,
|
|
300
|
+
input.path,
|
|
301
|
+
"",
|
|
302
|
+
canonicalHeaders,
|
|
303
|
+
signedHeaders,
|
|
304
|
+
input.payloadHash,
|
|
305
|
+
].join("\n");
|
|
306
|
+
const scope = `${dateStamp}/auto/s3/aws4_request`;
|
|
307
|
+
const stringToSign = [
|
|
308
|
+
"AWS4-HMAC-SHA256",
|
|
309
|
+
amzDate,
|
|
310
|
+
scope,
|
|
311
|
+
await sha256Hex(new TextEncoder().encode(canonicalRequest)),
|
|
312
|
+
].join("\n");
|
|
313
|
+
const signingKey = await awsSigningKey(input.credentials.secretAccessKey, dateStamp);
|
|
314
|
+
const signature = await hmacHex(signingKey, stringToSign);
|
|
315
|
+
const headers = new Headers();
|
|
316
|
+
for (const [name, value] of headerEntries) headers.set(name, value);
|
|
317
|
+
headers.set(
|
|
318
|
+
"authorization",
|
|
319
|
+
`AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
|
320
|
+
);
|
|
321
|
+
return headers;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function awsSigningKey(secretAccessKey: string, dateStamp: string): Promise<Uint8Array> {
|
|
325
|
+
const dateKey = await hmacBytes(new TextEncoder().encode(`AWS4${secretAccessKey}`), dateStamp);
|
|
326
|
+
const regionKey = await hmacBytes(dateKey, "auto");
|
|
327
|
+
const serviceKey = await hmacBytes(regionKey, "s3");
|
|
328
|
+
return hmacBytes(serviceKey, "aws4_request");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function hmacBytes(key: Uint8Array, data: string): Promise<Uint8Array> {
|
|
332
|
+
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
333
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(data)));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function hmacHex(key: Uint8Array, data: string): Promise<string> {
|
|
337
|
+
return bytesToHex(await hmacBytes(key, data));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function sha256Hex(bytes: Uint8Array): Promise<string> {
|
|
341
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
342
|
+
return bytesToHex(new Uint8Array(digest));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
346
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function awsAmzDate(date: Date): string {
|
|
350
|
+
return date.toISOString().replace(/[:-]|\.\d{3}/gu, "");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function encodeS3Key(key: string): string {
|
|
354
|
+
return key.split("/").map(encodeS3PathSegment).join("/");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function encodeS3PathSegment(segment: string): string {
|
|
358
|
+
return encodeURIComponent(segment).replace(/[!'()*]/gu, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
362
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function readString(record: Record<string, unknown>, key: string): string | null {
|
|
366
|
+
const value = record[key];
|
|
367
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
368
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The single source of truth for each built-in Zo cloud tool's user-facing
|
|
2
|
+
// metadata — today just its description. Consumed on BOTH sides of a gap the
|
|
3
|
+
// tool object can't bridge:
|
|
4
|
+
// - the tool definition itself (`image.ts` reads its `description` from here), and
|
|
5
|
+
// - `apps/api`'s definition parser, which shows a stored agent's tools in the
|
|
6
|
+
// /build Settings tab. That parser reads tenant tool files as TEXT (it never
|
|
7
|
+
// imports/executes them), and a built-in is seeded as a one-line re-export —
|
|
8
|
+
// `export { default } from "./image.ts"` — that carries no
|
|
9
|
+
// `description:` string of its own. It looks the built-in up here instead.
|
|
10
|
+
//
|
|
11
|
+
// Keyed by the tool's `@zocomputer/cloud-tools/<key>` subpath segment (the seed's
|
|
12
|
+
// re-export target), NOT the wire name the model sees (the tool file's name, e.g.
|
|
13
|
+
// `generate_image`). Keep this module import-light — no `ai`/`eve`/`zod`/runtime —
|
|
14
|
+
// so `apps/api` can import it without pulling the agent-runtime deps into its bundle.
|
|
15
|
+
export const CLOUD_TOOL_META = {
|
|
16
|
+
image: {
|
|
17
|
+
description: "Generate an image from a text prompt and save it as an external state asset.",
|
|
18
|
+
},
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export type CloudToolName = keyof typeof CLOUD_TOOL_META;
|
|
22
|
+
export type CloudToolMeta = (typeof CLOUD_TOOL_META)[CloudToolName];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Metadata for a built-in cloud tool by its `@zocomputer/cloud-tools/<key>`
|
|
26
|
+
* subpath segment, or `null` for an unknown key — the checked lookup callers
|
|
27
|
+
* use to resolve an arbitrary (untrusted) string against the map. The `as
|
|
28
|
+
* CloudToolName` is proven safe by the preceding `in` guard and isolated here.
|
|
29
|
+
*/
|
|
30
|
+
export function cloudToolMeta(key: string): CloudToolMeta | null {
|
|
31
|
+
return key in CLOUD_TOOL_META ? CLOUD_TOOL_META[key as CloudToolName] : null;
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { zoGateway } from "../runtime-ai/index.ts";
|
|
2
|
+
|
|
3
|
+
type Gateway = ReturnType<typeof zoGateway>;
|
|
4
|
+
type ExaSearchFactory = Gateway["tools"]["exaSearch"];
|
|
5
|
+
|
|
6
|
+
export type WebSearchConfig = Parameters<ExaSearchFactory>[0];
|
|
7
|
+
|
|
8
|
+
export function webSearch(config?: WebSearchConfig): ReturnType<ExaSearchFactory> {
|
|
9
|
+
const gateway = zoGateway();
|
|
10
|
+
return config === undefined
|
|
11
|
+
? gateway.tools.exaSearch()
|
|
12
|
+
: gateway.tools.exaSearch(config);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default webSearch;
|