From 88d098d49ad2e270ddd1860cb0354b807c11a922 Mon Sep 17 00:00:00 2001 From: Rami Date: Fri, 23 Oct 2026 16:46:00 +0100 Subject: [PATCH] fix(coordination): the workflow spec SDK compiles a validator on first use, not when it is imported `@openworkflowspec/sdk` 1.0.3-alpha8 builds an Ajv instance and compiles all 171 of its validators as it is imported, which generates 217 functions from strings. The runtime never asks the SDK to validate: the capability checks a document with an interpreting validator and keeps the SDK for its classes and its graph. A library that compiles what nobody asks for, at import, has the defect, and the fix is the library's; until it ships, the runtime carries it as a dependency patch. - `patches/@openworkflowspec__sdk@1.0.3-alpha8.patch`, named in `pnpm-workspace.yaml`, makes `index.mjs`, `index.cjs` and `index.umd.js` build the Ajv instance and compile the validator of a type the first time `validate` asks for that type, and keep it. The errors for a type the SDK does not know and for a schema it cannot find stay. `index.umd.min.js`, which Node does not load, is left as published. - The image copies `patches/` before it installs its dependencies, whose install otherwise stops with "Patch file not found". - A test runs a child Node process with `--disallow-code-generation-from-strings`: the SDK loads, hydrates a workflow, builds its graph and writes its Mermaid as it does without the flag, and only `validate` is refused. Without the patch the child cannot import the SDK and the test fails. - The capability's README says the SDK is patched, why, what the minified bundle and the npm package get, and that the patch goes when the SDK itself compiles on first use. - The coordination and tool-servers READMEs say what the first save of a workflow costs with the SDK patched, measured again and dated, beside what it cost as published. Patched, the import generates no function from strings and the first `validate` of a workflow 47. The import took 19 ms against 119 ms as published, at the median of 11 imports of each, and the first save of a workflow without expressions 82 ms, against 182 ms as published and 37 ms when the SDK loaded at the start, at the median of 9 servers of each; each interleaved, with Node 26.10.0 on one machine of 16 cores at a load average of 5.5 to 6.6. Co-Authored-By: Claude Fable 5.1 --- capabilities/coordination/README.md | 4 +- .../src/dsl/specification-sdk.test.ts | 62 +++++++ packages/server/Dockerfile | 1 + packages/tool-servers/README.md | 2 +- .../@openworkflowspec__sdk@1.0.3-alpha8.patch | 168 ++++++++++++++++++ pnpm-lock.yaml | 7 +- pnpm-workspace.yaml | 3 + 7 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 capabilities/coordination/src/dsl/specification-sdk.test.ts create mode 100644 patches/@openworkflowspec__sdk@1.0.3-alpha8.patch diff --git a/capabilities/coordination/README.md b/capabilities/coordination/README.md index a690f5435..ff436d568 100644 --- a/capabilities/coordination/README.md +++ b/capabilities/coordination/README.md @@ -31,7 +31,9 @@ A run a caller starts, one whose context names no `trigger`, whether `run_defini `dslProblems` (`src/dsl/dsl-validation.ts`) checks a document against the DSL, before the policy's rejections join its problems, and answers a promise, which the document's reader awaits: the version it names, its names, the schema of the specification, the names of its tasks and the graph of its steps, in that order, each only when those before it found nothing. It reads the document without the runtime's own `output.shape` and without the `kind` and `because` of an error, the runtime's extensions to the DSL's errors. -The schema is `workflowSchema` of `@openworkflowspec/sdk`, the specification's JSON Schema of draft 2020-12, and `@hyperjump/json-schema`, its `draft-2020-12` entry, checks a document against it by interpreting it (`src/dsl/specification-schema.ts`). The SDK's own validation, which `Classes.Workflow.deserialize` runs unless it is given `{ validate: false }`, is Ajv, which compiles each of the SDK's 171 validators into code generated from strings: it costs start-up time wherever it loads and is refused wherever no code may be generated. The interpreting validator reads the same schema as data, generates no code, and compiles the schema into the tree it interprets only when the first document is checked, in about 35 ms; its validation is asynchronous only for that, so the capability's check is a promise. It asserts the formats the schema names, `uri`, `uri-template`, `json-pointer` and `date-time`, registered with the checks of `@hyperjump/json-schema-formats` the library's own formats use, and asserted with `setShouldValidateFormat(true)`, a setting of the library for the whole process, both made once, as the library is loaded (`src/dsl/dsl-libraries.ts`), since Ajv asserted them too and draft 2020-12 otherwise reads a format as an annotation. It gives the same answers: over the 499 workflow documents the runtime's tests check, its corpus and its input logs hold, and the 13,340 made from them by changing one value each, it accepts and refuses exactly the documents Ajv does. Its `date-time` is stricter than Ajv's: it refuses a space or a tab between the date and the time, which the grammar of RFC 3339 does not allow, and a leap second on a day that had none. Nothing calls the SDK's validation; the SDK stays for its schema, its classes and its graph, and importing it still compiles its validators. So the capability imports the SDK, and the validator with the formats it asserts, with the first document it reads, through `specificationSdk` and `schemaValidator` (`src/dsl/dsl-libraries.ts`), the one place each is loaded, and keeps them while the server runs; no module the capability's entry imports reaches either (`src/dsl/library-entries.test.ts`). A server that reads no workflow document never loads them, which took 130 ms, 222 files and 1.4 MiB of a server's start, and the first document read pays for them beside the compiling of the schema: the first save of a workflow without expressions took 175 ms against 34 ms when the two loaded with the capability, and the second 5.2 ms against 4.9 ms (measured on 2026-10-23 on one machine of 16 cores, with Node 26.10.0, at the median of 11 starts and of 9 servers). +The schema is `workflowSchema` of `@openworkflowspec/sdk`, the specification's JSON Schema of draft 2020-12, and `@hyperjump/json-schema`, its `draft-2020-12` entry, checks a document against it by interpreting it (`src/dsl/specification-schema.ts`). The SDK's own validation, which `Classes.Workflow.deserialize` runs unless it is given `{ validate: false }`, is Ajv, which compiles each of the SDK's 171 validators into code generated from strings: it costs start-up time wherever it loads and is refused wherever no code may be generated. The interpreting validator reads the same schema as data, generates no code, and compiles the schema into the tree it interprets only when the first document is checked, in about 35 ms; its validation is asynchronous only for that, so the capability's check is a promise. It asserts the formats the schema names, `uri`, `uri-template`, `json-pointer` and `date-time`, registered with the checks of `@hyperjump/json-schema-formats` the library's own formats use, and asserted with `setShouldValidateFormat(true)`, a setting of the library for the whole process, both made once, as the library is loaded (`src/dsl/dsl-libraries.ts`), since Ajv asserted them too and draft 2020-12 otherwise reads a format as an annotation. It gives the same answers: over the 499 workflow documents the runtime's tests check, its corpus and its input logs hold, and the 13,340 made from them by changing one value each, it accepts and refuses exactly the documents Ajv does. Its `date-time` is stricter than Ajv's: it refuses a space or a tab between the date and the time, which the grammar of RFC 3339 does not allow, and a leap second on a day that had none. Nothing calls the SDK's validation; the SDK stays for its schema, its classes and its graph. The capability imports the SDK, and the validator with the formats it asserts, with the first document it reads, through `specificationSdk` and `schemaValidator` (`src/dsl/dsl-libraries.ts`), the one place each is loaded, and keeps them while the server runs; no module the capability's entry imports reaches either (`src/dsl/library-entries.test.ts`). A server that reads no workflow document never loads them, which took 130 ms, 222 files and 1.4 MiB of a server's start with the SDK as published (measured on 2026-10-23 on one machine of 16 cores, with Node 26.10.0, at the median of 11 starts), and the first document read pays for them beside the compiling of the schema: with the SDK patched, as the next paragraph says, the first save of a workflow without expressions took 82 ms, against 182 ms with the SDK as published and 37 ms when the two loaded with the capability, and the second 5.5 ms, against 5.0 ms and 5.2 ms (measured on 2026-10-23 on the same machine, with Node 26.10.0, at the median of 9 servers of each, interleaved, at a load average of 5.8 to 6.6). + +The SDK is patched so that importing it compiles no validator, since the runtime never asks it to validate. As published, `@openworkflowspec/sdk` 1.0.3-alpha8 builds its Ajv instance and compiles all 171 of its validators as it is imported, which generates 217 functions from strings. `patches/@openworkflowspec__sdk@1.0.3-alpha8.patch`, which `pnpm-workspace.yaml` names and the image copies before it installs its dependencies, makes `index.mjs`, `index.cjs` and `index.umd.js` build the instance and compile the validator of a type the first time `validate` is called for that type, and keep it, with the same errors for a type the SDK does not know and a schema it cannot find. The minified bundle, `index.umd.min.js`, which only the package's `browser` field names and Node does not load, is left as published. A patch applies only where this workspace installs its dependencies, so the npm package `@on.auto/runtime` depends on the SDK as published. Patched, importing the SDK, hydrating a workflow and building its graph generate no function from strings, and the first `validate` of a workflow generates 47; the import took 19 ms against 119 ms as published, and 28 ms of CPU against 174 ms (measured on 2026-10-23 on the same machine, with Node 26.10.0, at the median of 11 imports of each, interleaved, at a load average of 5.5 to 5.8). In a process run with `--disallow-code-generation-from-strings`, where as published it cannot be imported, the patched SDK loads, hydrates a workflow, builds its graph and writes its Mermaid as it does anywhere else, and refuses only to validate (`src/dsl/specification-sdk.test.ts`). The patch goes when the SDK itself compiles on first use. The schema is read with two changes that keep what it accepts. A `false` subschema is read as `{ not: {} }`, which refuses the same values, so that every refusal is a keyword of a schema at its place and a property the schema refuses outright is a `not` that allows nothing, wherever the schema refuses it. A task is checked on its own: each reference to `#/$defs/task` is read as any object, and each task `allTaskEntries` of `@on.auto/workflow-engine` walks is checked against `#/$defs/task`, so a document is checked once a task. Checked whole, a list of tasks is checked again for each type of task that may hold it, `do` and `for` among them, and the time doubles with each level the tasks nest, on the one thread every workflow shares: Ajv took 55 ms for 16 levels and about a second for 20; task by task, 30 levels take about a millisecond (`src/dsl/dsl-validation.test.ts`). A task under `use.functions`, which the policy refuses, is not checked further. diff --git a/capabilities/coordination/src/dsl/specification-sdk.test.ts b/capabilities/coordination/src/dsl/specification-sdk.test.ts new file mode 100644 index 000000000..40dec6ed3 --- /dev/null +++ b/capabilities/coordination/src/dsl/specification-sdk.test.ts @@ -0,0 +1,62 @@ +import { once } from 'node:events'; + +import { spawnForTheTest } from '@on.auto/operations/testing'; +import { Schema } from 'effect'; +import { describe, expect, it } from 'vitest'; + +import { workflow } from '../testing/workflows.ts'; + +const greeting = workflow('do:\n - greet: { set: { greeting: hello } }\n - answer: { set: { answered: true } }'); + +const usingTheSdk = [ + `const { buildGraph, Classes, convertToMermaidCode, validate } = await import(${JSON.stringify(import.meta.resolve('@openworkflowspec/sdk'))});`, + `const workflow = Classes.Workflow.deserialize(${JSON.stringify(JSON.stringify(greeting))}, { validate: false });`, + 'const nodes = buildGraph(workflow).nodes.map(({ id }) => id);', + 'const mermaid = convertToMermaidCode(workflow);', + "const validation = (() => { try { validate('Workflow', workflow); return 'passed'; } catch (error) { return String(error); } })();", + 'process.stdout.write(JSON.stringify({ nodes, mermaid, validation }));', +].join('\n'); + +const useOf = Schema.decodeUnknownSync( + Schema.fromJsonString( + Schema.Struct({ nodes: Schema.Array(Schema.String), mermaid: Schema.String, validation: Schema.String }), + ), +); + +interface Ended { + readonly exitCode: number | null; + readonly printed: string; +} + +async function theSdkUsedWith(flags: readonly string[]): Promise { + const child = spawnForTheTest(process.execPath, [...flags, '--input-type=module', '--eval', usingTheSdk], { + stdio: ['ignore', 'pipe', 'ignore'], + }); + const output = { printed: '' }; + child.stdout?.setEncoding('utf8').on('data', (chunk: string) => { + output.printed += chunk; + }); + await once(child, 'close'); + return { exitCode: child.exitCode, printed: output.printed }; +} + +const flowchart: unknown = expect.stringMatching(/^flowchart TD\n/u); + +describe('the SDK of the specification', () => { + it('loads where no code may be generated from strings, hydrates a workflow, builds its graph and writes its Mermaid there as anywhere else, and refuses only to validate', async () => { + const anywhere = await theSdkUsedWith([]); + const withoutCodeFromStrings = await theSdkUsedWith(['--disallow-code-generation-from-strings']); + + expect([anywhere.exitCode, withoutCodeFromStrings.exitCode]).toEqual([0, 0]); + expect(useOf(anywhere.printed)).toEqual({ + nodes: ['root-entry-node', 'root-exit-node', '/do/greet', '/do/answer'], + mermaid: flowchart, + validation: 'passed', + }); + expect(useOf(withoutCodeFromStrings.printed)).toEqual({ + ...useOf(anywhere.printed), + validation: + "DslValidationError: Validation failed at '' (Workflow): Code generation from strings disallowed for this context", + }); + }); +}); diff --git a/packages/server/Dockerfile b/packages/server/Dockerfile index a69b66032..5ffb94529 100644 --- a/packages/server/Dockerfile +++ b/packages/server/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update \ WORKDIR /app RUN npm install --global --no-fund --no-audit pnpm@12.11.2 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY patches patches/ COPY packages/server/package.json packages/server/ COPY packages/api/package.json packages/api/ COPY packages/playbooks/package.json packages/playbooks/ diff --git a/packages/tool-servers/README.md b/packages/tool-servers/README.md index 0b1a5ef0b..49de26ba0 100644 --- a/packages/tool-servers/README.md +++ b/packages/tool-servers/README.md @@ -4,7 +4,7 @@ How a playbook reaches the outside world: the tool servers of its org, MCP serve ## Entry points -- `@on.auto/tool-servers`: the settings, with `ServerEntrySchema`, the schema of one entry, `readEntry`, the reading of one entry of the file, with which `@on.auto/connections` reads a connection's entry, its credential placed, into the connection's `ServerSettings`, and whose context's `placedValues`, the values the runtime places by the name of their placeholder, stand in the entry's header and query values once the entry as written is judged, and whose context's `providers`, a `ProviderResolver`, resolves the provider an entry of the file names (see [An entry that names a provider](#an-entry-that-names-a-provider)), `noProviders` where it is left out; `givenByTheProvider(field)`, the words of a field of a server written beside a provider, and `secretOrKeyExpected`, the words of client credentials given neither a client secret nor a private key with its algorithm; `makeConsentClient`, the runtime acting as a client at the authorization server of a provider's way `authorization`, and the types of its contract, `ConsentClient`, `ClientRegistrations` and `ConnectionTokens`, the store of the tokens of a connection connected by consent, with `KeptRow`, a row as a store reads it, and `HeldTokens`, that row decoded, among them, and `keptTokensIn`, which decodes the value of a row, which `@on.auto/connections` calls and implements, and `ConsentCredential`, the third kind of the credential of `AuthSettings`, the tokens of a consent, which it composes, with `ClientSourceSchema`, `clientSources` and `isSameIssuer`, the client sources an entry may list, in the runtime's order, and the comparison of two issuers, `clientMetadataDocumentOf` and its `ClientMetadataDocument`, the document the runtime names its client by where `PUBLIC_URL` is https, which the server gives `@on.auto/api` to answer, `ClientIdSchema`, the rule of a client id the record and a registration's answer are held to, and `isScope`, the rule of a scope a provider's way and a grant are held to (see [The runtime as a client](#the-runtime-as-a-client)); `serverNameProblems`, the check of a server's name, with which it checks a connection's name, and `servedFieldsOf`, the playbooks, tools, `record_content` and `request_id` an entry is served by, as it writes them or by default where it leaves them out, with which it builds the serving of every connection it lists, and `isPinnableUrl` and `headerNamePattern`; `redactedValuesOf`, the values of a record each redacted under its key, which the reasoning capability takes for the headers and the query parameters of a gateway; `@on.auto/providers` takes the server part of a provider's entry from the fields of `ServerEntrySchema`, as a connection's entry does, and checks a provider's name with `serverNameProblems` and its addresses with `isPinnableUrl`; `namesEveryTool`, whether a tool reference names every tool of its server with `*`, as a reasoning function's `tools` may; the contract of a source of connections, `ConnectionSource` and its types, which `@on.auto/connections` implements, and `noConnections`; `describedToolServers`, `makeToolAccess`, the five operations, `defineSearch` and its org twin `defineSearchInOrg`, with `ProviderOffering` and `OfferedProvider`, the contract of the providers offered to a caller, which `@on.auto/connections` implements, `defineIntrospect`, `defineValidate`, `defineDryRun` and `defineExecute`, `handCallPresenter`, which shows a call made by hand, and the facts of a call that `@on.auto/definitions` builds the run's events from. The access loads the MCP client and its OAuth providers (`src/access/linked-access.ts`) through a dynamic import when a run first opens its tools, a search or a read first asks a server or `execute` first calls a tool, so a server that never does any of them never loads them: the server's start loaded 55 files and 0.94 MiB more with them (measured 2026-10-06 with a module load hook, 1,241 files against main's 1,186). The transport of a `stdio` server, the one module of the package that imports `node:child_process` (`src/clients/stdio-transport.ts`), loads through a dynamic import of its own when the access first opens a `stdio` server, so an access whose settings name none never loads child processes (`src/access/tool-access-loading.test.ts`). Without them it loads 84 files and 236 KiB of this package, the five operations, the reading of the settings of the three types and of an entry that names a provider, `describedToolServers` and the naming of the servers of an org and of its connections among them (measured 2026-10-20 with a module load hook over both entries, the sizes of the files as they are on disk, against 82 files and 219 KiB before the switch of Auto's consent service and the listing of its clients, `src/consent-through-auto/auto-clients.ts`, and the contract they call, `src/consent/consent-service.ts`, came to load with the consent client, against 82 files and 217 KiB before the metadata of the runtime's client, which its document and a dynamic registration share, was written in `src/consent/client-sources.ts`, against 81 files and 211 KiB before the tokens of a consent were served, which added `src/consent-tokens/kept-tokens.ts`, the value a token source of a consent reads, and against 77 files and 192 KiB before an entry could name a provider); the reading resolves a provider through the resolver its context gives, so it loads no code of a provider's; the reading of an OpenAPI document, its decoding and the tools derived from it (`src/openapi`, `described-servers.ts` and the types aside) load through a dynamic import too, when an entry of type `openapi` is described at start. The server's start loads neither the TypeScript compiler, which the check worker of `@on.auto/definitions` loads with the first check, nor the driver of a store its settings do not name: measured on 2026-10-23 on one machine of 16 cores, with Node 26.10.0, by the same hook on the thread that serves requests, from the start of the process to its ready line, at the median of 11 starts interleaved with main's, it loaded on SQLite 2,504 files and 21.0 MiB in 802 ms, against main's 2,573 files and 30.4 MiB in 1,143 ms, at a load average of 20 to 13, and on PostgreSQL 2,551 files and 19.5 MiB in 810 ms, against 2,573 files and 30.4 MiB in 1,134 ms, at 11 to 7. The compiler was 12 files, 8.8 MiB and 318 to 328 ms of it, the warming of the check worker among them, which the first save of a program now pays: 245 ms against 17 ms at the median of 9 servers interleaved with main's, and 6.7 ms against 5.9 ms for the second, at a load average of about 5. The driver of the other store, `pg` with the PostgreSQL entry on SQLite and `sqlite3` with its native addon on PostgreSQL, was 57 files and 0.6 MiB, and 10 files and 2.1 MiB, with a time, 13 and 6 ms, within the spread of the starts. Nor does it load the SDK of the workflow specification and the validator of its schema, which the coordination capability imports with the first document it reads, the S3 client, which the server imports when its settings name a bucket, `google-auth-library` with the AI SDK's Vertex providers, which model access imports when Vertex is configured, or the credential chain of AWS, which the first client that asks for it imports: measured on 2026-10-23 on the same machine, with Node 26.10.0, by the same hook, at the median of 11 starts on SQLite interleaved with main's, at a load average of 5 to 4, it loaded 2,307 files and 18.5 MiB in 654 ms, against main's 2,636 files and 21.4 MiB in 821 ms. The SDK and the validator were 222 files, 1.4 MiB and 130 ms of it, the SDK's 171 validators and Ajv among them, which the first save of a workflow now pays, 175 ms against 34 ms at the median of 9 servers; the S3 client was 26 files, 0.7 MiB and 13 ms, within the spread of the starts, and the Google library and the credential chain 81 files, 0.9 MiB and 24 ms. A CPU profile of 7 starts of each, sampled every 100 µs, gave the SDK with Ajv 115 ms of main's start, the validator 14 ms, the S3 client 11 ms, the Google library 10 ms and the credential chain 6 ms. Nor does the start compose what a server builds only for a request of a kind, which the server's capability registry composes on first use (see [the server](../server/README.md#what-is-composed-at-start-and-what-on-first-use)): the model access of reasoning functions, with a client for each provider and the credential libraries of Vertex, Bedrock and Azure without a key, and the access to the tool servers, with the document of each `openapi` entry read and described; and the prediction caches check the files they hold when a prediction first reads them. Measured on 2026-10-23 on the same machine, with Node 26.10.0, by the same hook, at the median of 11 starts on SQLite interleaved with main's: with nothing configured, 2,312 files and 18.5 MiB in 727 ms, against main's 2,310 files and 18.5 MiB in 727 ms, the two files more being the registry's, at a load average of 16 to 13; with OpenAI, Anthropic, Vertex, Bedrock, Azure without a key and two `openapi` entries over two of the documents the providers ship, of 1.4 MiB and 484 KiB, served on loopback, 2,312 files and 18.5 MiB in 732 ms, against 2,757 files and 20.8 MiB in 833 ms, at 13 to 12; and with 320 MiB of model files and rows in the folder of the caches, 723 ms against 871 ms, at 12 to 10. The first request of each kind pays its composition instead, at the median of 9 servers of that second setting interleaved with main's, at a load average of 10 to 9: the first `list_models` took 65.6 ms against 1.0 ms and the first `search` 51.0 ms against 17.8 ms, the second 2.0 ms against 0.8 ms and 1.6 ms against 2.4 ms, converging by the eighth, and the first listing of the playbooks, which composes nothing, 1.8 ms against 2.1 ms. Readying both caches over those 320 MiB, which the first prediction to read them pays, took 146 ms at the median of 9, and 0.04 ms once ready; one ask of the registry for what it composed takes about 10 µs. +- `@on.auto/tool-servers`: the settings, with `ServerEntrySchema`, the schema of one entry, `readEntry`, the reading of one entry of the file, with which `@on.auto/connections` reads a connection's entry, its credential placed, into the connection's `ServerSettings`, and whose context's `placedValues`, the values the runtime places by the name of their placeholder, stand in the entry's header and query values once the entry as written is judged, and whose context's `providers`, a `ProviderResolver`, resolves the provider an entry of the file names (see [An entry that names a provider](#an-entry-that-names-a-provider)), `noProviders` where it is left out; `givenByTheProvider(field)`, the words of a field of a server written beside a provider, and `secretOrKeyExpected`, the words of client credentials given neither a client secret nor a private key with its algorithm; `makeConsentClient`, the runtime acting as a client at the authorization server of a provider's way `authorization`, and the types of its contract, `ConsentClient`, `ClientRegistrations` and `ConnectionTokens`, the store of the tokens of a connection connected by consent, with `KeptRow`, a row as a store reads it, and `HeldTokens`, that row decoded, among them, and `keptTokensIn`, which decodes the value of a row, which `@on.auto/connections` calls and implements, and `ConsentCredential`, the third kind of the credential of `AuthSettings`, the tokens of a consent, which it composes, with `ClientSourceSchema`, `clientSources` and `isSameIssuer`, the client sources an entry may list, in the runtime's order, and the comparison of two issuers, `clientMetadataDocumentOf` and its `ClientMetadataDocument`, the document the runtime names its client by where `PUBLIC_URL` is https, which the server gives `@on.auto/api` to answer, `ClientIdSchema`, the rule of a client id the record and a registration's answer are held to, and `isScope`, the rule of a scope a provider's way and a grant are held to (see [The runtime as a client](#the-runtime-as-a-client)); `serverNameProblems`, the check of a server's name, with which it checks a connection's name, and `servedFieldsOf`, the playbooks, tools, `record_content` and `request_id` an entry is served by, as it writes them or by default where it leaves them out, with which it builds the serving of every connection it lists, and `isPinnableUrl` and `headerNamePattern`; `redactedValuesOf`, the values of a record each redacted under its key, which the reasoning capability takes for the headers and the query parameters of a gateway; `@on.auto/providers` takes the server part of a provider's entry from the fields of `ServerEntrySchema`, as a connection's entry does, and checks a provider's name with `serverNameProblems` and its addresses with `isPinnableUrl`; `namesEveryTool`, whether a tool reference names every tool of its server with `*`, as a reasoning function's `tools` may; the contract of a source of connections, `ConnectionSource` and its types, which `@on.auto/connections` implements, and `noConnections`; `describedToolServers`, `makeToolAccess`, the five operations, `defineSearch` and its org twin `defineSearchInOrg`, with `ProviderOffering` and `OfferedProvider`, the contract of the providers offered to a caller, which `@on.auto/connections` implements, `defineIntrospect`, `defineValidate`, `defineDryRun` and `defineExecute`, `handCallPresenter`, which shows a call made by hand, and the facts of a call that `@on.auto/definitions` builds the run's events from. The access loads the MCP client and its OAuth providers (`src/access/linked-access.ts`) through a dynamic import when a run first opens its tools, a search or a read first asks a server or `execute` first calls a tool, so a server that never does any of them never loads them: the server's start loaded 55 files and 0.94 MiB more with them (measured 2026-10-06 with a module load hook, 1,241 files against main's 1,186). The transport of a `stdio` server, the one module of the package that imports `node:child_process` (`src/clients/stdio-transport.ts`), loads through a dynamic import of its own when the access first opens a `stdio` server, so an access whose settings name none never loads child processes (`src/access/tool-access-loading.test.ts`). Without them it loads 84 files and 236 KiB of this package, the five operations, the reading of the settings of the three types and of an entry that names a provider, `describedToolServers` and the naming of the servers of an org and of its connections among them (measured 2026-10-20 with a module load hook over both entries, the sizes of the files as they are on disk, against 82 files and 219 KiB before the switch of Auto's consent service and the listing of its clients, `src/consent-through-auto/auto-clients.ts`, and the contract they call, `src/consent/consent-service.ts`, came to load with the consent client, against 82 files and 217 KiB before the metadata of the runtime's client, which its document and a dynamic registration share, was written in `src/consent/client-sources.ts`, against 81 files and 211 KiB before the tokens of a consent were served, which added `src/consent-tokens/kept-tokens.ts`, the value a token source of a consent reads, and against 77 files and 192 KiB before an entry could name a provider); the reading resolves a provider through the resolver its context gives, so it loads no code of a provider's; the reading of an OpenAPI document, its decoding and the tools derived from it (`src/openapi`, `described-servers.ts` and the types aside) load through a dynamic import too, when an entry of type `openapi` is described at start. The server's start loads neither the TypeScript compiler, which the check worker of `@on.auto/definitions` loads with the first check, nor the driver of a store its settings do not name: measured on 2026-10-23 on one machine of 16 cores, with Node 26.10.0, by the same hook on the thread that serves requests, from the start of the process to its ready line, at the median of 11 starts interleaved with main's, it loaded on SQLite 2,504 files and 21.0 MiB in 802 ms, against main's 2,573 files and 30.4 MiB in 1,143 ms, at a load average of 20 to 13, and on PostgreSQL 2,551 files and 19.5 MiB in 810 ms, against 2,573 files and 30.4 MiB in 1,134 ms, at 11 to 7. The compiler was 12 files, 8.8 MiB and 318 to 328 ms of it, the warming of the check worker among them, which the first save of a program now pays: 245 ms against 17 ms at the median of 9 servers interleaved with main's, and 6.7 ms against 5.9 ms for the second, at a load average of about 5. The driver of the other store, `pg` with the PostgreSQL entry on SQLite and `sqlite3` with its native addon on PostgreSQL, was 57 files and 0.6 MiB, and 10 files and 2.1 MiB, with a time, 13 and 6 ms, within the spread of the starts. Nor does it load the SDK of the workflow specification and the validator of its schema, which the coordination capability imports with the first document it reads, the S3 client, which the server imports when its settings name a bucket, `google-auth-library` with the AI SDK's Vertex providers, which model access imports when Vertex is configured, or the credential chain of AWS, which the first client that asks for it imports: measured on 2026-10-23 on the same machine, with Node 26.10.0, by the same hook, at the median of 11 starts on SQLite interleaved with main's, at a load average of 5 to 4, it loaded 2,307 files and 18.5 MiB in 654 ms, against main's 2,636 files and 21.4 MiB in 821 ms. The SDK and the validator were 222 files, 1.4 MiB and 130 ms of it with the SDK as published, its 171 validators and Ajv among them, which the first save of a workflow now pays; with the SDK patched to compile a validator only when it is asked to validate (see [the coordination capability](../../capabilities/coordination/README.md#the-dsl-check-of-a-document)), the first save took 82 ms, against 182 ms with the SDK as published and 37 ms when they loaded at the start, measured on 2026-10-23 on the same machine, with Node 26.10.0, at the median of 9 servers of each, interleaved, at a load average of 5.8 to 6.6; the S3 client was 26 files, 0.7 MiB and 13 ms, within the spread of the starts, and the Google library and the credential chain 81 files, 0.9 MiB and 24 ms. A CPU profile of 7 starts of each, sampled every 100 µs, gave the SDK with Ajv 115 ms of main's start, the validator 14 ms, the S3 client 11 ms, the Google library 10 ms and the credential chain 6 ms. Nor does the start compose what a server builds only for a request of a kind, which the server's capability registry composes on first use (see [the server](../server/README.md#what-is-composed-at-start-and-what-on-first-use)): the model access of reasoning functions, with a client for each provider and the credential libraries of Vertex, Bedrock and Azure without a key, and the access to the tool servers, with the document of each `openapi` entry read and described; and the prediction caches check the files they hold when a prediction first reads them. Measured on 2026-10-23 on the same machine, with Node 26.10.0, by the same hook, at the median of 11 starts on SQLite interleaved with main's: with nothing configured, 2,312 files and 18.5 MiB in 727 ms, against main's 2,310 files and 18.5 MiB in 727 ms, the two files more being the registry's, at a load average of 16 to 13; with OpenAI, Anthropic, Vertex, Bedrock, Azure without a key and two `openapi` entries over two of the documents the providers ship, of 1.4 MiB and 484 KiB, served on loopback, 2,312 files and 18.5 MiB in 732 ms, against 2,757 files and 20.8 MiB in 833 ms, at 13 to 12; and with 320 MiB of model files and rows in the folder of the caches, 723 ms against 871 ms, at 12 to 10. The first request of each kind pays its composition instead, at the median of 9 servers of that second setting interleaved with main's, at a load average of 10 to 9: the first `list_models` took 65.6 ms against 1.0 ms and the first `search` 51.0 ms against 17.8 ms, the second 2.0 ms against 0.8 ms and 1.6 ms against 2.4 ms, converging by the eighth, and the first listing of the playbooks, which composes nothing, 1.8 ms against 2.1 ms. Readying both caches over those 320 MiB, which the first prediction to read them pays, took 146 ms at the median of 9, and 0.04 ms once ready; one ask of the registry for what it composed takes about 10 µs. - `@on.auto/tool-servers/policy`: the pure helpers a caller needs without connecting to anything (`toolReferenceOf`, `toolReferenceShape`, `writtenOf`, `namesEveryTool`, `runBoundMs` and `runIdKey`). It loads no transport code, so the reasoning function adapter, which parses a function's `tools` and bounds its run, does not load the MCP client wherever it is bundled; it takes `ToolAccess` as a type only. - `@on.auto/tool-servers/testing`: the fake server and the helpers of the tests (see Testing). diff --git a/patches/@openworkflowspec__sdk@1.0.3-alpha8.patch b/patches/@openworkflowspec__sdk@1.0.3-alpha8.patch new file mode 100644 index 000000000..495978203 --- /dev/null +++ b/patches/@openworkflowspec__sdk@1.0.3-alpha8.patch @@ -0,0 +1,168 @@ +diff --git a/index.cjs b/index.cjs +index eb819cf54dc60177b8b9e3900df9386b4fe82338..31bdbac58cb481f7115ee0178b09ad7f30a7f166 100644 +--- a/index.cjs ++++ b/index.cjs +@@ -4470,20 +4470,32 @@ var DslValidationError = class DslValidationError extends WorkflowValidationErro + }; + //#endregion + //#region src/lib/validation.ts +-const ajv = new ajv_dist_2020_js.default({ +- schemas: [workflow_default], +- strict: false +-}); +-(0, ajv_formats.default)(ajv); ++let ajv; ++const getAjv = () => { ++ if (!ajv) { ++ ajv = new ajv_dist_2020_js.default({ ++ schemas: [workflow_default], ++ strict: false ++ }); ++ (0, ajv_formats.default)(ajv); ++ } ++ return ajv; ++}; + /** + * A Map of validation functions, where the key is the name of the schema to validate with + */ +-const validators = new Map(Object.entries(validationPointers).map(([typeName, jsonPointer]) => { ++const validators = new Map(); ++const getValidator = (typeName) => { ++ const compiled = validators.get(typeName); ++ if (compiled) return compiled; ++ if (!Object.hasOwn(validationPointers, typeName)) throw new Error(`Unable to find a validation function for '${typeName}'`); ++ const jsonPointer = validationPointers[typeName]; + if (!jsonPointer) throw new Error(`No JSON pointer provided for type '${typeName}'`); +- const validate = ajv.getSchema(jsonPointer); ++ const validate = getAjv().getSchema(jsonPointer); + if (!validate) throw new Error(`Unable to find schema '${jsonPointer}' for type '${typeName}'`); +- return [typeName, validate]; +-})); ++ validators.set(typeName, validate); ++ return validate; ++}; + /** + * Renders a JSON Pointer for display in an error message, labelling the root explicitly. + * @param path The RFC 6901 JSON Pointer ('' for the root) +@@ -4498,8 +4510,7 @@ const displayPath = (path) => path === "" ? "" : path; + * @param data The data to validate + */ + const validateSchema = (typeName, data) => { +- const validateFn = validators.get(typeName); +- if (!validateFn) throw new Error(`Unable to find a validation function for '${typeName}'`); ++ const validateFn = getValidator(typeName); + if (!validateFn(deepCopy(data))) { + const schemaErrors = validateFn.errors ?? []; + throw new SchemaValidationError(`'${typeName}' is invalid: +diff --git a/index.mjs b/index.mjs +index 1a8d0ffc3126c1d2e2bc05916fa0ce297020647b..73b0ea5803fdd2f45f022502044422365ccd112c 100644 +--- a/index.mjs ++++ b/index.mjs +@@ -4435,20 +4435,32 @@ var DslValidationError = class DslValidationError extends WorkflowValidationErro + }; + //#endregion + //#region src/lib/validation.ts +-const ajv = new Ajv({ +- schemas: [workflow_default], +- strict: false +-}); +-addFormats(ajv); ++let ajv; ++const getAjv = () => { ++ if (!ajv) { ++ ajv = new Ajv({ ++ schemas: [workflow_default], ++ strict: false ++ }); ++ addFormats(ajv); ++ } ++ return ajv; ++}; + /** + * A Map of validation functions, where the key is the name of the schema to validate with + */ +-const validators = new Map(Object.entries(validationPointers).map(([typeName, jsonPointer]) => { ++const validators = new Map(); ++const getValidator = (typeName) => { ++ const compiled = validators.get(typeName); ++ if (compiled) return compiled; ++ if (!Object.hasOwn(validationPointers, typeName)) throw new Error(`Unable to find a validation function for '${typeName}'`); ++ const jsonPointer = validationPointers[typeName]; + if (!jsonPointer) throw new Error(`No JSON pointer provided for type '${typeName}'`); +- const validate = ajv.getSchema(jsonPointer); ++ const validate = getAjv().getSchema(jsonPointer); + if (!validate) throw new Error(`Unable to find schema '${jsonPointer}' for type '${typeName}'`); +- return [typeName, validate]; +-})); ++ validators.set(typeName, validate); ++ return validate; ++}; + /** + * Renders a JSON Pointer for display in an error message, labelling the root explicitly. + * @param path The RFC 6901 JSON Pointer ('' for the root) +@@ -4463,8 +4475,7 @@ const displayPath = (path) => path === "" ? "" : path; + * @param data The data to validate + */ + const validateSchema = (typeName, data) => { +- const validateFn = validators.get(typeName); +- if (!validateFn) throw new Error(`Unable to find a validation function for '${typeName}'`); ++ const validateFn = getValidator(typeName); + if (!validateFn(deepCopy(data))) { + const schemaErrors = validateFn.errors ?? []; + throw new SchemaValidationError(`'${typeName}' is invalid: +diff --git a/index.umd.js b/index.umd.js +index 296a78e6a456684bfbaa90aa2da241619587806c..5a1ac55812616ff2e43fdffc643a72797bc1bd7c 100644 +--- a/index.umd.js ++++ b/index.umd.js +@@ -12240,20 +12240,32 @@ + //#region src/lib/validation.ts + var import__2020 = /* @__PURE__ */ __toESM(require__2020()); + var import_dist = /* @__PURE__ */ __toESM(require_dist()); +- const ajv = new import__2020.default({ +- schemas: [workflow_default], +- strict: false +- }); +- (0, import_dist.default)(ajv); ++ let ajv; ++ const getAjv = () => { ++ if (!ajv) { ++ ajv = new import__2020.default({ ++ schemas: [workflow_default], ++ strict: false ++ }); ++ (0, import_dist.default)(ajv); ++ } ++ return ajv; ++ }; + /** + * A Map of validation functions, where the key is the name of the schema to validate with + */ +- const validators = new Map(Object.entries(validationPointers).map(([typeName, jsonPointer]) => { ++ const validators = new Map(); ++ const getValidator = (typeName) => { ++ const compiled = validators.get(typeName); ++ if (compiled) return compiled; ++ if (!Object.hasOwn(validationPointers, typeName)) throw new Error(`Unable to find a validation function for '${typeName}'`); ++ const jsonPointer = validationPointers[typeName]; + if (!jsonPointer) throw new Error(`No JSON pointer provided for type '${typeName}'`); +- const validate = ajv.getSchema(jsonPointer); ++ const validate = getAjv().getSchema(jsonPointer); + if (!validate) throw new Error(`Unable to find schema '${jsonPointer}' for type '${typeName}'`); +- return [typeName, validate]; +- })); ++ validators.set(typeName, validate); ++ return validate; ++ }; + /** + * Renders a JSON Pointer for display in an error message, labelling the root explicitly. + * @param path The RFC 6901 JSON Pointer ('' for the root) +@@ -12268,8 +12280,7 @@ + * @param data The data to validate + */ + const validateSchema = (typeName, data) => { +- const validateFn = validators.get(typeName); +- if (!validateFn) throw new Error(`Unable to find a validation function for '${typeName}'`); ++ const validateFn = getValidator(typeName); + if (!validateFn(deepCopy(data))) { + const schemaErrors = validateFn.errors ?? []; + throw new SchemaValidationError(`'${typeName}' is invalid: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3abbb2a86..a48465ab4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,6 +205,9 @@ catalogs: specifier: 5.0.3 version: 5.0.3 +patchedDependencies: + '@openworkflowspec/sdk@1.0.3-alpha8': 0262a21b41e0999009ab1087b52a8546c502f99e25445f1ac1626835bf0fec19 + importers: .: @@ -317,7 +320,7 @@ importers: version: link:../../packages/workflow-host '@openworkflowspec/sdk': specifier: 1.0.3-alpha8 - version: 1.0.3-alpha8 + version: 1.0.3-alpha8(patch_hash=0262a21b41e0999009ab1087b52a8546c502f99e25445f1ac1626835bf0fec19) effect: specifier: 'catalog:' version: 4.0.0 @@ -6964,7 +6967,7 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} - '@openworkflowspec/sdk@1.0.3-alpha8': + '@openworkflowspec/sdk@1.0.3-alpha8(patch_hash=0262a21b41e0999009ab1087b52a8546c502f99e25445f1ac1626835bf0fec19)': dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 514e42b9b..66deb632b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,3 +22,6 @@ allowBuilds: onnxruntime-node: false protobufjs: false sqlite3: true + +patchedDependencies: + '@openworkflowspec/sdk@1.0.3-alpha8': patches/@openworkflowspec__sdk@1.0.3-alpha8.patch