@pipelex/sdk 0.47.0 → 0.48.1
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 +9 -7
- package/dist/artifacts.d.ts +4 -3
- package/dist/artifacts.js +20 -14
- package/dist/artifacts.js.map +1 -1
- package/dist/cli/script.d.ts +1 -1
- package/dist/cli/source.d.ts +8 -7
- package/dist/cli/source.js +11 -5
- package/dist/cli/source.js.map +1 -1
- package/dist/client.d.ts +326 -213
- package/dist/client.js +829 -517
- package/dist/client.js.map +1 -1
- package/dist/codegen-check.d.ts +2 -3
- package/dist/codegen-check.js +2 -3
- package/dist/codegen-check.js.map +1 -1
- package/dist/errors.d.ts +5 -2
- package/dist/errors.js +5 -2
- package/dist/errors.js.map +1 -1
- package/dist/hooks/bundle-gather.d.ts +47 -40
- package/dist/hooks/bundle-gather.js +121 -219
- package/dist/hooks/bundle-gather.js.map +1 -1
- package/dist/hooks/check-core.d.ts +70 -6
- package/dist/hooks/check-core.js +188 -29
- package/dist/hooks/check-core.js.map +1 -1
- package/dist/hooks/claude-mthds-check.d.ts +19 -5
- package/dist/hooks/claude-mthds-check.js +20 -53
- package/dist/hooks/claude-mthds-check.js.map +1 -1
- package/dist/hooks/secret-guard.d.ts +38 -0
- package/dist/hooks/secret-guard.js +134 -0
- package/dist/hooks/secret-guard.js.map +1 -0
- package/dist/hooks/validate-stage.d.ts +29 -0
- package/dist/hooks/validate-stage.js +66 -0
- package/dist/hooks/validate-stage.js.map +1 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/method-trees.d.ts +220 -74
- package/dist/method-trees.js +250 -37
- package/dist/method-trees.js.map +1 -1
- package/dist/models.d.ts +162 -59
- package/dist/node-host/node.d.ts +33 -0
- package/dist/node-host/node.js +27 -0
- package/dist/node-host/node.js.map +1 -0
- package/dist/node-host/unavailable.d.ts +8 -0
- package/dist/node-host/unavailable.js +8 -0
- package/dist/node-host/unavailable.js.map +1 -0
- package/dist/node-host.d.ts +53 -0
- package/dist/node-host.js +72 -0
- package/dist/node-host.js.map +1 -0
- package/dist/prepare-inputs.d.ts +46 -27
- package/dist/prepare-inputs.js +123 -42
- package/dist/prepare-inputs.js.map +1 -1
- package/dist/product-models.d.ts +14 -16
- package/dist/upload.d.ts +4 -9
- package/dist/upload.js +13 -16
- package/dist/upload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +44 -21
- package/docs/cli.md +1 -1
- package/docs/crate-routes.md +43 -21
- package/docs/errors.md +5 -5
- package/docs/hook-bundle.md +27 -1
- package/docs/input-preparation.md +13 -13
- package/docs/method-trees.md +120 -22
- package/docs/run-results.md +5 -2
- package/package.json +8 -2
- package/dist/bundle-resources.d.ts +0 -62
- package/dist/bundle-resources.js +0 -165
- package/dist/bundle-resources.js.map +0 -1
- package/docs/bundle-resources.md +0 -107
package/docs/errors.md
CHANGED
|
@@ -43,7 +43,7 @@ On an error from a request that may have started a run, `retryable` is `false` w
|
|
|
43
43
|
| Class | `errorDomain` | `retryable` | Why |
|
|
44
44
|
|---|---|---|---|
|
|
45
45
|
| `ApiResponseError` | the server's, else the fallback | the server's, else no for a sent `input` or `config` domain, else the fallback | See [below](#a-refused-requests-verdict). |
|
|
46
|
-
| `RequestArgumentError` | `input` | no | The client refused a call's arguments before sending any request: no run source given to `execute` or `start`, run sources or method selectors that exclude each other, a selector rule of `validate`,
|
|
46
|
+
| `RequestArgumentError` | `input` | no | The client refused a call's arguments before sending any request: no run source given to `execute` or `start`, run sources or method selectors that exclude each other, a selector rule of `validate`, the method's tree given in two forms, holding no file, or in a directory the standard's lister refuses (its `MethodTreeError` as the `cause`), a tree past the inline limit of an API that keeps no blob store, a reserved key in `extra`, a `publishMethod` without its token, a `getMethodVersion` version that is not a positive integer, an id a route puts in its path (a method's, an example's, a run's, an organization's or an API key's) that is empty, `.` or `..` or is not a string, since the URL would resolve it into another route's path, or that holds a lone surrogate, which a URL cannot carry, a `createMethodExample` without its `name` or `inputs` or whose `run_id` is given but empty, an `updateMethodExample` naming neither `name` nor `inputs`, a `recordMethodExampleLatestRun` without a run id or a positive integer `input_revision`, a selector `parseMethodSelector` refuses, an artifact selection that is empty or names an unknown artifact, a start's `idempotencyKey` that is not 1 to 255 visible ASCII characters ([below](#a-starts-idempotency-key--idempotencykey)), or a request body JSON cannot carry, such as one holding a cycle, or a `bigint` on a runtime without `JSON.rawJSON` (the body is written with `stringifyExactJson`, which writes a `bigint` as its exact digits; see [exact-integers.md](./exact-integers.md)), with the failure as `cause`. Its options take an optional `verdict`, and the client declares `config` for a base URL that is not host-only, since it typically comes from `PIPELEX_BASE_URL`. That refusal shows the base URL's scheme and host alone and names the other parts it carried (credentials, a path, a query, a fragment) without their text, since those are where a secret travels in a URL; a value that is not an http or https URL is not shown at all. It is a `PipelineRequestError`, as the bare refusal it replaced was, and a refusal the standard's own run-source check raises keeps that original as `cause`. |
|
|
47
47
|
| `ApiUnreachableError` | `config` | yes | The address or the network must be checked, and a later attempt can get through. With the code `ABORT_TIMEOUT`, the SDK's own request timeout, it is `runtime`: the API took the request and did not answer in time. |
|
|
48
48
|
| `PipelineExecuteTimeoutError` | `input` | no | The blocking `execute` cannot outlast the gateway's limit; start the run and poll instead. The run goes on, so its `runMayHaveStarted` is always `true`. |
|
|
49
49
|
| `RunFailedError` | the report's | the report's | See [a failed run](#a-failed-run--runerrorreport). |
|
|
@@ -54,7 +54,7 @@ On an error from a request that may have started a run, `retryable` is `false` w
|
|
|
54
54
|
| `InputPreparationError` and `InvalidLocalSourceError`, `InvalidInputValueError`, `UploadNotAllowedError`, `MethodLoadError`, `RejectedAssetError` | `input` | no | The asset, the inputs or the method selector must change. The family's base takes an optional `verdict` in its options, so a subclass of your own can declare another; `prepareInputs` declares `runtime` for a pipe I/O answer that breaks the route's contract: no boolean `is_valid`, an `input_form` that does not describe the pipe it selected, or a descriptor that is malformed or names a kind this SDK does not know. |
|
|
55
55
|
| `UnsupportedUploadCapabilityError`, `UploadAuthenticationError` | `config` | no | The deployment or the credential must change. |
|
|
56
56
|
| `UploadTransportError` | the cause's, else by `code` | the cause's, else by `code` | `uploadFile` wraps the `ApiResponseError` or `ApiUnreachableError` the client threw, and takes its verdict, so a wrapped `402` is `config` and not retryable. Without such a cause, which is always the case from `uploadWithGrant`: `timeout`, `storage_timeout` and `conflict` are `runtime` and retryable, `conflict` because storage documents its `409 ConditionalRequestConflict` as retryable and the grant is not spent by it (a spent grant is the `412` of a `RejectedAssetError` with code `grant_used`); `server_error` is `runtime`, and retryable when [the fallback table](#a-refused-requests-verdict) would call its `status` retryable, any `5xx` but the `501` storage answers for a request it does not implement, or when it carries no status; `unexpected` is `runtime`, and retryable when that table would call its `status` retryable, which is the `408` or `429` storage answers when it times out or throttles the request rather than refusing the file, and not retryable for any other status or none; `unreachable` is `config` and retryable; `redirected` is `config` and not retryable; `invalid_grant_url` is `runtime` and not retryable. |
|
|
57
|
-
| `MethodFileTransferError` | `runtime` | by `code` | A method's file could not be moved between this machine and storage: uploaded by `createMethod` or `writeDraft`, or downloaded by `writeMethodTree`. `unreachable` (no answer), `storage_error` (`408`, `429` or a `5xx` other than `501`) and `mismatch` (downloaded bytes that are not the file listed) are retryable; `refused`, any other status, an expired link among them, is not. It carries `operation`, `code`, `sha256`, `path` and `status`, and neither the link, nor storage's error body, nor a cause ([method-trees.md](./method-trees.md#when-a-transfer-fails)). |
|
|
57
|
+
| `MethodFileTransferError` | `runtime` | by `code` | A method's file could not be moved between this machine and storage: uploaded by `createMethod` or `writeDraft`, so the method was not saved, or by a run, `validate`, `resolve`, `codegen`, `pipeIo` or `prepareInputs` sending a `directory`, `tree` or `contents` by hash, so the request was not sent, or downloaded by `writeMethodTree`. `unreachable` (no answer), `storage_error` (`408`, `429` or a `5xx` other than `501`) and `mismatch` (downloaded bytes that are not the file listed) are retryable; `refused`, any other status, an expired link among them, is not. It carries `operation`, `code`, `sha256`, `path` and `status`, and neither the link, nor storage's error body, nor a cause ([method-trees.md](./method-trees.md#when-a-transfer-fails)). |
|
|
58
58
|
| `ArtifactOperationError`, `ScopeUnavailableError` | `runtime` | no | The family's base also takes an optional `verdict`: the SDK declares `input` where it refuses an argument (a `scope`, a bound, a location) and `config` where the environment refuses the download (a runtime with no filesystem, a directory that cannot be created). |
|
|
59
59
|
| `ArtifactAuthenticationError` | `config` | no | The credential must change. Its `verdict` member is the download's result so far, not the error's. |
|
|
60
60
|
| `ArtifactFetchError` | by `code` | by `code` | `invalid_storage_uri`, `forbidden`, `unsupported_url`, `not_found` and `too_large` are `input` and not retryable; `plain_http_refused` is `config` and not retryable; `redirect_refused` and `store_refused` are `runtime` and not retryable; `timeout` and `network` are `runtime` and retryable; `store_error` is `runtime`, and retryable when [the fallback table](#a-refused-requests-verdict) would call its `status` retryable: a `408`, a `429`, or a `5xx` other than `501`. |
|
|
@@ -327,15 +327,15 @@ A method refused for what it says, rather than for how its run went, comes back
|
|
|
327
327
|
| `message` | What is wrong, in words. |
|
|
328
328
|
| `error_type` | The error's code, such as `unknown_model`. |
|
|
329
329
|
| `pipe_code`, `concept_code`, `domain_code` | The pipe, concept and domain the error is in. |
|
|
330
|
-
| `source`, `field_path`, `field_name` | The file the error is in (
|
|
330
|
+
| `source`, `field_path`, `field_name` | The file the error is in (its tree path in the tree the request sent), and the field, as a path such as `pipe.summarize.model` and by name. |
|
|
331
331
|
| `missing_concept_code`, `missing_pipe_code`, `declared_concepts` | The concept or pipe a reference names that the bundle does not declare, and the concepts the domain does declare. |
|
|
332
332
|
| `variable_names` | The input variables an input error concerns. |
|
|
333
333
|
| `model_reference`, `model_type`, `suggestions` | On an `unknown_model` item: the model reference exactly as the author wrote it, the kind of model the field takes (`llm`, `img_gen`, …), and the close matches, each spelled as a reference the field accepts. |
|
|
334
|
-
| `template_file`, `template_location` | On a `template_check_warning` item, which rides the valid verdict's `warnings[]`: the template file as the `PipeDocGen` step names it, `null` for a step of the server's own library, and the place in it the template check's finding names, in the engine's own words, `null` when it names none ([`
|
|
334
|
+
| `template_file`, `template_location` | On a `template_check_warning` item, which rides the valid verdict's `warnings[]`: the template file as the `PipeDocGen` step names it, `null` for a step of the server's own library, and the place in it the template check's finding names, in the engine's own words, `null` when it names none ([`method-trees.md`](./method-trees.md#the-template-checks-warnings)). |
|
|
335
335
|
| `suggested_fix` | The runner's deterministic correction, when it has one: a `description` in words, `ops` that patch the `.mthds` document by table and key rather than by text, `safety` (`safe` to apply without asking, or `unsafe`) and the `source` file the ops apply to. An unknown model with exactly one close match carries a `rename-model` fix that remaps the reference to it. |
|
|
336
336
|
|
|
337
337
|
```ts
|
|
338
|
-
const report = await client.validate(
|
|
338
|
+
const report = await client.validate({ directory: "methods/receipts" });
|
|
339
339
|
if (!report.is_valid) {
|
|
340
340
|
for (const item of report.validation_errors) {
|
|
341
341
|
console.error(`${item.source ?? "?"} ${item.pipe_code ?? ""}: ${item.message}`);
|
package/docs/hook-bundle.md
CHANGED
|
@@ -6,7 +6,33 @@
|
|
|
6
6
|
|
|
7
7
|
Each file goes through three stages: a local lint that blocks on any diagnostic, a local format that rewrites the file in place when its layout changes, and a validation by the hosted API (`POST /v1/validate`) that blocks on an invalid verdict and adds a non-blocking note about pending pipe signatures. The hook fails open: when the engine cannot load, when `PIPELEX_API_KEY` is unset or the API cannot be reached, the stage that could not run passes silently and the others stand. When several files are checked, one block among them wins and the notes are joined, and each file is read when its turn comes, so an edit made to it while an earlier file was being validated is checked rather than overwritten. The hook always exits 0, since its verdict is on stdout.
|
|
8
8
|
|
|
9
|
-
The validation sends the
|
|
9
|
+
The validation sends the method's tree around the file, the tree a run of the method would send: the edited file's directory listed by the `mthds` package's lister under the MTHDS standard's membership rule, so it is rooted at the nearest `METHODS.toml` at or above that directory, else at the directory itself, and holds every file the rule keeps — every bundle of the method, and every file a step reads, such as the template a `PipeDocGen` step names in `template_file`, under its tree path — while hidden entries, `node_modules`, `__pycache__`, what a `.gitignore` inside the tree keeps out and a run's inputs file never travel ([`method-trees.md`](./method-trees.md#the-four-forms-of-a-tree)), and neither does a file that looks like a secret ([below](#secrets-stay-home)). A method that fills a template thus validates as it would load from disk, its template checked by the dry run, and a step whose template file the tree does not hold makes the verdict say so, which blocks until the file is written. The client sends the tree inline when small and by hash otherwise, uploading the files the API lacks. The whole validation runs under one ten-second deadline, the version handshake, the blob route, the uploads and the validation request together, and the deadline stops whichever of them is under way when it runs out. Validation passes silently, as when the API cannot be reached, when that deadline runs out, when the tree holds a symbolic link or a directory the lister cannot read, when the edited file is not in its own tree because the rule keeps it out, when a file of the tree changes between its listing and its reading, when a file of the tree fails to upload, and past the hook's own caps: 100 files, 5 MiB for one of them or 12 MiB in all, each checked while the tree is listed, before any file is read.
|
|
10
|
+
|
|
11
|
+
## Secrets stay home
|
|
12
|
+
|
|
13
|
+
When a `PIPELEX_API_KEY` is set, and before the validation sends anything, the hook reads every file of the tree once, after the format write-back, and screens it with the workshop's secret guard, the rule pipelex-mcp applies to every tree it sends, in a copy (`src/hooks/secret-guard.ts`) that is ahead of pipelex-mcp's `src/capabilities/secret-guard.ts` on the SSH key names and the PEM private key block below, until the workshop takes them up. The bytes screened are the bytes sent, never read again. The screen is needed because the listing reads only the `.gitignore` files inside the tree, so a repository's own `.gitignore` above the method's directory protects nothing in it, and the hook sends every file of the tree, not only its bundles. A `.env` never travels in any case, since the membership rule keeps out every name starting with a dot. With no key, the hook neither reads nor sends any file of the tree.
|
|
14
|
+
|
|
15
|
+
A file looks like a secret when its name, matched on its last segment ignoring case, is a known secret file's, whatever it holds: a `.key` file that is no zip archive (a Keynote deck is one), a key or certificate store (`.p12`, `.pfx`, `.jks`, `.keystore`), a PuTTY `.ppk`, a KeePass `.kdbx`, an SSH private key under exactly its default name (`id_rsa`, `id_dsa`, `id_ecdsa` or `id_ed25519`, with the `_sk` of a hardware-backed key, while its public `.pub` half and a file such as `id_rsa_utils.py` go), a `credentials` or `credentials.json` file, or an OAuth `client_secret…json` export. It also looks like one when its bytes hold a well-known key format, in any encoding, UTF-16 included: a PEM private key block, its header followed by a key's body where a key writes it, at the start of a line soon after the header, as a key file writes it, an encrypted legacy key after its `Proc-Type` and `DEK-Info` lines and a key escaped into JSON after a `\n`, or directly on the header's line, as a key flattened onto one line writes it, so a prompt that names the header in prose goes, even with a long path or link after it in the sentence; an AWS access key id, a GitHub, GitLab, Slack or npm token, a Stripe live key, a Google, Anthropic, OpenAI or Hugging Face API key, or a Pipelex API key. Each format is the prefix its provider puts on its keys so that scanners find them, with enough of the key after it that prose naming the prefix does not match. A `.pem` file is judged by what it holds, since one is as often a public certificate, and a service-account export is found by the private key block inside it. The rule is a tripwire for the well-known shapes, never a promise that a tree holds no secret: a password written in prose goes through it.
|
|
16
|
+
|
|
17
|
+
What the hook does with such a file depends on whether every validation reads it:
|
|
18
|
+
|
|
19
|
+
| The file | What the hook does |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| Neither a bundle nor the manifest, such as a key file beside the method, a data file, a template or a Python module | It stays home, and the method is validated without it. When the validation returns a verdict, the verdict decides as usual, and a note after the verdict's text names each file kept home: non-blocking beside a valid verdict, and inside the block of an invalid one. When it returns none, because the API cannot be reached, answers with an error or runs past the deadline, the validation passes silently as it always does then, and no note is shown, though the file was never sent. |
|
|
22
|
+
| A `.mthds` bundle, or the `METHODS.toml` manifest at the tree's root | Nothing is sent, not even the version handshake, because every validation reads the method's bundles and its manifest, so no tree without one is the method. A non-blocking note names the method by its tree's root and every file of the tree that looks like a secret, the bundles and the manifest first, in place of a verdict. |
|
|
23
|
+
|
|
24
|
+
Leaving a file out rather than refusing the tree keeps the hook validating in the common case of a key file that merely sits beside a method, which no step reads and whose absence changes nothing in the verdict. A file a step does read, a `PipeDocGen` step's template or a `PipeFunc` step's module, is one the step cannot load without, so the verdict comes back invalid and blocks. The note, placed after that verdict's errors, says the hook kept the file home, so the agent reads the error as the hook's doing, and the fix it points to is the one the method needs anyway, since a run sends the same tree: move the secret out of the file.
|
|
25
|
+
|
|
26
|
+
A note names each file by its absolute path and what matched, **never the matched text**, since the hook's output is shown to a model and may be logged. Each path, and the root a refusal names, is written as a Markdown code span that no name can break out of: a control character in it, a line break included, is shown as an escape such as `\n`, and the span is fenced by more backticks than any run inside the path, so a file's name cannot add a line of its own to what the agent reads. The path is under the tree's root as the caller reached it, through a link above the root when that is how the edited file was named, and under the root's real path when a link points into the tree, since the caller's path then spells no way to the root. A note names the first ten files and counts the rest, and it never takes more than a share of the hook's output, so a verdict's errors keep their place and every character the note leaves them. Several files of one tree checked in one call, as a Codex patch may write, name the tree's note once, and a note is kept when another file's check blocks, since what stayed home matters whatever that check found. The reported case, a method directory holding its bundle, the template the bundle's `PipeDocGen` step fills, a `key.pem`, a `secrets.yaml` holding keys and a `service-account.json` export, sends the bundle and the template, and the valid verdict comes with this note:
|
|
27
|
+
|
|
28
|
+
> The .mthds hook kept these files of the method's tree on this machine, since each looks like a secret, and validated the method without them:
|
|
29
|
+
> - `/work/methods/foo/key.pem` (it holds a private key block (PEM))
|
|
30
|
+
> - `/work/methods/foo/secrets.yaml` (it holds an AWS access key id and an Anthropic API key)
|
|
31
|
+
> - `/work/methods/foo/service-account.json` (it holds a private key block (PEM))
|
|
32
|
+
>
|
|
33
|
+
> If a step reads one of them, as its template or its Python module, the validation reports that file missing until the secret is moved out of it. A key belongs in the environment, never in a method: a method's files leave this machine for the Pipelex API, which keeps them in the organization's storage, where every member can read them. Move the secret out of the file, or keep the file out of the method's tree with a `.gitignore` line inside the tree, since one above the tree's root is not read, or with a name starting with a dot. A file kept home for its name alone that holds no key can be renamed.
|
|
34
|
+
|
|
35
|
+
The manifest's `exclude` keeps a file out of the tree as well. A file kept out by any of these ways is never read by the hook, and so never named.
|
|
10
36
|
|
|
11
37
|
## Which file it checks
|
|
12
38
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Status: implemented** (`src/upload.ts`, `src/upload-grant.ts`, `src/prepare-inputs.ts`). This document records the contract, and is the public account of it. The raw `upload()` primitive described in [architecture.md](./architecture.md) is the wire call `uploadFile` and `prepareInputs` build on.
|
|
4
4
|
>
|
|
5
|
-
> **Current scope.** `prepareInputs` names the method the same three ways every other method-taking operation does —
|
|
5
|
+
> **Current scope.** `prepareInputs` names the method the same three ways every other method-taking operation does — the method's tree, a `method_ref` address, or a stored `method_id` — exactly one per call, all three resolved server-side by the one `POST /v1/pipe-io` call it composes. One piece remains deliberately deferred and additive (it does not change this contract): the opt-in ingest of `http(s)` URLs into storage — for now an `http(s)` URL at a file position always passes through unchanged.
|
|
6
6
|
|
|
7
7
|
## Why this exists
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ Uploads one asset and returns its upload record. It is the language-native conve
|
|
|
21
21
|
- A string that is an **HTTP(S) URL** or an existing **`pipelex-storage://` URI** is not a local asset and passes through in any runtime (see pass-through rules below).
|
|
22
22
|
- Open file objects and streams are **deferred** — they can be added later without removing anything.
|
|
23
23
|
|
|
24
|
-
> **Runtime acceptance vs. bundling.** These source types are accepted at *runtime* in every environment,
|
|
24
|
+
> **Runtime acceptance vs. bundling.** These source types are accepted at *runtime* in every environment, and the main `@pipelex/sdk` entry bundles for a browser or edge target with nothing marked external: such a bundle resolves the Node host to its empty variant, a module holding no Node built-in ([architecture.md](./architecture.md#the-node-host)), so path strings stay Node-only, and where the host is absent a path string is refused with `InvalidLocalSourceError`, never read. The client still belongs on a server, since it holds an API key (Next.js Server Actions, the MCP server, the plugins hook), so a page imports the main entry for its error classes and types, and a page that must store a file uses the upload grant below instead, whose sender ships from the `@pipelex/sdk/upload` entry.
|
|
25
25
|
|
|
26
26
|
The returned **upload record** guarantees, beyond the source identity:
|
|
27
27
|
|
|
@@ -66,7 +66,7 @@ It sends the file as the raw body of a `PUT` to `grant.url`, with `grant.headers
|
|
|
66
66
|
|
|
67
67
|
**The time limit.** The whole exchange with storage runs under a time limit — the request, the file going out, the answer coming back and the read of an error body — so a stalled upload settles even when the caller passes no signal. By default the limit is 60 s plus 1 s for every started 128 KiB of the file, which assumes about 1 Mbit/s and comes to 460 s for a 50 MiB file. `timeoutMs` replaces the default, for a slower link or a caller that wants a tighter bound; it must be a positive number no larger than 2147483647, the longest delay a timer honours, or the call throws an `InputPreparationError` before anything is sent. A `signal` ends the upload whenever it fires, so it can shorten the limit but never lengthen it, and a caller's abort still propagates as the signal's own reason even when the limit ran out too. When the limit runs out before storage answers, the call throws an `UploadTransportError` whose `code` is `timeout` and whose message names the seconds allowed. When it runs out while storage's error body is still arriving, storage has already answered, so the call settles as that answer instead, classified from its status and whatever part of the body arrived. After a timeout, whether storage stored the file is unknown: retrying with the same grant before it expires either stores the file or answers the `412` of a used grant, and then the grant's `uri` already names the file. The grant's expiry does not bound the upload, because storage checks the signature when the request starts, not when it ends.
|
|
68
68
|
|
|
69
|
-
**The browser-safe entry.** `uploadWithGrant`, the grant types and the three error classes it raises (`InputPreparationError`, `RejectedAssetError`, `UploadTransportError`) are exported from `@pipelex/sdk/upload`, whose import graph reaches no Node builtin, no `undici` and not the client, so a page bundles it with nothing marked external. They are the same function and the same classes the main entry exports, so an `instanceof` check agrees whichever entry the error came through. The unit suite bundles
|
|
69
|
+
**The browser-safe entry.** `uploadWithGrant`, the grant types and the three error classes it raises (`InputPreparationError`, `RejectedAssetError`, `UploadTransportError`) are exported from `@pipelex/sdk/upload`, whose import graph reaches no Node builtin, no `undici` and not the client, so a page bundles it with nothing marked external. They are the same function and the same classes the main entry exports, so an `instanceof` check agrees whichever entry the error came through. The unit suite bundles both entries for the browser with esbuild and fails if anything Node-only becomes reachable.
|
|
70
70
|
|
|
71
71
|
**Storage's refusals** map onto the input-preparation errors, so a caller that already handles `uploadFile` needs no new case:
|
|
72
72
|
|
|
@@ -93,7 +93,7 @@ It sends the file as the raw body of a `PUT` to `grant.url`, with `grant.headers
|
|
|
93
93
|
### `prepareInputs` — signature-driven input preparation
|
|
94
94
|
|
|
95
95
|
```
|
|
96
|
-
client.prepareInputs({ files,
|
|
96
|
+
client.prepareInputs({ directory | files | tree | contents, dependencies?, pipe_ref?, inputs }) → PreparedInputs
|
|
97
97
|
client.prepareInputs({ method_ref, pipe_ref?, inputs }) → PreparedInputs
|
|
98
98
|
client.prepareInputs({ method_id, pipe_ref?, inputs }) → PreparedInputs
|
|
99
99
|
```
|
|
@@ -105,19 +105,19 @@ Takes the **method** as exactly one of three selectors, the optional target **pi
|
|
|
105
105
|
|
|
106
106
|
The prepared `inputs` are passed to the existing run lifecycle unchanged.
|
|
107
107
|
|
|
108
|
-
**Stopping it.** An optional `signal` stops the preparation between its steps: once it has aborted, neither the pipe I/O request nor any upload starts, and `prepareInputs` throws the abort, as the run wait does. It is read again once each file's bytes are in hand, right before its upload request, so a file whose read outlasted the caller's patience is not sent; `uploadFile` takes the same `signal` in its options for that check. A request already sent runs to its end, so an upload under way finishes, but the file after it is not sent. The signal is not sent to the server.
|
|
108
|
+
**Stopping it.** An optional `signal` stops the preparation between its steps: once it has aborted, neither the pipe I/O request nor any upload starts, and `prepareInputs` throws the abort, as the run wait does. It is read again once each file's bytes are in hand, right before its upload request, so a file whose read outlasted the caller's patience is not sent; `uploadFile` takes the same `signal` in its options for that check. A request already sent runs to its end, so an upload under way finishes, but the file after it is not sent. A `directory`, `tree` or `contents` adds steps before the pipe I/O request — the version handshake, the directory's listing, and for a tree sent by hash the blob route and each read and upload of a missing blob — and the signal is read before each of them too and handed to each, through the route's `CrateRouteOptions`, so an abort stops the one under way, a blob's upload included: one during the handshake stops it and leaves the blob route, the uploads and the pipe I/O request unasked. The signal is not sent to the server.
|
|
109
109
|
|
|
110
110
|
#### The three selectors
|
|
111
111
|
|
|
112
|
-
Exactly one per call. The type pins the other two to `never`, so a second selector is a compile error; an untyped caller gets an `InputPreparationError` naming the three forms. **Empty is absent** — `files:
|
|
112
|
+
Exactly one per call. The type pins the other two to `never`, so a second selector is a compile error; an untyped caller gets an `InputPreparationError` naming the three forms. **Empty is absent** — `files: {}`, `contents: {}`, `directory: ""`, `method_ref: ""`, `method_id: " "` — mirroring the run options' rule, so an empty selector may sit beside a real one without tripping the XOR. A `files` that is not a map, such as the retired list of `{ content, source }` items, is an `InputPreparationError` rather than an absent selector, and so, before any request, is a `directory` that is not a string, a `contents` that is not a map of text or bytes, a `dependencies` that is not a map of file maps, and a `tree` that is not a listed tree (its `files`, its `digest`, a `readFile`) or holds no file.
|
|
113
113
|
|
|
114
114
|
| Selector | What it is | Who resolves it |
|
|
115
115
|
| --- | --- | --- |
|
|
116
|
-
| `files`
|
|
116
|
+
| the tree | the method's tree, in exactly one of the forms the crate routes take: `files`, the protocol's map from tree path to file entry; `directory`, the method's directory on disk, listed by the `mthds` package's lister (Node only); `tree`, a listed `MethodTreeSource`; or `contents`, each file's bytes or text by tree path ([crate-routes.md](./crate-routes.md#the-shared-envelope)) | nobody — sent with the request, a `directory`, `tree` or `contents` inline when small and by hash otherwise |
|
|
117
117
|
| `method_ref` | a published method's address, `github.com/<owner>/<repo>[/<selector>][@<tag>]` | the runner, server-side (pipelex-api >= 0.21.0 fetches the repository at the tag) |
|
|
118
|
-
| `method_id` | a stored method's catalog id (`mt_…`, `mt_…@<n>` or `mt_…@draft`) | the hosted platform, which injects the
|
|
118
|
+
| `method_id` | a stored method's catalog id (`mt_…`, `mt_…@<n>` or `mt_…@draft`) | the hosted platform, which injects the tree of the version it names before the runner sees the request |
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
The tree carries every file the method's steps read, such as a `PipeDocGen` step's `template_file`, at the tree path the step names ([method-trees.md](./method-trees.md#the-files-a-step-reads)), and it may bring `dependencies`, the method packages it calls by address, each a `files` map keyed by its address. They ride the one pipe I/O request, which loads the closure and fails to load it, as a `MethodLoadError`, when a step's template file is not in the tree or a call reaches a package nothing shipped. A published or stored method's packages are its own, so `dependencies` beside `method_ref` or `method_id` is an `InputPreparationError` before any request, and so is the tree given in two forms.
|
|
121
121
|
|
|
122
122
|
Nothing is expanded client-side: `method_id` here is a **pass-through**, the same rule every other id-taking operation follows (see "[A method's files in hand](#a-methods-files-in-hand)").
|
|
123
123
|
|
|
@@ -126,12 +126,12 @@ Nothing is expanded client-side: `method_id` here is a **pass-through**, the sam
|
|
|
126
126
|
One `POST /v1/pipe-io` per call, whatever the selector, through the client's own `pipeIo` (see [crate-routes.md](./crate-routes.md#pipeio--a-methods-io-artifacts-without-a-validation)):
|
|
127
127
|
|
|
128
128
|
```
|
|
129
|
-
pipeIo({ <files [+
|
|
129
|
+
pipeIo({ <directory | files | tree | contents [+ dependencies] | method_ref | method_id>, pipe_ref? })
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
The route resolves the closure, **selects the pipe**, and answers with that pipe's **input-form descriptor** under the qualified ref it resolved. It runs no dry run, so preparing inputs costs one static load rather than a validation of the whole method. It also refuses nothing for a pending signature elsewhere in the method: preparation needs a pipe's *declared* inputs, and whether the method runs is the run's verdict, not preparation's. An `is_valid: false` verdict still means the closure does not load, which is a preparation failure.
|
|
133
133
|
|
|
134
|
-
A `
|
|
134
|
+
A `directory`, `tree` or `contents` too large to send inline is uploaded first, by hash, and a blob that fails to upload throws `MethodFileTransferError` before the route is asked. A `method_ref` makes the server clone a repository first, so the call gets the crate routes' fetch-sized budget. On the hosted API the gateway caps a request at about 30 seconds whatever the client allows, so a cold clone can answer a `502` `ApiResponseError`; a retry clears it once the runner has cached the clone.
|
|
135
135
|
|
|
136
136
|
`prepareInputs` needs an API that serves `POST /v1/pipe-io`. Against one that does not, the route's `404` (a runner) or `403` (a hosted origin whose gateway does not list it yet) propagates as an `ApiResponseError`. It also needs the route's pipe-selection refusals typed, as step 3 of "Pipe selection" below describes, which `pipelex-api` does from v0.33.1: v0.33.0, the release that added the route, typed them as the generic `ValidationError`, so against it a refused selection reaches the caller as that `ApiResponseError` rather than an `InputPreparationError`.
|
|
137
137
|
|
|
@@ -139,8 +139,8 @@ A `method_ref` makes the server clone a repository first, so the call gets the c
|
|
|
139
139
|
|
|
140
140
|
The route selects the pipe, and the ref it answers with is the one prepared — the helper derives no default of its own:
|
|
141
141
|
|
|
142
|
-
1. **`pipe_ref` when given.** Qualified-only: `domain.pipe_code`, normalized (trimmed, and blank is absent) and checked **before any request**. A bare code is refused with an `InputPreparationError` asking for the qualified form, since a request names a pipe by its qualified ref and search is a run-route affordance this helper never grows. An `alias->domain.pipe_code` ref is refused for a different reason: the alias names a dependency package's pipe, and preparation covers the method's *own* pipes
|
|
143
|
-
2. **Otherwise the method's own entry pipe**, chosen by the route:
|
|
142
|
+
1. **`pipe_ref` when given.** Qualified-only: `domain.pipe_code`, normalized (trimmed, and blank is absent) and checked **before any request**. A bare code is refused with an `InputPreparationError` asking for the qualified form, since a request names a pipe by its qualified ref and search is a run-route affordance this helper never grows. An `alias->domain.pipe_code` ref is refused for a different reason: the alias names a dependency package's pipe, and preparation covers the method's *own* pipes, whose input form the pipe I/O route describes. The run route takes such a ref; preparation cannot.
|
|
143
|
+
2. **Otherwise the method's own entry pipe**, chosen by the route: the `main_pipe` of the `METHODS.toml` at the tree's root, whether the tree was sent or fetched with a package, else the closure's `main_pipe` when exactly one domain declares one. A method that names its entry pipe in `METHODS.toml` alone — the published `github.com/Pipelex/methods/documents` does — therefore prepares with no `pipe_ref`.
|
|
144
144
|
3. **A selection the route refuses** — a `pipe_ref` that names no pipe of the method, a method with no entry pipe, one whose domains declare several, a manifest `main_pipe` the closure does not declare or declares in several domains — comes back as a `422` whose `errorType` is `EntryPipeNotFoundError` or `EntryPipeAmbiguousError`. The helper turns exactly those into an `InputPreparationError` carrying the server's detail, which names the candidates where there are some, with the `ApiResponseError` as its `cause`. The run route refuses a selector-less run in the same cases, so preparation never succeeds for a pipe the run would not take.
|
|
145
145
|
|
|
146
146
|
There is no single-pipe fallback: a method that declares one pipe and no `main_pipe` is refused a selector-less run, and a selector-less preparation with it.
|
package/docs/method-trees.md
CHANGED
|
@@ -1,28 +1,119 @@
|
|
|
1
|
-
# A
|
|
1
|
+
# A method's tree
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A method is a **method tree**: the files it is made of, each at its tree path — its bundles, its manifest when it has one, and every file a step reads or that travels with the method, such as a `PipeDocGen` step's template. A tree is named by its **tree digest**, `sha256:` and the SHA-256 of its listing, so two methods holding the same files hold the same digest. Both are defined by the MTHDS standard (`mthds/docs/spec/method-tree.md`), and on the wire a tree is the protocol's `files` map, each key a tree path and each value the file's text, `{ base64 }` for bytes that are not text, or `{ sha256, size }` for bytes the receiver already holds.
|
|
4
4
|
|
|
5
|
-
The platform keeps each file once per organization, as a blob named by its SHA-256, so a
|
|
5
|
+
Every call that takes a method takes its tree the same way: the runs (`execute`, `start`, `startAndWaitForResult`), `validate`, the crate routes (`resolve`, `codegen`, `pipeIo`), `prepareInputs`, and the saves (`createMethod`, `writeDraft`). A saved method holds one too: the draft holds one, and each published version holds the one it was published from. The platform keeps each file once per organization, as a blob named by its SHA-256, so a request sends only the files the organization does not hold yet, and a read lists the files with a short-lived link to each one's bytes.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## The four forms of a tree
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
A call takes the tree in exactly one of four forms (`MethodTreeForms`):
|
|
10
10
|
|
|
11
11
|
| Form | What it is | How it is sent |
|
|
12
12
|
|---|---|---|
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
13
|
+
| `directory` | The method's directory on disk. **Node only.** | Listed, then inline when small and by hash otherwise (below). |
|
|
14
|
+
| `contents` | Each file's bytes (`Uint8Array`) or its text, by tree path, a map held in memory. | Inline when small, by hash otherwise. |
|
|
15
|
+
| `tree` | A `MethodTreeSource`: a tree's `files` (each with its `path`, `size` and `sha256`), its `digest`, and `readFile(path)` giving each file's bytes. The `MethodTree` the `mthds` package's `listMethodTree` returns is one. | Inline when small, by hash otherwise. |
|
|
16
|
+
| `files` | The protocol's map of file entries (`MethodFiles` from `mthds/protocol`). | As given. |
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// A run of the method in a directory: the client lists it, and sends what the API lacks.
|
|
20
|
+
const results = await client.startAndWaitForResult({
|
|
21
|
+
directory: "methods/receipts",
|
|
22
|
+
inputs: { receipt: "https://example.com/receipt.png" },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// The same method validated from memory, its template among its files.
|
|
26
|
+
const verdict = await client.validate({
|
|
27
|
+
contents: {
|
|
28
|
+
"receipts.mthds": source,
|
|
29
|
+
"templates/receipt.docx": await readFile("methods/receipts/templates/receipt.docx"),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**A directory is listed by the standard's lister.** The client loads the `mthds` package's `listMethodTree` when a call gives a `directory`, and lists it under the MTHDS standard's membership rule and Manifest Discovery: the tree is rooted at the nearest `METHODS.toml` at or above the directory, else at the directory itself, and holds every file the rule keeps. Hidden entries, `node_modules`, `__pycache__`, what a `.gitignore` or the manifest's `exclude` keeps out, and **a run's inputs file** (`inputs.json`, `inputs.toml`) never travel with the method. A tree holding a symbolic link or a special file is refused rather than followed. The directory is listed under the limits the deployment announces (below), so a tree the deployment would refuse for its size is refused while it is listed, before any file is hashed. Every refusal is a `RequestArgumentError` naming the call and the directory, whose `cause` is `mthds`'s `MethodTreeError`, with its `code` (`not_found`, `symbolic_link`, `limit_exceeded`, `empty_tree` and the rest). Listing needs Node: the lister comes from the Node host ([architecture.md](./architecture.md#the-node-host)), so a bundle built for the browser holds none of it, and a `directory` given outside Node is refused with `RequestArgumentError` before the version handshake, so before any request, and a browser gives `contents`, `tree` or `files`.
|
|
35
|
+
|
|
36
|
+
**A map in memory.** A text in `contents` is encoded in UTF-8, and a text a file cannot hold, one carrying a lone surrogate, is refused, as is a value that is neither a string nor a `Uint8Array`. The keys are sent as they are, and the platform refuses one that is not a tree path with the `422` `invalid_method_files` naming it.
|
|
37
|
+
|
|
38
|
+
**One form, and never an empty tree.** Two forms at once are refused with a `RequestArgumentError` before anything is read or sent. A `files` map, `contents` or a `tree` holding no file is refused too on the run routes and on `validate`, as the standard refuses it: a tree holds at least one file. On the crate routes and in `prepareInputs`, an empty map is absent instead, so it never reaches the wire as a second selector, and in a save `files: null` empties the draft. An empty `directory` string is absent everywhere.
|
|
39
|
+
|
|
40
|
+
## How a tree travels
|
|
41
|
+
|
|
42
|
+
A tree given as a `directory`, `contents` or `tree` is sent by the rule the `mthds` package's `MthdsApiClient` follows, so the tree a save sends is the tree a run would send. The client reads the API's version handshake first (`GET /v1/version`) and keeps its answer for five minutes, so every call in that time reads the same answer and a deployment that changes is read afresh soon after ([below](#the-deployments-limits)):
|
|
43
|
+
|
|
44
|
+
- **When the API keeps a blob store** (`blob_store: true`), a tree whose inline map weighs no more than the inline threshold travels **inline**, with no further round trip. The map is weighed as the request spends it, its JSON with no whitespace, in UTF-8, and the threshold is the lower of `INLINE_TREE_MAX_BYTES` (256 KiB) and the `max_inline_bytes` the handshake announces. Inline, a file whose bytes are UTF-8 travels as its text and any other as `{ base64 }`, so a binary file weighs its Base64. A heavier tree travels **by hash**: the client asks `POST /v1/blobs/missing` which files the receiver lacks, naming the tree's digest, uploads each one it lacks once, at most eight at a time, straight to the upload target the platform names, under the client's request time limit (`requestTimeoutMs`, twenty minutes unless the client was built with another), and sends every file as `{ sha256, size }`. A file uploaded once is never uploaded again, by this method or any other of the organization. A `404` from the blob route says the kept handshake is stale, the API having lost its blob store since it was read: the client forgets the handshake and prepares the tree once more on a fresh one.
|
|
45
|
+
- **When the API keeps no blob store**, which a bare runner usually does, the whole tree travels inline whatever its weight, held to the `max_inline_bytes` the handshake announces: a heavier tree is refused with a `RequestArgumentError` before anything is sent, since the API would refuse it. The files' listed sizes are checked first, since the inline map weighs at least that much, so a tree whose files alone pass the limit is refused before any of them is read; the message says which was measured, the files alone or the inline map, with its weight and the limit.
|
|
16
46
|
|
|
17
|
-
A
|
|
47
|
+
A `files` map is sent as given, with no handshake. A run's `start` carrying a tree, a run that names a `method_ref`, and a start whose `extra` carries a method bundle (`mthds_contents` or `bundle_b64`, as the `pipelex-sdk` command sends one) get the client's request ceiling rather than the 30-second start budget, since the body can be large. On the crate routes, a request whose `files` carry more than `INLINE_TREE_MAX_BYTES` inline, as a tree sent to an API keeping no blob store or a `files` map given as is may, gets the request ceiling too rather than the 30-second budget of a static route ([crate-routes.md](./crate-routes.md)). A save carrying a tree (`createMethod`, `writeDraft`), inline or by hash, gets the request ceiling as well, rather than the 30-second budget of a save sending none, since a tree sent inline to an API keeping no blob store can be large and its whole upload is charged against the budget.
|
|
18
48
|
|
|
19
|
-
A
|
|
49
|
+
**A refusal of the tree's size is sent once more.** When the API refuses a tree the client prepared, a `directory`, `contents` or `tree`, as past one of its limits (a `422` coded `method_tree_too_large`, or `method.package_invalid` naming a limit, or of type `MethodTreeTooLarge`), the handshake that announced the limits may be stale: the client forgets it, prepares the tree again on a fresh one and sends the request once more, as `mthds`'s client does. A `files` map given as is was not prepared from the handshake, so it is not sent again, and neither is a `start` under an idempotency key, since the platform binds the key to the refusal it stores.
|
|
50
|
+
|
|
51
|
+
**Stopping a call.** A call that takes a `signal` (`validate`, `startAndWaitForResult`, the crate routes, `prepareInputs`) reads it before each step a `directory`, `contents` or `tree` costs and hands it to the step's request, so an abort stops the version handshake, the directory's listing, the blob route, a file's read and an upload already under way, and nothing further starts.
|
|
52
|
+
|
|
53
|
+
A `MethodTree` reads a file again whenever its bytes are sent, inline or as an upload, and checks it against the hash its listing recorded, so a file changed since the listing is refused by `mthds` rather than sent as another tree than the one listed. Apart from listing a directory, sending a tree reads no Node module: hashing is Web Crypto's, and Base64 is written by the SDK.
|
|
54
|
+
|
|
55
|
+
**What is uploaded is what the listing names.** Before each upload the client hashes the bytes a tree's `readFile` gives and refuses them, with a `RequestArgumentError`, unless they have the size and the SHA-256 the tree lists for them: a blob is stored under its hash for the whole organization, so a hand-built `MethodTreeSource` whose `readFile` disagrees with its listing never stores other bytes under that hash. An answer of `POST /v1/blobs/missing` asking for a blob the tree does not hold is the platform's fault, not the caller's: it is an `ApiResponseError` of an answer the SDK cannot read, verdict `runtime` and not retryable, thrown before any upload. An upload that fails throws `MethodFileTransferError` with `operation` `upload`, and the request is not sent or the method not saved ([below](#when-a-transfer-fails)).
|
|
56
|
+
|
|
57
|
+
**A blob that went missing.** A request whose hash entries name a blob the API does not hold is refused with a `422` `missing_blobs` listing them all, which also releases a start's idempotency key. A request built from a `directory`, `contents` or `tree` uploads every blob it names first, so it meets this only if a blob was removed between the upload and the request. The client answers it as `mthds`'s client does: a `422` problem coded `request.blobs_missing`, whose `params.missing_blobs` names blobs the tree holds, has those blobs asked of the blob route again, uploaded and the request sent once more, while the platform's `missing_blobs` code is not read as one and is thrown, so sending the same call again asks the blob route afresh and uploads what is missing.
|
|
58
|
+
|
|
59
|
+
On the crate routes, `read_scope` is the scope the blob route is asked under, so the blobs are uploaded into the store the request's hash entries are read from; the hosted platform reads the caller's organization whatever the request says ([crate-routes.md](./crate-routes.md#the-shared-envelope)).
|
|
60
|
+
|
|
61
|
+
## The files a step reads
|
|
62
|
+
|
|
63
|
+
A file a step reads, such as a `PipeDocGen` step's `template_file` (the Word, Excel or PowerPoint template it fills, or the HTML template it prints as a PDF), is a file of the tree like any other, at the tree path the step names from the root. A `directory` sends it with the bundles; a `contents` or `files` map carries it under its tree path. A step that names a file the tree does not hold cannot load: on `validate`, `pipeIo`, `resolve` and `codegen` that is an `is_valid: false` verdict whose validation error says the file is missing, `prepareInputs` throws it as a `MethodLoadError`, and on `execute` and `start` it is a `422` `ValidateBundleError`, whose `validationErrors` say the same.
|
|
64
|
+
|
|
65
|
+
### The template check's warnings
|
|
66
|
+
|
|
67
|
+
A dry run hands an office template file of the tree to the document engine's template check. An error finding fails the step's dry run, and so the verdict, as a `dry_run` validation error on the step. A warning finding, such as a field of the template that matches no input, rides the valid verdict's `warnings` as a `template_check_warning` item:
|
|
68
|
+
|
|
69
|
+
| Member | Value |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `category` | `dry_run` |
|
|
72
|
+
| `error_type` | `template_check_warning` |
|
|
73
|
+
| `pipe_code`, `domain_code` | The `PipeDocGen` step |
|
|
74
|
+
| `template_file` | The template file as the step names it; `null` for a step of the server's own library, whose path is never shown |
|
|
75
|
+
| `template_location` | The place in the file the finding names, in the engine's words (a defined name, a table, a cell, a shape, a tag); `null` when it names none |
|
|
76
|
+
| `message` | The finding in words |
|
|
77
|
+
|
|
78
|
+
The warnings come in families, always in this order: `optional_force_redundant`, `input_presence_vacuous`, the `hint_*` lints, then `template_check_warning`. None of them flips `is_valid`.
|
|
20
79
|
|
|
21
80
|
```ts
|
|
22
|
-
|
|
81
|
+
if (verdict.is_valid) {
|
|
82
|
+
for (const warning of verdict.warnings) {
|
|
83
|
+
if (warning.error_type === "template_check_warning") {
|
|
84
|
+
console.warn(`${warning.template_file ?? "?"} ${warning.template_location ?? ""}: ${warning.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
23
89
|
|
|
24
|
-
|
|
25
|
-
|
|
90
|
+
### Which step reads which file
|
|
91
|
+
|
|
92
|
+
Whatever form the method was sent in, its tree in any of the four forms, a `method_ref` or a `method_id`, the valid verdict names the files of the method's tree each step reads. `tree_files_read` is keyed by the same namespaced `pipe_ref` as `pipe_io_contracts`, each entry a list of `TreeFileRead` items, and a step that reads no file, a controller included, has an empty list. `tree_file_readers` states the same facts the other way: keyed by tree path in sorted order, it lists for each file the refs of the steps that read it, each once, and a file of the tree with no entry, such as a bundle itself or a README, is read by no step.
|
|
93
|
+
|
|
94
|
+
| Member | Value |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `tree_path` | The file's path from the root of the method's tree, with forward slashes, never a path on the server |
|
|
97
|
+
| `use` | `template` for a `PipeDocGen` step's `template_file`; `template_asset` for an image, a stylesheet or a font its HTML template names by a literal relative URL, or one a stylesheet it names refers to; `python_module` for the module holding a `PipeFunc` step's function, when that module is a file of the tree |
|
|
98
|
+
|
|
99
|
+
The paths come from what each step already names, so a client can show which step uses a file, or warn before one a step reads is renamed or removed, without parsing the method.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
if (verdict.is_valid && verdict.tree_file_readers) {
|
|
103
|
+
for (const [treePath, pipeRefs] of Object.entries(verdict.tree_file_readers)) {
|
|
104
|
+
console.log(`${treePath} is read by ${pipeRefs.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`use` is typed `TreeFileUse | (string & {})`: a runner of a later version may add a use as another kind of step comes to read files of its tree, so a consumer switching on it says what a use it does not know does. Both fields are optional. A `pipelex-api` runner reports them from the first pipelex release after 0.90.0, and the hosted API once it runs such a release; an older runner sends neither, which reads as `undefined`, where an empty list says that the step reads no file.
|
|
110
|
+
|
|
111
|
+
## Saving a tree
|
|
112
|
+
|
|
113
|
+
`createMethod(input)` and `writeDraft(methodId, input)` take the tree in the four forms (`MethodTreeInput`), and send it by the rule above.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const method = await client.createMethod({ name: "Receipts", directory: "methods/receipts" });
|
|
26
117
|
|
|
27
118
|
const draft = await client.writeDraft(method.method_id, {
|
|
28
119
|
contents: { "main.mthds": source, "helpers/fx.py": helper },
|
|
@@ -30,9 +121,7 @@ const draft = await client.writeDraft(method.method_id, {
|
|
|
30
121
|
});
|
|
31
122
|
```
|
|
32
123
|
|
|
33
|
-
**A draft write replaces the whole tree.** `writeDraft` refuses a draft that gives no tree, and `files: null` empties the draft. `createMethod` with no tree creates a method with an empty draft.
|
|
34
|
-
|
|
35
|
-
**What is uploaded is what the listing names.** Before each upload the client hashes the bytes a tree's `readFile` gives and refuses them, with a `RequestArgumentError`, unless they have the size and the SHA-256 the tree lists for them: a blob is stored under its hash for the whole organization, so a hand-built `MethodTreeSource` whose `readFile` disagrees with its listing never stores other bytes under that hash. An answer of `POST /v1/blobs/missing` asking for a blob the tree does not hold is the platform's fault, not the caller's: it is an `ApiResponseError` of an answer the SDK cannot read, verdict `runtime` and not retryable, thrown before any upload.
|
|
124
|
+
**A draft write replaces the whole tree.** `writeDraft` refuses a draft that gives no tree, and `files: null` empties the draft. `createMethod` with no tree creates a method with an empty draft. A `contents` or `tree` holding no file is refused before anything is sent.
|
|
36
125
|
|
|
37
126
|
The save is refused, with the `ApiResponseError` of its problem, when the platform will not take the tree:
|
|
38
127
|
|
|
@@ -41,14 +130,21 @@ The save is refused, with the `ApiResponseError` of its problem, when the platfo
|
|
|
41
130
|
| `422` `invalid_method_files` | The map breaks the standard's rules, a path that is not a tree path or two paths that collide among them; the problem's `tree_refusal` names the rule. |
|
|
42
131
|
| `422` `method_tree_too_large` | The tree passes one of the deployment's limits, which the problem's `limit` names. |
|
|
43
132
|
| `422` `blob_mismatch` | A hash entry states another size than the blob the organization holds under that hash. |
|
|
44
|
-
| `422` `missing_blobs` | A hash entry names bytes the organization does not hold
|
|
133
|
+
| `422` `missing_blobs` | A hash entry names bytes the organization does not hold (above). |
|
|
45
134
|
| `413` `payload_too_large` | The method would leave no room for the summary a publish adds. |
|
|
46
135
|
|
|
47
|
-
An upload that fails throws `MethodFileTransferError` with `operation` `upload`, and the method is not saved (below).
|
|
48
|
-
|
|
49
136
|
## The deployment's limits
|
|
50
137
|
|
|
51
|
-
|
|
138
|
+
The version handshake (`GET /v1/version`) announces the limits the deployment holds a tree to under `limits.method_tree`: `max_files`, `max_file_bytes`, `max_total_bytes` and `max_inline_bytes`. A limit the server does not announce is absent, a value that is not a positive integer is left out rather than trusted, and none is ever assumed, so a bare runner, which announces none, yields no limit. The client keeps the handshake's answer for five minutes (`HANDSHAKE_TTL_MS` in `src/method-trees.ts`), so every call in that time reads the same answer, and asks again once it is older; a handshake that fails is asked again on the next call. A `404` from the blob route and a refusal of the tree's size each drop the kept answer at once, since each says it may be stale ([above](#how-a-tree-travels)).
|
|
139
|
+
|
|
140
|
+
`methodTreeLimits()` returns the three listing limits, `{ maxFileCount, maxFileSize, maxTotalSize }`, in the shape `listMethodTree` takes as its `limits` option, for a caller listing a tree of its own; a `directory` given to a call is listed under them already. Listed under those limits, a tree the platform would refuse for its size is refused while it is listed, with `mthds`'s `TreeLimitExceededError` naming the limit, before any file is hashed. The inline limit, `max_inline_bytes`, is no listing limit: the client applies it when it chooses how a tree travels.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { listMethodTree } from "mthds";
|
|
144
|
+
|
|
145
|
+
const tree = await listMethodTree("methods/receipts", { limits: await client.methodTreeLimits() });
|
|
146
|
+
const method = await client.createMethod({ name: "Receipts", tree });
|
|
147
|
+
```
|
|
52
148
|
|
|
53
149
|
## Reading a tree
|
|
54
150
|
|
|
@@ -70,7 +166,7 @@ A method whose draft names a tree the platform holds no record of answers `getMe
|
|
|
70
166
|
|
|
71
167
|
## Writing a tree to disk
|
|
72
168
|
|
|
73
|
-
`writeMethodTree(files, { destination })` writes a listing to disk, each file at its tree path under `destination`. It runs in Node only,
|
|
169
|
+
`writeMethodTree(files, { destination })` writes a listing to disk, each file at its tree path under `destination`. It runs in Node only, with the file system of the Node host ([architecture.md](./architecture.md#the-node-host)), and is refused with `RequestArgumentError` anywhere else, before any download:
|
|
74
170
|
|
|
75
171
|
```ts
|
|
76
172
|
const detail = await client.getMethod("mt_123");
|
|
@@ -83,7 +179,7 @@ Each file is downloaded through its link, at most eight at a time, with no crede
|
|
|
83
179
|
|
|
84
180
|
## When a transfer fails
|
|
85
181
|
|
|
86
|
-
`MethodFileTransferError` is thrown when a file could not be moved between this machine and storage. `operation` says which way (`upload` for a save, `download` for `writeMethodTree`), `code` why, `sha256` which bytes, `path` the tree path when one is known, and `status` storage's HTTP status when it answered. Neither its message nor any member carries the link, nor storage's error body, which can echo the signed request, and it keeps no `cause`: the failure `fetch` throws for a request that got no answer can quote the URL, so only its network code (`ECONNREFUSED`, `ABORT_TIMEOUT`) reaches the message.
|
|
182
|
+
`MethodFileTransferError` is thrown when a file could not be moved between this machine and storage. `operation` says which way (`upload` for a request or a save that sends a tree by hash, `download` for `writeMethodTree`), `code` why, `sha256` which bytes, `path` the tree path when one is known, and `status` storage's HTTP status when it answered. Neither its message nor any member carries the link, nor storage's error body, which can echo the signed request, and it keeps no `cause`: the failure `fetch` throws for a request that got no answer can quote the URL, so only its network code (`ECONNREFUSED`, `ABORT_TIMEOUT`) reaches the message.
|
|
87
183
|
|
|
88
184
|
| `code` | When | Retryable |
|
|
89
185
|
|---|---|---|
|
|
@@ -96,6 +192,8 @@ Its `errorDomain` is always `runtime`. An upload to a target that asks for a cre
|
|
|
96
192
|
|
|
97
193
|
## What changed from the source fields
|
|
98
194
|
|
|
195
|
+
A run and a validation used to take a method as inline `.mthds` texts (`mthds_contents`, with `mthds_sources` naming them on `validate`), a zipped bundle (`bundle_b64`), or a `files` map of texts, with the files a step reads sent beside it as `resources`. They take its tree now, as every other call does, and `validateFiles`, `MthdsFile`, `ValidateFilesOptions`, `BundleResources` and the SDK's own copy of the runner's path rule for `resources` are gone, with no alias kept. A field the client no longer names still passes through `extra` to a runner that reads it.
|
|
196
|
+
|
|
99
197
|
A method used to hold its source as two strings, `mthds` (a bundle, or the JSON text of a file array) and `python` (the JSON text of the custom PipeFunc files), and a version carried `source_digest`. Both fields are gone from every read and every write, with no alias kept: a method is a tree, a version's digest is `tree_digest`, and the publish state is derived from the two digests, the draft unchanged when `draft_digest` is not `null` and equals `latest_published.tree_digest`. `methodSourceToContents`, `getMethodClosure` and `EmptyMethodSourceError` went with them.
|
|
100
198
|
|
|
101
199
|
A run's `source_digest` is something else, and stays: the SHA-256 of the sources a run sent as text, 64 hex digits with no prefix. `RunDetail` also carries the run's `tree_digest` and `crate_fingerprint`, each `null` for a run that recorded none.
|
package/docs/run-results.md
CHANGED
|
@@ -9,7 +9,10 @@ Two paths produce it. Against the hosted API the SDK starts a durable run and po
|
|
|
9
9
|
```ts
|
|
10
10
|
import { resultsFromExecute, summarizeUsage } from "@pipelex/sdk";
|
|
11
11
|
|
|
12
|
-
const executeResult = await client.execute({
|
|
12
|
+
const executeResult = await client.execute({
|
|
13
|
+
pipe_code: "my_domain.my_pipe",
|
|
14
|
+
contents: { "my_domain.mthds": source },
|
|
15
|
+
});
|
|
13
16
|
const results = resultsFromExecute(executeResult);
|
|
14
17
|
console.log(results.main_stuff, summarizeUsage(results).total_cost_usd);
|
|
15
18
|
```
|
|
@@ -72,7 +75,7 @@ const results = await client.startAndWaitForResult(
|
|
|
72
75
|
|
|
73
76
|
It is never called on the blocking path, a bare runner's `POST /v1/execute` or the fallback to it, which has no run id to give before it answers. The acknowledgement is handed over whole, a `method_ref` run's `method_provenance` and a `method_id` run's `method_version` (the version the run runs, a number or `"draft"`) included. The callback runs synchronously and its return value is ignored; an exception it throws propagates out of `startAndWaitForResult` before anything is polled, and the run it was told about keeps going.
|
|
74
77
|
|
|
75
|
-
**An abort before the run exists creates none.** The `signal` is read before the run is started as well as during the wait: a caller that aborts while the `GET /v1/version` handshake is in flight gets its abort, and neither `POST /v1/start` nor the blocking `POST /v1/execute` is sent; nor is the execute when the abort lands while a runner that looked hosted refuses the start. Once the start or the execute is sent, the abort stops only the wait, and a started run goes on, by its id.
|
|
78
|
+
**An abort before the run exists creates none.** The `signal` is read before the run is started as well as during the wait: a caller that aborts while the `GET /v1/version` handshake is in flight gets its abort, and neither `POST /v1/start` nor the blocking `POST /v1/execute` is sent; nor is the execute when the abort lands while a runner that looked hosted refuses the start. The same holds while the method's tree is prepared: the signal is handed to the directory's listing, each file's read, the blob route and each upload of a missing blob, so an abort stops the one under way and no run is started ([method-trees.md](./method-trees.md#how-a-tree-travels)). The tree is prepared once, before the start, so a fallback to the blocking execute sends the files the start sent, reading and uploading none of them again. Once the start or the execute is sent, the abort stops only the wait, and a started run goes on, by its id.
|
|
76
79
|
|
|
77
80
|
**Knowing whether a run may exist.** `onStarting`, the other callback `StartAndWaitForResultOptions` takes, is called right before each request that may create a run is sent: `POST /v1/start`, and the blocking `POST /v1/execute` of a bare runner or of the fallback to it, so it is called twice when a runner that looked hosted refuses the start. Until it is called no run exists, and once the `signal` has aborted it is not called and no such request is sent, so a caller that stops waiting before then can say that no run was started; from then until `onStarted`, a run may exist that the API has not yet named. It runs synchronously and its return value is ignored; an exception it throws propagates before the request is sent. The `pipelex-sdk` command words its interrupt message by it ([`cli.md`](cli.md)).
|
|
78
81
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelex/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.1",
|
|
4
4
|
"description": "TypeScript SDK for the Pipelex hosted API — execute methods, manage runs, and call the product surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,12 @@
|
|
|
34
34
|
"default": "./dist/upload-grant.js"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
+
"imports": {
|
|
38
|
+
"#node-host": {
|
|
39
|
+
"node": "./dist/node-host/node.js",
|
|
40
|
+
"default": "./dist/node-host/unavailable.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
37
43
|
"scripts": {
|
|
38
44
|
"build": "tsc",
|
|
39
45
|
"build:hook": "node scripts/build-hook.mjs",
|
|
@@ -74,7 +80,7 @@
|
|
|
74
80
|
"vitest": "^4.0.18"
|
|
75
81
|
},
|
|
76
82
|
"dependencies": {
|
|
77
|
-
"mthds": "^0.
|
|
83
|
+
"mthds": "^0.33.0",
|
|
78
84
|
"smol-toml": "^1.6.0",
|
|
79
85
|
"undici": "^7.29.1"
|
|
80
86
|
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The files a bundle's steps read, sent with the bundle as the request's `resources`.
|
|
3
|
-
*
|
|
4
|
-
* A bundle sent as content has no directory, so a file one of its steps reads from beside the
|
|
5
|
-
* bundle, such as a `PipeDocGen` step's `template_file`, has to travel with it. The runner takes
|
|
6
|
-
* such files as `resources`: a JSON object mapping each file's path, exactly as the step names it,
|
|
7
|
-
* relative to the bundle and written with forward slashes, to the file's bytes in standard padded
|
|
8
|
-
* base64. It takes them beside `mthds_contents` (or a method bundle) on `/v1/validate`,
|
|
9
|
-
* `/v1/execute` and `/v1/start`, and beside inline `files[]` on `/v1/pipe-io`, `/v1/resolve` and
|
|
10
|
-
* `/v1/codegen`. A caller hands this SDK the bytes, keyed by that path, and the SDK encodes them.
|
|
11
|
-
*
|
|
12
|
-
* The SDK refuses before any request what the runner refuses for certain about the keys, with the
|
|
13
|
-
* runner's own rule (`resource_path_refusal` in pipelex's `bundle_resource.py`): an empty path, a
|
|
14
|
-
* backslash, a colon, a NUL character, an absolute path, a `..` segment, a path naming no file,
|
|
15
|
-
* and two keys naming one file once `.` segments and repeated slashes collapse. Each is a
|
|
16
|
-
* `RequestArgumentError` naming the key, where the runner answers a `422` `InvalidResourcePath`.
|
|
17
|
-
* The sizes are left to the server, since a deployment configures them: 32 files, 5 MiB each and
|
|
18
|
-
* 10 MiB in all by default, a `413` `PayloadTooLarge` beyond, and 512 KiB in all for the `.html`
|
|
19
|
-
* template files, a `413` `BundleResourcesTooLargeError`. So is every rule that needs the parsed
|
|
20
|
-
* bundle: a file no step names is a `422` `BundleResourceNotNamedError`.
|
|
21
|
-
*/
|
|
22
|
-
/** The bytes of one file a bundle's steps read: the byte forms `uploadFile` takes. */
|
|
23
|
-
export type BundleResourceBytes = Uint8Array | ArrayBuffer | Blob;
|
|
24
|
-
/**
|
|
25
|
-
* The files a bundle's steps read, such as a `PipeDocGen` step's template file, keyed by the path
|
|
26
|
-
* the step names (`templates/invoice.docx`), each as its bytes. `./templates/invoice.docx` names
|
|
27
|
-
* the same file, so only one of the two spellings may be given.
|
|
28
|
-
*/
|
|
29
|
-
export type BundleResources = Readonly<Record<string, BundleResourceBytes>>;
|
|
30
|
-
/**
|
|
31
|
-
* Whether `resources` is a plain object, the one shape a map of paths to bytes takes: its
|
|
32
|
-
* prototype is `Object.prototype` or `null`. A `Map`, an array or a class instance is not one,
|
|
33
|
-
* and the encoder refuses it rather than reading it as a map with no file in it.
|
|
34
|
-
*/
|
|
35
|
-
export declare function isResourceMap(resources: unknown): resources is Record<string, unknown>;
|
|
36
|
-
/**
|
|
37
|
-
* Whether `resources` carries at least one file. An absent, `null` or empty map carries none, so
|
|
38
|
-
* it is not sent, as an empty bundle encoding is not. A value that is not a plain object carries
|
|
39
|
-
* none either; the encoder refuses it.
|
|
40
|
-
*/
|
|
41
|
-
export declare function hasBundleResources(resources: unknown): boolean;
|
|
42
|
-
/**
|
|
43
|
-
* Why `path` cannot name a file inside a bundle, or `undefined` when it can — the runner's rule,
|
|
44
|
-
* held by the key of a file sent with a bundle and by the path a step names.
|
|
45
|
-
*/
|
|
46
|
-
export declare function bundleResourcePathRefusal(path: string): string | undefined;
|
|
47
|
-
/**
|
|
48
|
-
* The one spelling of a bundle-relative path the runner keys a file by: `.` segments and repeated
|
|
49
|
-
* slashes collapse, so `./templates//invoice.docx` is `templates/invoice.docx`. Only meaningful
|
|
50
|
-
* for a path {@link bundleResourcePathRefusal} accepts.
|
|
51
|
-
*/
|
|
52
|
-
export declare function canonicalBundleResourcePath(path: string): string;
|
|
53
|
-
/**
|
|
54
|
-
* Encode `resources` for the wire: each key as given, each value as its bytes in standard padded
|
|
55
|
-
* base64. Returns `undefined` when there is nothing to send, so the key stays off the wire.
|
|
56
|
-
*
|
|
57
|
-
* Every key is checked before any value is read, so a refused path costs no read of a `Blob`.
|
|
58
|
-
*
|
|
59
|
-
* @throws {RequestArgumentError} `resources` is not a map of paths to bytes, a key cannot name a
|
|
60
|
-
* file inside the bundle, two keys name one file, or a value is not bytes.
|
|
61
|
-
*/
|
|
62
|
-
export declare function encodeBundleResources(resources: BundleResources | null | undefined): Promise<Record<string, string> | undefined>;
|