@pipelex/sdk 0.48.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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/artifacts.d.ts +4 -3
  3. package/dist/artifacts.js +20 -14
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/cli/script.d.ts +1 -1
  6. package/dist/client.d.ts +6 -4
  7. package/dist/client.js +12 -5
  8. package/dist/client.js.map +1 -1
  9. package/dist/codegen-check.d.ts +2 -3
  10. package/dist/codegen-check.js +2 -3
  11. package/dist/codegen-check.js.map +1 -1
  12. package/dist/hooks/bundle-gather.d.ts +29 -10
  13. package/dist/hooks/bundle-gather.js +91 -14
  14. package/dist/hooks/bundle-gather.js.map +1 -1
  15. package/dist/hooks/check-core.d.ts +70 -6
  16. package/dist/hooks/check-core.js +188 -29
  17. package/dist/hooks/check-core.js.map +1 -1
  18. package/dist/hooks/claude-mthds-check.d.ts +12 -0
  19. package/dist/hooks/claude-mthds-check.js +12 -0
  20. package/dist/hooks/claude-mthds-check.js.map +1 -1
  21. package/dist/hooks/secret-guard.d.ts +38 -0
  22. package/dist/hooks/secret-guard.js +134 -0
  23. package/dist/hooks/secret-guard.js.map +1 -0
  24. package/dist/hooks/validate-stage.d.ts +5 -3
  25. package/dist/hooks/validate-stage.js +16 -7
  26. package/dist/hooks/validate-stage.js.map +1 -1
  27. package/dist/method-trees.d.ts +19 -8
  28. package/dist/method-trees.js +37 -11
  29. package/dist/method-trees.js.map +1 -1
  30. package/dist/node-host/node.d.ts +33 -0
  31. package/dist/node-host/node.js +27 -0
  32. package/dist/node-host/node.js.map +1 -0
  33. package/dist/node-host/unavailable.d.ts +8 -0
  34. package/dist/node-host/unavailable.js +8 -0
  35. package/dist/node-host/unavailable.js.map +1 -0
  36. package/dist/node-host.d.ts +53 -0
  37. package/dist/node-host.js +72 -0
  38. package/dist/node-host.js.map +1 -0
  39. package/dist/upload.d.ts +4 -9
  40. package/dist/upload.js +13 -16
  41. package/dist/upload.js.map +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/docs/architecture.md +29 -5
  45. package/docs/crate-routes.md +1 -1
  46. package/docs/hook-bundle.md +27 -1
  47. package/docs/input-preparation.md +2 -2
  48. package/docs/method-trees.md +2 -2
  49. package/package.json +7 -1
@@ -199,7 +199,7 @@ if (!report.isCurrent) {
199
199
 
200
200
  A `codegen()` response feeds straight in with no mapping — `GeneratedArtifact` and `CodegenTreeFile` are structurally identical on purpose, so `runCodegenCheck({ lockContent: result.lock, files: result.artifacts })` type-checks and reports `isCurrent`. That is worth doing right after a regeneration, before writing anything to disk.
201
201
 
202
- The function is `async` because it hashes through **WebCrypto** (`crypto.subtle`) rather than `node:crypto`, so the check adds no Node builtin to the barrel's import graph. (That is not the same as the barrel being browser-bundleable today: `upload.ts` still names `node:fs/promises`, so a browser-targeting bundler must mark `node:*` external.) One caveat that never bites a CI script but should not surprise anyone: `crypto.subtle` is secure-context-only, so a browser page must be served over https (or localhost).
202
+ The function is `async` because it hashes through **WebCrypto** (`crypto.subtle`) rather than `node:crypto`, so the check adds no Node builtin to the barrel's import graph, which bundles for the browser with nothing marked external ([architecture.md](./architecture.md#module-format--packaging)). One caveat that never bites a CI script but should not surprise anyone: `crypto.subtle` is secure-context-only, so a browser page must be served over https (or localhost).
203
203
 
204
204
  ### The algorithm
205
205
 
@@ -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 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` keeps out and a run's inputs file never travel ([`method-trees.md`](./method-trees.md#the-four-forms-of-a-tree)). 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 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.
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
 
@@ -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, but at the *packaging* level the main `@pipelex/sdk` entry is **Node-first**: it statically references `node:fs/promises` (to read path strings), so bundling it for a browser or edge target requires marking `node:*` external. This is a deliberate stance — the client holds an API key, so it runs server-side (Next.js Server Actions, the MCP server, the plugins hook). A browser page that must store a file uses the upload grant below instead, whose sender ships from the browser-safe `@pipelex/sdk/upload` entry.
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 the entry for the browser with esbuild and fails if anything Node-only becomes reachable.
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
 
@@ -31,7 +31,7 @@ const verdict = await client.validate({
31
31
  });
32
32
  ```
33
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: a browser gives `contents`, `tree` or `files`.
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
35
 
36
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
37
 
@@ -166,7 +166,7 @@ A method whose draft names a tree the platform holds no record of answers `getMe
166
166
 
167
167
  ## Writing a tree to disk
168
168
 
169
- `writeMethodTree(files, { destination })` writes a listing to disk, each file at its tree path under `destination`. It runs in Node only, and loads Node's file system modules when it is called:
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:
170
170
 
171
171
  ```ts
172
172
  const detail = await client.getMethod("mt_123");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipelex/sdk",
3
- "version": "0.48.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",