@noimosai/platform-cli 0.1.3 → 0.1.4
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/README.md +12 -56
- package/bin/noimos-platform.mjs +3 -4
- package/dist/cli/cli.js +477 -87
- package/dist/cli/command-help.js +349 -23
- package/dist/cli/download.js +25 -25
- package/dist/cli/input-error.js +48 -0
- package/dist/cli/local-agents.js +199 -0
- package/dist/cli/mcp-supervisor.js +136 -0
- package/dist/cli/noimos-cli.js +72 -0
- package/dist/cli/project-archive.js +27 -0
- package/dist/cli/project-files.js +25 -0
- package/dist/cli/project-sync.js +259 -0
- package/dist/cli/setup-lock.js +1 -0
- package/dist/cli/setup.js +347 -0
- package/dist/cli/skills.js +142 -0
- package/dist/cli/team-harness-targets.js +337 -0
- package/dist/cli/team-harness.js +1562 -0
- package/dist/cli/team-resources.js +222 -0
- package/dist/cli/team-skills.js +457 -0
- package/dist/cli/update-bootstrap.js +437 -0
- package/dist/cli/update-release.js +212 -0
- package/dist/cli/version.js +1 -0
- package/dist/cli/wallet.js +85 -0
- package/dist/client/agent-run.js +1425 -0
- package/dist/client/browser.js +905 -0
- package/dist/client/distribution.js +2 -0
- package/dist/client/evm-wallet.js +214 -0
- package/dist/client/execution-context.js +14 -0
- package/dist/client/failure-recovery.js +128 -0
- package/dist/client/index.js +4 -225
- package/dist/client/local-input.js +33 -12
- package/dist/client/local-profile-lock.js +52 -0
- package/dist/client/local-profile.js +181 -0
- package/dist/client/local-projects.js +131 -0
- package/dist/client/local-setup.js +93 -0
- package/dist/client/realtime-voice.js +184 -0
- package/dist/client/tool-name-hints.js +48 -0
- package/dist/client/wallet-access.js +214 -0
- package/dist/mcp/agent-server.js +345 -0
- package/dist/mcp/context.js +86 -0
- package/dist/mcp/host-schema.js +76 -0
- package/dist/mcp/icon.js +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/instructions.js +26 -0
- package/dist/mcp/local-projects.js +42 -0
- package/dist/mcp/local-upload.js +39 -0
- package/dist/mcp/noimos-runtime.js +18 -0
- package/dist/mcp/noimos-server.js +38 -0
- package/dist/mcp/operations.js +40 -0
- package/dist/mcp/parallel-result.js +17 -0
- package/dist/mcp/project-sync.js +105 -0
- package/dist/mcp/results-reply.js +15 -0
- package/dist/mcp/runtime.js +81 -0
- package/dist/mcp/server.js +410 -0
- package/dist/mcp/team-skill.js +63 -0
- package/dist/mcp/update-notice.js +75 -0
- package/dist/mcp/version.js +1 -0
- package/dist/schema/accountRequestMethods.js +1 -0
- package/dist/schema/boundedBody.js +30 -0
- package/dist/schema/cronValidator.js +150 -0
- package/dist/schema/platformApiKey.js +14 -0
- package/dist/schema/platformAppAuth.js +11 -0
- package/dist/schema/platformBillingConstants.js +4 -0
- package/dist/schema/platformCapabilityFamilies.js +293 -0
- package/dist/schema/platformConnectionCatalog.js +39 -0
- package/dist/schema/platformConnectionPolicy.js +33 -0
- package/dist/schema/platformCustomTools.js +148 -0
- package/dist/schema/platformEndUserApi.js +62 -0
- package/dist/schema/platformFunding.js +10 -0
- package/dist/schema/platformHttpConstraints.js +12 -0
- package/dist/schema/platformLocalSetup.js +25 -0
- package/dist/schema/platformMcpTools.js +7 -0
- package/dist/schema/platformMetaTools.js +63 -0
- package/dist/schema/platformMonitorSchedule.js +123 -0
- package/dist/schema/platformMonitors.js +159 -0
- package/dist/schema/platformOAuth.js +13 -0
- package/dist/schema/platformProjectPermissions.js +33 -0
- package/dist/schema/platformPublicExecution.js +202 -21
- package/dist/schema/platformRoutes.js +122 -0
- package/dist/schema/platformSkills.js +11 -0
- package/dist/schema/platformTeamSecretName.js +3 -0
- package/dist/schema/platformTeamSkillSlug.js +1 -0
- package/dist/schema/platformTeamSkills.js +302 -0
- package/dist/schema/platformToolAccess.js +62 -0
- package/dist/schema/platformTriggers.js +153 -0
- package/dist/schema/platformWorkflows.js +100 -0
- package/dist/schema/scheduleTimeZone.js +15 -0
- package/dist/schema/toolDiscovery.js +71 -0
- package/dist/schema/toolInputRepair.js +122 -0
- package/dist/schema/toolResultContext.js +51 -0
- package/dist/schema/untrustedContent.js +35 -0
- package/npm-shrinkwrap.json +73 -0
- package/package.json +6 -3
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { projectScopeSchema, platformJsonObjectSchema, PLATFORM_SANDBOX_LIMITS } from "./platformPublicExecution.js";
|
|
3
|
+
import { isPlatformCallerHeaderName, isPlatformHeaderValue, PLATFORM_APP_HOST_PATTERN, PLATFORM_APP_RESERVED_HEADERS } from "./platformHttpConstraints.js";
|
|
4
|
+
import { platformTeamSecretNameSchema } from "./platformTeamSecretName.js";
|
|
5
|
+
export const PLATFORM_CUSTOM_TOOL_PREFIX = "custom__";
|
|
6
|
+
export const PLATFORM_CUSTOM_TOOL_LIMITS = {
|
|
7
|
+
perProject: 100,
|
|
8
|
+
versions: 50,
|
|
9
|
+
slugChars: 40,
|
|
10
|
+
titleChars: 80,
|
|
11
|
+
descriptionChars: 1_000,
|
|
12
|
+
codeChars: 40_000,
|
|
13
|
+
schemaBytes: 16_384,
|
|
14
|
+
inputBytes: 16_384,
|
|
15
|
+
};
|
|
16
|
+
export const PLATFORM_CUSTOM_ENDPOINT_LIMITS = { bindings: 5, baseUrlChars: 2_048, nameChars: 64, templateChars: 256, secretChars: 4_096, headers: 10, query: 10, valueChars: 1_024 };
|
|
17
|
+
export const PLATFORM_METERED_CALL = { includedMs: 5_000, stepMs: 5_000, includedBytes: 1_048_576, stepBytes: 262_144, unitPriceMicros: 300 };
|
|
18
|
+
export const PLATFORM_CUSTOM_ENDPOINT_BOUNDS = { timeoutMs: { standard: PLATFORM_METERED_CALL.includedMs, maximum: 300_000 }, responseMiB: { standard: 1, maximum: 14 } };
|
|
19
|
+
const startedSteps = (amount, included, step) => Math.ceil(Math.max(amount - included, 0) / step);
|
|
20
|
+
export const platformMeteredCallUnits = (elapsedMs, receivedBytes) => startedSteps(elapsedMs, PLATFORM_METERED_CALL.includedMs, PLATFORM_METERED_CALL.stepMs) + startedSteps(receivedBytes, PLATFORM_METERED_CALL.includedBytes, PLATFORM_METERED_CALL.stepBytes);
|
|
21
|
+
export const isExtendedCustomEndpoint = ({ timeoutMs, responseMiB }) => timeoutMs > PLATFORM_CUSTOM_ENDPOINT_BOUNDS.timeoutMs.standard || responseMiB > PLATFORM_CUSTOM_ENDPOINT_BOUNDS.responseMiB.standard;
|
|
22
|
+
export const platformCustomEndpointCostCeiling = (limits) => isExtendedCustomEndpoint(limits)
|
|
23
|
+
? platformMeteredCallUnits(limits.timeoutMs, limits.responseMiB * 1_048_576) * PLATFORM_METERED_CALL.unitPriceMicros : 0;
|
|
24
|
+
export const platformCustomToolKindSchema = z.enum(["python", "http"]);
|
|
25
|
+
export const platformCustomEndpointLocationSchema = z.enum(["header", "query"]);
|
|
26
|
+
const HEADER_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
27
|
+
const QUERY_NAME = /^[A-Za-z0-9_.$-]{1,64}$/;
|
|
28
|
+
const FORBIDDEN_BINDING_HEADER = /^(?:keep-alive|trailer|proxy-.*|x-forwarded-.*|x-real-ip|set-cookie|accept-encoding|content-type|accept)$/i;
|
|
29
|
+
const CONTROL = /[\u0000-\u001f\u007f]/;
|
|
30
|
+
export const PLATFORM_CUSTOM_ENDPOINT_SECRET_PLACEHOLDER = "{secret}";
|
|
31
|
+
export function isPlatformCustomEndpointBaseUrl(value) {
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(value);
|
|
34
|
+
return url.protocol === "https:" && !url.username && !url.password && !url.hash && !url.search && !value.includes("?") && !value.includes("#")
|
|
35
|
+
&& PLATFORM_APP_HOST_PATTERN.test(url.host) && !/(?:^|\/)\.{1,2}(?:\/|$)/.test(url.pathname) && !/[\\\s]/.test(value) && !CONTROL.test(value);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export const platformCustomEndpointBaseUrlSchema = z.string().trim().min(1).max(PLATFORM_CUSTOM_ENDPOINT_LIMITS.baseUrlChars)
|
|
42
|
+
.refine(isPlatformCustomEndpointBaseUrl, "An https URL on a public host name, without credentials, a query or a fragment")
|
|
43
|
+
.describe("The API root, e.g. https://api.example.com or https://api.example.com/v1. Requests go to a path under it.");
|
|
44
|
+
const bindingTemplateSchema = z.string().min(1).max(PLATFORM_CUSTOM_ENDPOINT_LIMITS.templateChars)
|
|
45
|
+
.refine(value => value.includes(PLATFORM_CUSTOM_ENDPOINT_SECRET_PLACEHOLDER) && value.replaceAll(PLATFORM_CUSTOM_ENDPOINT_SECRET_PLACEHOLDER, "").search(/[{}]/) === -1 && !CONTROL.test(value), "The template contains {secret} and no other braces or control characters");
|
|
46
|
+
const bindingNameCheck = (binding) => binding.location === "header"
|
|
47
|
+
? HEADER_NAME.test(binding.name) && !PLATFORM_APP_RESERVED_HEADERS.has(binding.name.toLowerCase()) && !FORBIDDEN_BINDING_HEADER.test(binding.name)
|
|
48
|
+
: QUERY_NAME.test(binding.name);
|
|
49
|
+
const bindingLocation = platformCustomEndpointLocationSchema.describe("header or query: where Relvo puts the credential on every request.");
|
|
50
|
+
const bindingName = z.string().min(1).max(PLATFORM_CUSTOM_ENDPOINT_LIMITS.nameChars).describe("The header or query parameter name, e.g. Authorization or api_key.");
|
|
51
|
+
export const platformCustomEndpointBindingInputSchema = z.object({
|
|
52
|
+
id: z.string().regex(/^[a-z0-9]{8,32}$/).optional().describe("Only when editing: the id of a saved binding whose secret is kept when no new secret is sent."),
|
|
53
|
+
location: bindingLocation,
|
|
54
|
+
name: bindingName,
|
|
55
|
+
template: bindingTemplateSchema.default(PLATFORM_CUSTOM_ENDPOINT_SECRET_PLACEHOLDER).describe("The value sent, with {secret} where the secret goes, e.g. Bearer {secret}."),
|
|
56
|
+
secret: z.string().min(1).max(PLATFORM_CUSTOM_ENDPOINT_LIMITS.secretChars).refine(value => !CONTROL.test(value), "The secret has no control characters").optional()
|
|
57
|
+
.describe("The credential. Required for a new binding without secretRef; sent again only to replace it. Relvo never returns it."),
|
|
58
|
+
secretRef: platformTeamSecretNameSchema.optional().describe("The name of a saved team secret to use instead of an inline secret. Only the console may set it."),
|
|
59
|
+
}).strict().refine(bindingNameCheck, "A valid header or query parameter name that is not reserved")
|
|
60
|
+
.refine(binding => binding.id !== undefined || binding.secret !== undefined || binding.secretRef !== undefined, "A new binding needs its secret")
|
|
61
|
+
.refine(binding => binding.secret === undefined || binding.secretRef === undefined, "A binding takes a secret or a secretRef, not both");
|
|
62
|
+
export const platformCustomEndpointBindingSchema = z.object({ id: z.string(), location: bindingLocation, name: bindingName, template: bindingTemplateSchema,
|
|
63
|
+
maskedSecret: z.string().nullable(), secretRef: platformTeamSecretNameSchema.nullable().default(null) }).strict();
|
|
64
|
+
export const isPlatformCustomEndpointFixedName = (location, name) => location === "header"
|
|
65
|
+
? isPlatformCallerHeaderName(name) && !/^(?:keep-alive|trailer)$/i.test(name)
|
|
66
|
+
: name.length >= 1 && name.length <= PLATFORM_CUSTOM_ENDPOINT_LIMITS.nameChars && !CONTROL.test(name);
|
|
67
|
+
export const isPlatformCustomEndpointFixedValue = (location, value) => value.length >= 1 && value.length <= PLATFORM_CUSTOM_ENDPOINT_LIMITS.valueChars
|
|
68
|
+
&& !CONTROL.test(value) && (location === "query" || isPlatformHeaderValue(value));
|
|
69
|
+
const fixedValuesSchema = (location) => z.record(z.string(), z.string())
|
|
70
|
+
.refine(values => Object.keys(values).length <= PLATFORM_CUSTOM_ENDPOINT_LIMITS[location === "header" ? "headers" : "query"], "Too many fixed values")
|
|
71
|
+
.refine(values => Object.entries(values).every(([name, value]) => isPlatformCustomEndpointFixedName(location, name) && isPlatformCustomEndpointFixedValue(location, value)), `${location === "header" ? "A header a caller may set" : "A name without control characters"}, with a value of 1–${PLATFORM_CUSTOM_ENDPOINT_LIMITS.valueChars} characters that can be sent`)
|
|
72
|
+
.refine(values => new Set(Object.keys(values).map(name => name.toLowerCase())).size === Object.keys(values).length, "Each name once, in any case");
|
|
73
|
+
export const platformCustomEndpointHeadersSchema = fixedValuesSchema("header")
|
|
74
|
+
.describe("Fixed, non-secret headers Relvo adds to every request, e.g. {\"X-Api-Version\":\"2024-06-01\"}; anyone who can read the tool sees them, so a credential goes in a binding. A request that sets one of these names is refused.");
|
|
75
|
+
export const platformCustomEndpointQuerySchema = fixedValuesSchema("query")
|
|
76
|
+
.describe("Fixed, non-secret query parameters Relvo adds to every request, e.g. {\"format\":\"json\"}; anyone who can read the tool sees them, so a credential goes in a binding. A request that sets one of these names is refused.");
|
|
77
|
+
export const platformCustomEndpointSchema = z.object({ baseUrl: platformCustomEndpointBaseUrlSchema,
|
|
78
|
+
headers: platformCustomEndpointHeadersSchema.optional(), query: platformCustomEndpointQuerySchema.optional(),
|
|
79
|
+
bindings: z.array(platformCustomEndpointBindingSchema).max(PLATFORM_CUSTOM_ENDPOINT_LIMITS.bindings) }).strict();
|
|
80
|
+
const endpointResponseMiB = z.number().int().min(PLATFORM_CUSTOM_ENDPOINT_BOUNDS.responseMiB.standard).max(PLATFORM_CUSTOM_ENDPOINT_BOUNDS.responseMiB.maximum);
|
|
81
|
+
export const platformStoredCustomEndpointSchema = platformCustomEndpointSchema.extend({ responseMiB: endpointResponseMiB.optional() }).strict();
|
|
82
|
+
export const platformCustomToolSlugSchema = z.string().max(PLATFORM_CUSTOM_TOOL_LIMITS.slugChars).regex(/^[a-z][a-z0-9_]+$/)
|
|
83
|
+
.describe("Lowercase letters, digits and underscores, starting with a letter; the tool is named custom__<slug>.");
|
|
84
|
+
export const PLATFORM_CUSTOM_TOOL_NAME = /^custom__[a-z][a-z0-9_]{1,39}$/;
|
|
85
|
+
export const platformCustomToolNameSchema = z.string().regex(PLATFORM_CUSTOM_TOOL_NAME);
|
|
86
|
+
export const isPlatformCustomToolName = (name) => PLATFORM_CUSTOM_TOOL_NAME.test(name);
|
|
87
|
+
export const platformCustomToolInputSchemaSchema = platformJsonObjectSchema
|
|
88
|
+
.refine(schema => schema.type === "object" && schema.properties !== null && typeof schema.properties === "object" && !Array.isArray(schema.properties), "The input schema is an object schema with properties")
|
|
89
|
+
.refine(schema => new TextEncoder().encode(JSON.stringify(schema)).byteLength <= PLATFORM_CUSTOM_TOOL_LIMITS.schemaBytes, "The input schema is too large");
|
|
90
|
+
export const platformCustomToolDefinitionSchema = z.object({
|
|
91
|
+
kind: z.literal("python").default("python"),
|
|
92
|
+
title: z.string().trim().min(1).max(PLATFORM_CUSTOM_TOOL_LIMITS.titleChars),
|
|
93
|
+
description: z.string().trim().min(1).max(PLATFORM_CUSTOM_TOOL_LIMITS.descriptionChars).describe("What the tool does and returns; agents choose tools by it."),
|
|
94
|
+
inputSchema: platformCustomToolInputSchemaSchema.describe("JSON Schema of the input object, e.g. {\"type\":\"object\",\"properties\":{\"days\":{\"type\":\"integer\"}},\"required\":[\"days\"]}."),
|
|
95
|
+
code: z.string().min(1).max(PLATFORM_CUSTOM_TOOL_LIMITS.codeChars)
|
|
96
|
+
.describe("Python. The validated input is the dict `input`; print the result (JSON is easiest to read back). Files of sourceExecutionIds are listed in /home/user/tool-results/manifest.json. No credentials are present."),
|
|
97
|
+
timeoutMs: z.number().int().min(1000).max(PLATFORM_SANDBOX_LIMITS.timeoutMs).default(30_000),
|
|
98
|
+
enabled: z.boolean().default(true),
|
|
99
|
+
}).strict();
|
|
100
|
+
const uniqueBindings = (bindings) => new Set(bindings.map(binding => `${binding.location}:${binding.name.toLowerCase()}`)).size === bindings.length;
|
|
101
|
+
export const platformCustomEndpointDefinitionSchema = z.object({
|
|
102
|
+
kind: z.literal("http"),
|
|
103
|
+
title: z.string().trim().min(1).max(PLATFORM_CUSTOM_TOOL_LIMITS.titleChars),
|
|
104
|
+
description: z.string().trim().min(1).max(PLATFORM_CUSTOM_TOOL_LIMITS.descriptionChars).describe("What the API is and when to use it; agents choose tools by it."),
|
|
105
|
+
baseUrl: platformCustomEndpointBaseUrlSchema,
|
|
106
|
+
headers: platformCustomEndpointHeadersSchema.optional(),
|
|
107
|
+
query: platformCustomEndpointQuerySchema.optional(),
|
|
108
|
+
bindings: z.array(platformCustomEndpointBindingInputSchema).max(PLATFORM_CUSTOM_ENDPOINT_LIMITS.bindings).refine(uniqueBindings, "Each header or query parameter is bound once").default([])
|
|
109
|
+
.describe("Credentials Relvo adds to every request, in the header or query parameter named. Agents never see them. Empty for an API that needs none."),
|
|
110
|
+
timeoutMs: z.number().int().min(PLATFORM_CUSTOM_ENDPOINT_BOUNDS.timeoutMs.standard).max(PLATFORM_CUSTOM_ENDPOINT_BOUNDS.timeoutMs.maximum).default(PLATFORM_CUSTOM_ENDPOINT_BOUNDS.timeoutMs.standard)
|
|
111
|
+
.describe("How long one call may take, in milliseconds, 5000 to 300000. Above 5000 every call is metered: USD 0.0003 for each started 5 seconds after the first 5 and each started 256 KiB after the first 1 MiB, with both limits reserved before it runs."),
|
|
112
|
+
responseMiB: endpointResponseMiB.default(PLATFORM_CUSTOM_ENDPOINT_BOUNDS.responseMiB.standard)
|
|
113
|
+
.describe("The largest response a call keeps, in MiB, 1 to 14. Above 1 every call is metered as for timeoutMs."),
|
|
114
|
+
enabled: z.boolean().default(true),
|
|
115
|
+
}).strict();
|
|
116
|
+
const fixedApartFromBindings = (endpoint) => endpoint.bindings.every(binding => !Object.keys((binding.location === "header" ? endpoint.headers : endpoint.query) ?? {}).some(name => name.toLowerCase() === binding.name.toLowerCase()));
|
|
117
|
+
const apart = { message: "A header or query parameter is either fixed or bound, not both", path: ["bindings"] };
|
|
118
|
+
const slugged = { slug: platformCustomToolSlugSchema };
|
|
119
|
+
const versioned = { expectedVersion: z.number().int().min(1) };
|
|
120
|
+
export const platformCustomToolCreateSchema = z.union([platformCustomToolDefinitionSchema.extend(slugged).strict(),
|
|
121
|
+
platformCustomEndpointDefinitionSchema.extend(slugged).strict().refine(fixedApartFromBindings, apart)]);
|
|
122
|
+
export const platformCustomToolUpdateSchema = z.union([platformCustomToolDefinitionSchema.extend(versioned).strict(),
|
|
123
|
+
platformCustomEndpointDefinitionSchema.extend(versioned).strict().refine(fixedApartFromBindings, apart)]);
|
|
124
|
+
export const platformCustomToolRestoreSchema = z.object({ version: z.number().int().min(1), expectedVersion: z.number().int().min(1) }).strict();
|
|
125
|
+
export const platformCustomToolSchema = z.object({
|
|
126
|
+
kind: platformCustomToolKindSchema.default("python"), endpoint: platformCustomEndpointSchema.nullable().default(null),
|
|
127
|
+
name: platformCustomToolNameSchema, slug: platformCustomToolSlugSchema, title: z.string(), description: z.string(), inputSchema: platformJsonObjectSchema,
|
|
128
|
+
code: z.string(), timeoutMs: z.number().int(), responseMiB: endpointResponseMiB.default(PLATFORM_CUSTOM_ENDPOINT_BOUNDS.responseMiB.standard),
|
|
129
|
+
enabled: z.boolean(), version: z.number().int().min(1), createdAt: z.string(), updatedAt: z.string(),
|
|
130
|
+
});
|
|
131
|
+
export const platformCustomToolSummarySchema = platformCustomToolSchema.omit({ code: true, inputSchema: true });
|
|
132
|
+
export const platformCustomToolVersionSchema = z.object({ version: z.number().int().min(1), title: z.string(), createdAt: z.string() });
|
|
133
|
+
export const platformVersionAuthorSchema = z.object({ email: z.string().nullable(), apiKey: z.string().nullable() }).strict();
|
|
134
|
+
export const platformCustomToolHistorySchema = platformCustomToolVersionSchema.extend({ author: platformVersionAuthorSchema });
|
|
135
|
+
export const platformCustomToolVersionDetailSchema = platformCustomToolSchema.pick({ kind: true, endpoint: true, title: true, description: true, inputSchema: true, code: true, timeoutMs: true, responseMiB: true, enabled: true })
|
|
136
|
+
.extend({ version: z.number().int().min(1), createdAt: z.string(), author: platformVersionAuthorSchema });
|
|
137
|
+
const toolInput = projectScopeSchema.extend({ name: platformCustomToolNameSchema });
|
|
138
|
+
const toolResult = z.object({ tool: platformCustomToolSchema }).strict();
|
|
139
|
+
export const platformCustomToolContracts = {
|
|
140
|
+
listDeveloperCustomTools: { input: projectScopeSchema.strict(), output: z.object({ tools: z.array(platformCustomToolSummarySchema) }).strict() },
|
|
141
|
+
getDeveloperCustomTool: { input: toolInput.strict(), output: toolResult },
|
|
142
|
+
createDeveloperCustomTool: { input: projectScopeSchema.extend({ tool: platformCustomToolCreateSchema }).strict(), output: toolResult },
|
|
143
|
+
updateDeveloperCustomTool: { input: toolInput.extend({ tool: platformCustomToolUpdateSchema }).strict(), output: toolResult },
|
|
144
|
+
restoreDeveloperCustomTool: { input: toolInput.merge(platformCustomToolRestoreSchema).strict(), output: toolResult },
|
|
145
|
+
deleteDeveloperCustomTool: { input: toolInput.strict(), output: z.object({ success: z.literal(true) }).strict() },
|
|
146
|
+
listDeveloperCustomToolVersions: { input: toolInput.strict(), output: z.object({ versions: z.array(platformCustomToolHistorySchema) }).strict() },
|
|
147
|
+
getDeveloperCustomToolVersion: { input: toolInput.extend({ version: z.number().int().min(1) }).strict(), output: z.object({ version: platformCustomToolVersionDetailSchema }).strict() },
|
|
148
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PLATFORM_APP_LIMITS, platformAppOAuthClientSchema, platformAppScopesSchema } from "./platformAppAuth.js";
|
|
3
|
+
import { platformAppIdSchema, platformConnectionCatalogQuerySchema, platformAnyConnectionProviderSchema, PLATFORM_CONNECTION_PAGE_MAX } from "./platformConnectionCatalog.js";
|
|
4
|
+
import { platformConnectionOperationsSchema } from "./platformConnectionPolicy.js";
|
|
5
|
+
import { platformToolNameSchema } from "./platformPublicExecution.js";
|
|
6
|
+
import { PLATFORM_API_KEY_PATTERN } from "./platformApiKey.js";
|
|
7
|
+
import { USAGE_BREAKDOWN_MAX_DAYS } from "./platformBillingConstants.js";
|
|
8
|
+
export const PLATFORM_CONNECT_LINK_LIMITS = { lifetimeMinutes: 60, activePerProject: 100, fields: PLATFORM_APP_LIMITS.fields, callbackChars: 2048 };
|
|
9
|
+
export const PLATFORM_END_USER_LIMITS = { idChars: 128, sessionMinutes: 60, tools: 500, usageOperations: 100 };
|
|
10
|
+
export const PLATFORM_END_USER_ROUTES = {
|
|
11
|
+
links: "/v1/connect-links", sessions: "/v1/sessions", session: "/v1/sessions/:id{[a-f0-9-]{36}}",
|
|
12
|
+
revokeSession: "/v1/sessions/:id{[a-f0-9-]{36}}/revoke", users: "/v1/users", setUser: "/v1/users/update",
|
|
13
|
+
restoreSession: "/v1/sessions/restore", userUsage: "/v1/users/:userId{[A-Za-z0-9._:@%-]+}/usage",
|
|
14
|
+
linkStatus: "/v1/connect-links/status", revokeLink: "/v1/connect-links/revoke",
|
|
15
|
+
connections: "/v1/users/:userId{[A-Za-z0-9._:@%-]+}/connections", disconnect: "/v1/users/connections/disconnect",
|
|
16
|
+
};
|
|
17
|
+
export const platformEndUserIdSchema = z.string().trim().min(1).max(PLATFORM_END_USER_LIMITS.idChars).regex(/^[A-Za-z0-9._:@-]+$/);
|
|
18
|
+
export const platformConnectCallbackUrlSchema = z.url().max(PLATFORM_CONNECT_LINK_LIMITS.callbackChars).refine(value => {
|
|
19
|
+
const url = new URL(value);
|
|
20
|
+
return !url.username && !url.password && (url.protocol === "https:" || url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname));
|
|
21
|
+
});
|
|
22
|
+
export const platformConnectMethodSchema = z.enum(["oauth2", "oauth1", "mcp_oauth", "key", "client_credentials", "service_account"]);
|
|
23
|
+
export const platformConnectLinkDefinitionSchema = z.object({
|
|
24
|
+
appId: platformAppIdSchema, method: platformConnectMethodSchema,
|
|
25
|
+
oauthClient: platformAppOAuthClientSchema.optional(), scopes: platformAppScopesSchema.optional(),
|
|
26
|
+
operations: platformConnectionOperationsSchema.optional(),
|
|
27
|
+
}).strict();
|
|
28
|
+
export const platformConnectLinkTokenSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
29
|
+
const numericQuery = (value) => typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
30
|
+
const customerConnection = z.object({ id: z.string(), provider: platformAnyConnectionProviderSchema, label: z.string(),
|
|
31
|
+
status: z.enum(["connected", "reauth_required"]), scopes: z.array(z.string()), createdAt: z.string() });
|
|
32
|
+
export const platformEndUserContracts = {
|
|
33
|
+
createLink: { input: z.object({ userId: platformEndUserIdSchema, connection: platformConnectLinkDefinitionSchema, callbackUrl: platformConnectCallbackUrlSchema.optional(),
|
|
34
|
+
expiresInMinutes: z.number().int().min(1).max(PLATFORM_CONNECT_LINK_LIMITS.lifetimeMinutes).default(30) }).strict(), output: z.object({ url: z.url(), expiresAt: z.iso.datetime() }) },
|
|
35
|
+
linkStatus: { input: z.object({ token: platformConnectLinkTokenSchema }).strict(), output: z.object({ userId: platformEndUserIdSchema, appId: platformAppIdSchema, expiresAt: z.iso.datetime(),
|
|
36
|
+
status: z.enum(["pending", "authorizing", "selecting", "completed", "failed", "expired", "revoked", "unavailable"]), connectionId: z.string().nullable() }) },
|
|
37
|
+
revokeLink: { input: z.object({ token: platformConnectLinkTokenSchema }).strict(), output: z.object({ success: z.literal(true) }) },
|
|
38
|
+
connections: { input: platformConnectionCatalogQuerySchema.omit({ provider: true }).extend({ userId: platformEndUserIdSchema,
|
|
39
|
+
offset: z.preprocess(numericQuery, platformConnectionCatalogQuerySchema.shape.offset), limit: z.preprocess(numericQuery, platformConnectionCatalogQuerySchema.shape.limit) }).strict(),
|
|
40
|
+
output: z.object({ connections: z.array(customerConnection).max(PLATFORM_CONNECTION_PAGE_MAX), total: z.number().int().nonnegative(), nextOffset: z.number().int().nonnegative().nullable() }) },
|
|
41
|
+
disconnect: { input: z.object({ userId: platformEndUserIdSchema, connectionId: z.string().min(1).max(128) }).strict(), output: z.object({ success: z.literal(true) }) },
|
|
42
|
+
createSession: { input: z.object({ userId: platformEndUserIdSchema, tools: z.array(platformToolNameSchema).max(PLATFORM_END_USER_LIMITS.tools).optional(),
|
|
43
|
+
expiresInMinutes: z.number().int().min(1).max(PLATFORM_END_USER_LIMITS.sessionMinutes).default(30) }).strict(), output: z.object({
|
|
44
|
+
id: z.uuid(), userId: platformEndUserIdSchema, expiresAt: z.iso.datetime(), mcp: z.object({ url: z.url(), headers: z.object({ Authorization: z.string() }) }),
|
|
45
|
+
}) },
|
|
46
|
+
session: { input: z.object({ id: z.uuid() }).strict(), output: z.object({ id: z.uuid(), userId: platformEndUserIdSchema, expiresAt: z.iso.datetime(), active: z.boolean() }) },
|
|
47
|
+
revokeSession: { input: z.object({ id: z.uuid() }).strict(), output: z.object({ success: z.literal(true) }) },
|
|
48
|
+
users: { input: z.object({ limit: z.coerce.number().int().min(1).max(100).default(30), after: platformEndUserIdSchema.optional() }).strict(),
|
|
49
|
+
output: z.object({ users: z.array(z.object({ id: platformEndUserIdSchema, active: z.boolean(), connections: z.number().int().nonnegative() })), nextCursor: z.string().nullable() }) },
|
|
50
|
+
setUser: { input: z.object({ userId: platformEndUserIdSchema, active: z.boolean() }).strict(), output: z.object({ success: z.literal(true) }) },
|
|
51
|
+
restoreSession: { input: z.object({ id: z.uuid(), token: z.string().regex(PLATFORM_API_KEY_PATTERN) }).strict(),
|
|
52
|
+
output: z.object({ id: z.uuid(), userId: platformEndUserIdSchema, expiresAt: z.iso.datetime(), mcp: z.object({ url: z.url(), headers: z.object({ Authorization: z.string() }) }) }) },
|
|
53
|
+
};
|
|
54
|
+
const usageTotals = z.object({ requests: z.number().int().nonnegative(), succeeded: z.number().int().nonnegative(), failed: z.number().int().nonnegative(),
|
|
55
|
+
pending: z.number().int().nonnegative(), chargedMicros: z.number().int().nonnegative(), reservedMicros: z.number().int().nonnegative() });
|
|
56
|
+
export const platformEndUserUsageContract = {
|
|
57
|
+
input: z.object({ userId: platformEndUserIdSchema, from: z.iso.date(), to: z.iso.date() }).strict().refine(value => value.from >= "0001-01-01" && value.from <= value.to
|
|
58
|
+
&& (Date.parse(value.to) - Date.parse(value.from)) / 86_400_000 < USAGE_BREAKDOWN_MAX_DAYS),
|
|
59
|
+
output: z.object({ userId: platformEndUserIdSchema, from: z.iso.date(), to: z.iso.date(), currency: z.literal("usd"), summary: usageTotals,
|
|
60
|
+
daily: z.array(usageTotals.extend({ day: z.iso.date() })).max(USAGE_BREAKDOWN_MAX_DAYS),
|
|
61
|
+
byOperation: z.object({ rows: z.array(usageTotals.extend({ operation: z.string() })).max(PLATFORM_END_USER_LIMITS.usageOperations), other: usageTotals.nullable() }) }),
|
|
62
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PLATFORM_CHECKOUT_MIN_CENTS } from "./platformBillingConstants.js";
|
|
3
|
+
const micros = z.string().regex(/^\d{1,20}$/);
|
|
4
|
+
export const platformFundingShortfallSchema = z.object({
|
|
5
|
+
reason: z.enum(["balance", "organization_monthly_limit", "project_monthly_limit", "member_monthly_limit", "organization_daily_limit", "api_key_daily_limit"]),
|
|
6
|
+
requiredMicros: micros,
|
|
7
|
+
availableMicros: micros,
|
|
8
|
+
});
|
|
9
|
+
export const platformFundingRecoverySchema = z.object({ billingUrl: z.url({ protocol: /^https?$/ }), shortfall: platformFundingShortfallSchema.optional() });
|
|
10
|
+
export const PLATFORM_WALLET_TOP_UP_MIN_CENTS = PLATFORM_CHECKOUT_MIN_CENTS;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ACCOUNT_REQUEST_HTTP_METHODS } from "./accountRequestMethods.js";
|
|
3
|
+
export const PLATFORM_APP_HOST_PATTERN = /^(?=[^:]{4,253}(?::|$))(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9](?::[1-9][0-9]{0,4})?$/;
|
|
4
|
+
export const PLATFORM_APP_RESERVED_HEADERS = new Set(["host", "cookie", "content-length", "transfer-encoding", "connection", "proxy-authorization", "forwarded", "te", "upgrade", "expect"]);
|
|
5
|
+
export const PLATFORM_CREDENTIAL_HEADER = /^(?:authorization|x-api-key|api-key|apikey|x-auth-token|x-access-token|cookie|set-cookie|proxy-.*|x-forwarded-.*|x-real-ip)$/i;
|
|
6
|
+
export const isPlatformCallerHeaderName = (name) => /^[A-Za-z0-9_-]{1,64}$/.test(name) && !PLATFORM_APP_RESERVED_HEADERS.has(name.toLowerCase())
|
|
7
|
+
&& !PLATFORM_CREDENTIAL_HEADER.test(name) && !/^(?:x-http-method-override|x-http-method|x-method-override)$/i.test(name);
|
|
8
|
+
export const isPlatformHeaderValue = (value) => !/[\r\n\u0000]|[^\x00-\xff]/.test(value);
|
|
9
|
+
export const PLATFORM_APP_MAX_BASE_URLS = 32;
|
|
10
|
+
export const platformAppApiPathSchema = z.object({ baseUrl: z.number().int().min(0).max(PLATFORM_APP_MAX_BASE_URLS), method: z.enum(ACCOUNT_REQUEST_HTTP_METHODS),
|
|
11
|
+
path: z.string().startsWith("/").max(2048).refine(path => !/[?#\\\s\u0000-\u001f]/.test(path) && !path.includes("//")
|
|
12
|
+
&& path.split("/").every(segment => segment !== "." && segment !== ".." && (!/[{}]/.test(segment) || /^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(segment)))) }).strict();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { developerKeyScopeSchema } from "./platformPublicExecution.js";
|
|
3
|
+
import { PLATFORM_API_KEY_PATTERN } from "./platformApiKey.js";
|
|
4
|
+
export const PLATFORM_LOCAL_SETUP_TTL_SECONDS = 600;
|
|
5
|
+
export const PLATFORM_LOCAL_SETUP_INTERVAL_SECONDS = 5;
|
|
6
|
+
export const platformLocalSetupClientSchema = z.enum(["claude", "codex", "agents", "terminal"]);
|
|
7
|
+
export const platformLocalSetupAuthorizationSchema = z.string().min(1).max(4096);
|
|
8
|
+
export const platformLocalSetupStartInputSchema = z.object({ client: platformLocalSetupClientSchema }).strict();
|
|
9
|
+
export const platformLocalSetupProofSchema = z.object({ authorizationRequest: platformLocalSetupAuthorizationSchema,
|
|
10
|
+
deviceCode: z.string().regex(/^[A-Za-z0-9_-]{43}$/) }).strict();
|
|
11
|
+
export const platformLocalSetupStartSchema = z.object({ authorizationRequest: platformLocalSetupAuthorizationSchema,
|
|
12
|
+
deviceCode: platformLocalSetupProofSchema.shape.deviceCode, userCode: z.string().regex(/^[A-Z2-9]{4}-[A-Z2-9]{4}$/),
|
|
13
|
+
verificationUri: z.url(), expiresAt: z.iso.datetime(), intervalSeconds: z.literal(PLATFORM_LOCAL_SETUP_INTERVAL_SECONDS) }).strict();
|
|
14
|
+
const grantMetadata = { apiKeyId: z.string(), developerOrganizationId: z.string(), developerProjectId: z.string(),
|
|
15
|
+
scopes: z.array(developerKeyScopeSchema).min(1).max(5), expiresAt: z.iso.datetime(), approvedExpiresAt: z.iso.datetime() };
|
|
16
|
+
export const platformLocalSetupAcknowledgedSchema = z.object({ status: z.literal("acknowledged"), ...grantMetadata }).strict();
|
|
17
|
+
export const platformLocalSetupPollSchema = z.discriminatedUnion("status", [
|
|
18
|
+
z.object({ status: z.literal("pending"), expiresAt: z.iso.datetime(), intervalSeconds: z.literal(PLATFORM_LOCAL_SETUP_INTERVAL_SECONDS) }).strict(),
|
|
19
|
+
z.object({ status: z.literal("approved"), apiKey: z.string().regex(PLATFORM_API_KEY_PATTERN), ...grantMetadata }).strict(),
|
|
20
|
+
platformLocalSetupAcknowledgedSchema,
|
|
21
|
+
]);
|
|
22
|
+
export const platformLocalSetupMetadataSchema = z.object({ client: platformLocalSetupClientSchema,
|
|
23
|
+
expiresAt: z.iso.datetime(), status: z.enum(["pending", "approved", "acknowledged"]),
|
|
24
|
+
apiKeyId: z.string().optional(), developerOrganizationId: z.string().optional(), developerProjectId: z.string().optional(),
|
|
25
|
+
scopes: z.array(developerKeyScopeSchema).optional(), approvedExpiresAt: z.iso.datetime().optional(), requestedFrom: z.string().optional() }).strict();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const PLATFORM_MCP_DIRECT_TOOLS_MAX = 20;
|
|
2
|
+
export function platformMcpDirectToolNames(list) {
|
|
3
|
+
const names = [...new Set((list ?? "").split(",").map(name => name.trim()).filter(Boolean))];
|
|
4
|
+
if (names.length > PLATFORM_MCP_DIRECT_TOOLS_MAX || names.some(name => !/^[a-z][a-z0-9_]{1,63}$/u.test(name)))
|
|
5
|
+
throw new Error(`Name at most ${PLATFORM_MCP_DIRECT_TOOLS_MAX} catalog tools, separated by commas.`);
|
|
6
|
+
return names;
|
|
7
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { platformCatalogSchema, platformCreateExecutionSchema, platformExecutionIdSchema, platformExecutionStatusSchema, platformMicrosSchema, platformQuoteSchema, platformToolNameSchema, platformToolSessionIdSchema, platformOptionalToolSessionIdSchema, PLATFORM_SANDBOX_LIMITS, platformExecutionSchema, platformExecutionContextSchema, platformRequestIntentSchema, validatePlatformArgumentIdentifiers, PLATFORM_TOOL_REQUEST_MAX_BYTES } from "./platformPublicExecution.js";
|
|
3
|
+
import { platformAnyConnectionProviderSchema, platformConnectionCatalogSchema } from "./platformConnectionCatalog.js";
|
|
4
|
+
import { toolDiscoveryInputSchema, toolDiscoveryResultSchema } from "./toolDiscovery.js";
|
|
5
|
+
import { platformFundingRecoverySchema } from "./platformFunding.js";
|
|
6
|
+
export { PLATFORM_MCP_DIRECT_TOOLS_MAX, platformMcpDirectToolNames } from "./platformMcpTools.js";
|
|
7
|
+
export const PLATFORM_META_LIMITS = { queries: 4, schemas: 12, batch: 50, concurrency: 4, contextBytes: 24_000,
|
|
8
|
+
requestBytes: PLATFORM_TOOL_REQUEST_MAX_BYTES, requestTimeoutMs: 90_000, sessionLifetimeMs: 86_400_000, activeSessions: 100,
|
|
9
|
+
codeChars: PLATFORM_SANDBOX_LIMITS.code, commandChars: 16_000, sourceExecutions: PLATFORM_SANDBOX_LIMITS.sourceExecutions, timeoutMs: PLATFORM_SANDBOX_LIMITS.timeoutMs };
|
|
10
|
+
export const isSessionSandboxTool = (name) => name === "remote_workbench" || name === "remote_bash";
|
|
11
|
+
export const platformToolSessionSchema = z.object({ id: platformToolSessionIdSchema, expiresAt: z.iso.datetime(),
|
|
12
|
+
sandbox: z.enum(["new", "ready", "busy", "needs_review", "state_lost"]), activeExecutionId: platformExecutionIdSchema.nullable() });
|
|
13
|
+
const sessionInput = { sessionId: platformToolSessionIdSchema };
|
|
14
|
+
export const platformMetaSearchSchema = z.object({ sessionId: platformOptionalToolSessionIdSchema.describe("Continue a returned session. Omit or use an empty string to open a new session."),
|
|
15
|
+
intent: platformRequestIntentSchema,
|
|
16
|
+
queries: z.array(toolDiscoveryInputSchema).min(1).max(PLATFORM_META_LIMITS.queries)
|
|
17
|
+
.describe("Independent capability queries, one action per query. Use exact app names when known; omit private account data.") }).strict();
|
|
18
|
+
export const platformMetaSearchResultSchema = z.object({ session: platformToolSessionSchema,
|
|
19
|
+
results: z.array(z.object({ query: z.string().optional(), ...toolDiscoveryResultSchema.shape })),
|
|
20
|
+
nextSteps: z.array(z.enum(["inspect_schemas", "check_connections", "quote", "execute_independent", "process_results"])) });
|
|
21
|
+
export const platformMetaSchemasSchema = z.object({ ...sessionInput,
|
|
22
|
+
tools: z.array(platformToolNameSchema).min(1).max(PLATFORM_META_LIMITS.schemas).refine(names => new Set(names).size === names.length, "Tool names must be unique") }).strict();
|
|
23
|
+
export const platformMetaSchemasResultSchema = z.object({ session: platformToolSessionSchema,
|
|
24
|
+
tools: platformCatalogSchema.shape.tools, remaining: z.array(platformToolNameSchema) });
|
|
25
|
+
export const platformMetaConnectionsSchema = z.object({ ...sessionInput,
|
|
26
|
+
providers: z.array(platformAnyConnectionProviderSchema).max(32).default([])
|
|
27
|
+
.describe("Providers to inspect: OAuth providers or connected apps as app:<id> (e.g. app:airtable). Empty means every connection. Data-provider API keys are managed by Relvo.") }).strict();
|
|
28
|
+
export const platformMetaConnectionsResultSchema = z.object({ session: platformToolSessionSchema,
|
|
29
|
+
developerOrganizationId: z.string(), developerProjectId: z.string(),
|
|
30
|
+
manageUrl: z.url().nullable(), connections: platformConnectionCatalogSchema.shape.connections,
|
|
31
|
+
total: z.number().int().nonnegative(), providers: z.array(z.object({ provider: platformAnyConnectionProviderSchema,
|
|
32
|
+
status: z.enum(["connected", "reauth_required", "not_connected"]) })),
|
|
33
|
+
instruction: z.string() });
|
|
34
|
+
const item = z.object(platformCreateExecutionSchema.shape).omit({ sessionId: true }).extend({ idempotencyKey: z.string().regex(/^[A-Za-z0-9._:-]{8,128}$/) }).strict()
|
|
35
|
+
.superRefine((value, context) => validatePlatformArgumentIdentifiers(value.tool, value.arguments, context));
|
|
36
|
+
export const platformMetaExecuteSchema = z.object({ ...sessionInput,
|
|
37
|
+
maximumChargeMicros: platformMicrosSchema.describe("Accepted total reservation ceiling across this batch, in addition to each accepted quote."),
|
|
38
|
+
tools: z.array(item).min(1).max(PLATFORM_META_LIMITS.batch)
|
|
39
|
+
.describe("Logically independent, quoted calls only. Dependent calls must wait for their inputs in a later batch.") }).strict().superRefine((value, ctx) => {
|
|
40
|
+
if (new Set(value.tools.map(tool => tool.idempotencyKey)).size !== value.tools.length)
|
|
41
|
+
ctx.addIssue({ code: "custom", path: ["tools"], message: "Each call needs its own retained idempotency key" });
|
|
42
|
+
if (value.tools.reduce((total, tool) => total + tool.maximumChargeMicros, 0) > value.maximumChargeMicros)
|
|
43
|
+
ctx.addIssue({ code: "custom", path: ["maximumChargeMicros"], message: "Batch exceeds the accepted reservation ceiling" });
|
|
44
|
+
});
|
|
45
|
+
export const platformMetaExecuteResultSchema = z.object({ session: platformToolSessionSchema,
|
|
46
|
+
allAccepted: z.boolean(),
|
|
47
|
+
results: z.array(z.object({ index: z.number().int().nonnegative(), tool: platformToolNameSchema, idempotencyKey: z.string(),
|
|
48
|
+
admission: z.enum(["accepted", "rejected", "unknown"]), executionId: platformExecutionIdSchema.optional(),
|
|
49
|
+
status: platformExecutionStatusSchema.optional(), reservedMicros: platformMicrosSchema.optional(),
|
|
50
|
+
errorCode: z.string().optional(), fundingRecovery: platformFundingRecoverySchema.optional() })),
|
|
51
|
+
instruction: z.string() });
|
|
52
|
+
const remote = { ...sessionInput, timeoutMs: z.number().int().min(1000).max(PLATFORM_META_LIMITS.timeoutMs).default(30_000),
|
|
53
|
+
intent: platformRequestIntentSchema,
|
|
54
|
+
sourceExecutionIds: z.array(platformExecutionIdSchema).max(PLATFORM_META_LIMITS.sourceExecutions).default([])
|
|
55
|
+
.refine(ids => new Set(ids).size === ids.length, "Source execution IDs must be unique"),
|
|
56
|
+
quote: platformQuoteSchema,
|
|
57
|
+
idempotencyKey: item.shape.idempotencyKey };
|
|
58
|
+
export const platformMetaWorkbenchSchema = z.object({ ...remote,
|
|
59
|
+
code: z.string().min(1).max(PLATFORM_META_LIMITS.codeChars).describe("Python cell in this session's remote sandbox. Variables and files persist. Process prior outputs via sourceExecutionIds; no provider credentials or direct tool proxy are present.") }).strict();
|
|
60
|
+
export const platformMetaBashSchema = z.object({ ...remote,
|
|
61
|
+
command: z.string().min(1).max(PLATFORM_META_LIMITS.commandChars).describe("Bash command in this session's remote sandbox, never on the user's machine. Shares files with Python. No provider credentials are present.") }).strict();
|
|
62
|
+
export const platformMetaRemoteResultSchema = z.object({ session: platformToolSessionSchema,
|
|
63
|
+
execution: z.union([platformExecutionContextSchema, platformExecutionSchema]) });
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isCronTooFrequent, isValidCron, parseCron } from "./cronValidator.js";
|
|
3
|
+
import { ScheduleTimeZone } from "./scheduleTimeZone.js";
|
|
4
|
+
export const PLATFORM_MONITOR_INTERVALS = { "15m": 900, "1h": 3600, "1d": 86_400 };
|
|
5
|
+
export const platformMonitorIntervalSchema = z.enum(["15m", "1h", "1d"]);
|
|
6
|
+
export const PLATFORM_MONITOR_DEFAULT_INTERVAL = "1h";
|
|
7
|
+
export const PLATFORM_MONITOR_DEFAULT_TIME_ZONE = "UTC";
|
|
8
|
+
export const PLATFORM_MONITOR_MIN_CRON_MINUTES = 15;
|
|
9
|
+
export const PLATFORM_MONITOR_CRON_CHARS = 100;
|
|
10
|
+
const cronText = z.string().trim().max(PLATFORM_MONITOR_CRON_CHARS)
|
|
11
|
+
.refine(isValidCron, "platform_monitor_cron_invalid")
|
|
12
|
+
.refine(cron => !isCronTooFrequent(cron, PLATFORM_MONITOR_MIN_CRON_MINUTES), "platform_monitor_cron_too_frequent");
|
|
13
|
+
export const platformMonitorCronSchema = z.union([z.literal(""), cronText]);
|
|
14
|
+
export const platformMonitorTimeZoneSchema = z.union([z.literal(""), ScheduleTimeZone]);
|
|
15
|
+
export const normalizeMonitorCron = (cron) => cron.trim().split(/\s+/).map(field => field.toUpperCase()).join(" ");
|
|
16
|
+
export class PlatformMonitorScheduleError extends Error {
|
|
17
|
+
}
|
|
18
|
+
export function resolvePlatformMonitorSchedule(input, after = Date.now()) {
|
|
19
|
+
if (!input.cron) {
|
|
20
|
+
if (input.timeZone)
|
|
21
|
+
throw new PlatformMonitorScheduleError("platform_monitor_time_zone_needs_cron");
|
|
22
|
+
return { interval: input.interval ?? PLATFORM_MONITOR_DEFAULT_INTERVAL };
|
|
23
|
+
}
|
|
24
|
+
if (input.interval)
|
|
25
|
+
throw new PlatformMonitorScheduleError("platform_monitor_schedule_conflict");
|
|
26
|
+
const zone = ScheduleTimeZone.safeParse(input.timeZone || PLATFORM_MONITOR_DEFAULT_TIME_ZONE);
|
|
27
|
+
if (!zone.success)
|
|
28
|
+
throw new PlatformMonitorScheduleError("platform_monitor_time_zone_invalid");
|
|
29
|
+
const cron = normalizeMonitorCron(input.cron), timeZone = zone.data;
|
|
30
|
+
if (nextCronFire(cron, timeZone, after) === null)
|
|
31
|
+
throw new PlatformMonitorScheduleError("platform_monitor_cron_never_runs");
|
|
32
|
+
return { cron, timeZone };
|
|
33
|
+
}
|
|
34
|
+
const MINUTE = 60_000, HOUR = 3_600_000, DAY = 86_400_000;
|
|
35
|
+
const SEARCH_DAYS = 366 * 8;
|
|
36
|
+
const formatters = new Map();
|
|
37
|
+
function offsetAt(timeZone, at) {
|
|
38
|
+
let format = formatters.get(timeZone);
|
|
39
|
+
if (!format) {
|
|
40
|
+
format = new Intl.DateTimeFormat("en-US", { timeZone, hourCycle: "h23", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric" });
|
|
41
|
+
formatters.set(timeZone, format);
|
|
42
|
+
}
|
|
43
|
+
const part = {};
|
|
44
|
+
for (const { type, value } of format.formatToParts(at))
|
|
45
|
+
if (type !== "literal")
|
|
46
|
+
part[type] = Number(value);
|
|
47
|
+
return Date.UTC(part.year, part.month - 1, part.day, part.hour, part.minute, part.second) - Math.floor(at / 1000) * 1000;
|
|
48
|
+
}
|
|
49
|
+
function instantOf(timeZone, wall) {
|
|
50
|
+
const before = offsetAt(timeZone, wall - DAY), after = offsetAt(timeZone, wall + DAY);
|
|
51
|
+
const candidates = [...new Set([wall - before, wall - after])].sort((a, b) => a - b);
|
|
52
|
+
const valid = candidates.filter(at => offsetAt(timeZone, at) === wall - at);
|
|
53
|
+
if (valid.length)
|
|
54
|
+
return valid[0];
|
|
55
|
+
let low = candidates[0], high = candidates[candidates.length - 1];
|
|
56
|
+
if (low === high)
|
|
57
|
+
return low;
|
|
58
|
+
const lowOffset = offsetAt(timeZone, low);
|
|
59
|
+
while (high - low > MINUTE) {
|
|
60
|
+
const mid = low + Math.floor((high - low) / (2 * MINUTE)) * MINUTE;
|
|
61
|
+
if (offsetAt(timeZone, mid) === lowOffset)
|
|
62
|
+
low = mid;
|
|
63
|
+
else
|
|
64
|
+
high = mid;
|
|
65
|
+
}
|
|
66
|
+
return high;
|
|
67
|
+
}
|
|
68
|
+
const dayMatches = (parsed, year, month, day) => {
|
|
69
|
+
if (!parsed.months.has(month))
|
|
70
|
+
return false;
|
|
71
|
+
const dom = parsed.daysOfMonth.has(day), dow = parsed.daysOfWeek.has(new Date(Date.UTC(year, month - 1, day)).getUTCDay());
|
|
72
|
+
return parsed.dayRestrictionOr ? dom || dow : dom && dow;
|
|
73
|
+
};
|
|
74
|
+
const ascending = (values) => [...values].sort((a, b) => a - b);
|
|
75
|
+
export function nextCronFire(cron, timeZone, after) {
|
|
76
|
+
const parsed = parseCron(cron);
|
|
77
|
+
if (!parsed)
|
|
78
|
+
return null;
|
|
79
|
+
const hours = ascending(parsed.hours), minutes = ascending(parsed.minutes);
|
|
80
|
+
const wallAfter = after + offsetAt(timeZone, after);
|
|
81
|
+
const first = new Date(wallAfter);
|
|
82
|
+
const start = Date.UTC(first.getUTCFullYear(), first.getUTCMonth(), first.getUTCDate()) - DAY;
|
|
83
|
+
for (let index = 0; index < SEARCH_DAYS; index++) {
|
|
84
|
+
const dayStart = start + index * DAY, date = new Date(dayStart);
|
|
85
|
+
if (!dayMatches(parsed, date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()))
|
|
86
|
+
continue;
|
|
87
|
+
for (const hour of hours)
|
|
88
|
+
for (const minute of minutes) {
|
|
89
|
+
const wall = dayStart + hour * HOUR + minute * MINUTE;
|
|
90
|
+
if (wall < wallAfter - 6 * HOUR)
|
|
91
|
+
continue;
|
|
92
|
+
const at = instantOf(timeZone, wall);
|
|
93
|
+
if (at > after)
|
|
94
|
+
return at;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
export function upcomingCronFires(cron, timeZone, after, count) {
|
|
100
|
+
const fires = [];
|
|
101
|
+
for (let at = after; fires.length < count;) {
|
|
102
|
+
const next = nextCronFire(cron, timeZone, at);
|
|
103
|
+
if (next === null)
|
|
104
|
+
break;
|
|
105
|
+
fires.push(next);
|
|
106
|
+
at = next;
|
|
107
|
+
}
|
|
108
|
+
return fires;
|
|
109
|
+
}
|
|
110
|
+
export function monitorRunsPerMonth(schedule) {
|
|
111
|
+
if (!("cron" in schedule))
|
|
112
|
+
return Math.round((365 / 12) * 86_400 / PLATFORM_MONITOR_INTERVALS[schedule.interval]);
|
|
113
|
+
const parsed = parseCron(schedule.cron);
|
|
114
|
+
if (!parsed)
|
|
115
|
+
return 0;
|
|
116
|
+
let days = 0;
|
|
117
|
+
for (let index = 0; index < 365; index++) {
|
|
118
|
+
const date = new Date(Date.UTC(2026, 0, 1 + index));
|
|
119
|
+
if (dayMatches(parsed, date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()))
|
|
120
|
+
days++;
|
|
121
|
+
}
|
|
122
|
+
return Math.round(days * parsed.hours.size * parsed.minutes.size / 12);
|
|
123
|
+
}
|