stitchkit 0.113.1 → 0.113.3
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/CHANGELOG.md +14 -0
- package/dist/contract/errors.d.ts +2 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/entrypoints/agent-runtime/browser.js +2 -2
- package/dist/entrypoints/agent-runtime/coding-tools.js +4 -4
- package/dist/entrypoints/agent-runtime/harness.js +5 -5
- package/dist/entrypoints/agent-runtime/realtime.js +2 -2
- package/dist/entrypoints/agent-runtime/sandbox.js +4 -4
- package/dist/entrypoints/agent-runtime/sqlite/bun.js +6 -6
- package/dist/entrypoints/agent-runtime/sqlite/node.js +6 -6
- package/dist/entrypoints/agent-runtime/testing.js +6 -6
- package/dist/entrypoints/agent-runtime.js +11 -11
- package/dist/entrypoints/application/grammy.js +1 -1
- package/dist/entrypoints/application.js +2 -2
- package/dist/entrypoints/cli.js +8 -8
- package/dist/entrypoints/contract.js +2 -2
- package/dist/entrypoints/files.js +1 -1
- package/dist/entrypoints/index.js +5 -5
- package/dist/entrypoints/node.js +7 -7
- package/dist/entrypoints/observability.js +1 -1
- package/dist/entrypoints/realtime.js +3 -3
- package/dist/entrypoints/remote.js +1 -1
- package/dist/entrypoints/server.js +8 -8
- package/dist/entrypoints/testing.js +3 -3
- package/dist/entrypoints/tools/connections.js +5 -5
- package/dist/entrypoints/tools/invoker.js +5 -5
- package/dist/entrypoints/tools/mcp.js +7 -7
- package/dist/entrypoints/tools.js +13 -13
- package/dist/{index-8jjzfts8.js → index-095ya5hm.js} +5 -5
- package/dist/{index-sjww6myv.js → index-1csp1hwm.js} +4 -4
- package/dist/{index-xmgjtkvr.js → index-2bqf0a74.js} +1 -1
- package/dist/{index-ykdcnq1f.js → index-3za04re9.js} +1 -1
- package/dist/{index-yjnmac9b.js → index-523tq7y4.js} +1 -1
- package/dist/{index-mhrtwttb.js → index-7fc0c2np.js} +1 -1
- package/dist/{index-emrmgq3s.js → index-86nbfysb.js} +3 -3
- package/dist/{index-556g5hsh.js → index-98hv3sba.js} +1 -1
- package/dist/{index-h1spd9kv.js → index-9dyd8xzf.js} +1 -1
- package/dist/{index-gz9jf461.js → index-aykhzcnf.js} +1 -1
- package/dist/{index-86sxnvpr.js → index-bjcjqj46.js} +1 -1
- package/dist/{index-pzw0fpek.js → index-cqmqyjax.js} +1 -1
- package/dist/{index-nk1mh535.js → index-ega3yz4c.js} +1 -1
- package/dist/{index-k8fbzggr.js → index-fy4av0hq.js} +1 -1
- package/dist/{index-v6mzw27b.js → index-htxh46ej.js} +3 -3
- package/dist/{index-pvh3z03x.js → index-jk29rwkr.js} +1 -1
- package/dist/{index-fynr93w4.js → index-k42ckyzx.js} +1 -1
- package/dist/{index-fytsnhzj.js → index-k7z3j3hr.js} +211 -125
- package/dist/{index-j9gxbztg.js → index-qcn84byh.js} +2 -0
- package/dist/{index-jgwqc38k.js → index-rhzkndr5.js} +2 -2
- package/dist/{index-k0zfcxf5.js → index-tpwbct8j.js} +1 -1
- package/dist/{index-8asb62xn.js → index-v1tswj0z.js} +1 -1
- package/dist/{index-tg2q89ps.js → index-v6vb5qx8.js} +4 -4
- package/dist/{index-2qymcsq2.js → index-vtann8bz.js} +3 -3
- package/dist/{index-nn6607y3.js → index-vwhrnp67.js} +4 -4
- package/dist/{index-dsxrcseb.js → index-zktz242w.js} +2 -2
- package/dist/tools/cli/command-shape.d.ts +4 -0
- package/dist/tools/cli/command-shape.d.ts.map +1 -0
- package/dist/tools/cli/command.d.ts +2 -1
- package/dist/tools/cli/command.d.ts.map +1 -1
- package/dist/tools/cli/format.d.ts +5 -1
- package/dist/tools/cli/format.d.ts.map +1 -1
- package/dist/tools/cli/help.d.ts.map +1 -1
- package/dist/tools/cli/invoke.d.ts +1 -1
- package/dist/tools/cli/invoke.d.ts.map +1 -1
- package/dist/tools/cli/managed-surface.d.ts +3 -2
- package/dist/tools/cli/managed-surface.d.ts.map +1 -1
- package/dist/tools/cli/policy.d.ts +2 -0
- package/dist/tools/cli/policy.d.ts.map +1 -1
- package/dist/tools/cli/run-managed.d.ts.map +1 -1
- package/dist/tools/cli/run-native.d.ts.map +1 -1
- package/dist/tools/cli/session.d.ts +0 -1
- package/dist/tools/cli/session.d.ts.map +1 -1
- package/dist/tools/cli/unavailable.d.ts +10 -0
- package/dist/tools/cli/unavailable.d.ts.map +1 -0
- package/llms/cli.2.txt +282 -1
- package/llms/cli.txt +49 -287
- package/llms/server.4.txt +1 -1
- package/llms-full.txt +51 -8
- package/llms.txt +1 -1
- package/package.json +1 -1
package/llms/cli.txt
CHANGED
|
@@ -213,9 +213,11 @@ const doctor = defineCliCommand({
|
|
|
213
213
|
it the canonical JSON output is unchanged. `exitCode` classifies a successfully
|
|
214
214
|
validated result and must return an integer in `0..255`; failed `ToolResult`
|
|
215
215
|
envelopes and the application-wide `exitCodes` mapping remain authoritative for
|
|
216
|
-
failures. A throwing
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
failures. A throwing or invalid exit-status callback becomes a normalized
|
|
217
|
+
`INTERNAL_SERVER_ERROR`. A throwing or non-string `present` preserves the validated
|
|
218
|
+
result on stdout and reports `CLI_PRESENTATION_FAILED` separately, as described
|
|
219
|
+
under [Aggregate views](#aggregate-views). Void native commands cannot declare
|
|
220
|
+
either callback.
|
|
219
221
|
|
|
220
222
|
`--version`, a selected native command and its command help run before
|
|
221
223
|
`resolveAuth`, services, context or runtime-tool factories. Top-level help is
|
|
@@ -430,6 +432,7 @@ newline-terminated JSON record on its respective stream; progress and CLI usage
|
|
|
430
432
|
diagnostics remain ordinary stderr text. This keeps stdout pipeable and
|
|
431
433
|
`2>/dev/null` clean. The process exit code carries the error class (`0` ok,
|
|
432
434
|
`VALIDATION_ERROR → 1`, `UNAUTHORIZED → 2`, `FORBIDDEN → 3`, `NOT_FOUND → 4`,
|
|
435
|
+
`CLI_PRESENTATION_FAILED → 8`,
|
|
433
436
|
…) — override per app with `exitCodes`.
|
|
434
437
|
|
|
435
438
|
### A narrower question than "all of them"
|
|
@@ -490,13 +493,42 @@ command name — validated against the declared schema, and kept out of every
|
|
|
490
493
|
operation's arguments: `handoff_read` above receives `{ handoffId: 'u' }` and
|
|
491
494
|
nothing else. The values reach `resolveAuth(globals)`, `context(auth, globals)`
|
|
492
495
|
and a native command's `globals`. An invalid value is an argument error naming
|
|
493
|
-
the flag
|
|
494
|
-
|
|
496
|
+
the flag. An application global that collides with a framework option is refused
|
|
497
|
+
at startup. Field collisions in authored commands are configuration errors;
|
|
498
|
+
runtime catalogue entries are isolated as described below. Past a bare
|
|
495
499
|
`--` every token is a literal, so a positional value that reads like a global
|
|
496
500
|
survives intact.
|
|
497
501
|
|
|
498
502
|
Both help levels list them under `Application options:`.
|
|
499
503
|
|
|
504
|
+
## When one runtime command cannot be represented in argv
|
|
505
|
+
|
|
506
|
+
A `runtimeTools` catalogue can change independently of the CLI application. An
|
|
507
|
+
entry whose input declares a framework option such as `json`, or a field shadowed
|
|
508
|
+
by an application global such as `caller`, cannot receive that field through argv.
|
|
509
|
+
Only that entry becomes unavailable. Other commands still run, and top-level,
|
|
510
|
+
filtered and command-specific help retain the entry and its full reason:
|
|
511
|
+
|
|
512
|
+
```text
|
|
513
|
+
inspect Inspect an operation [unavailable]
|
|
514
|
+
Unavailable: [stitchkit] CLI command "inspect" declares field(s) shadowed by application global options: caller
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Calling it returns `CLI_COMMAND_UNAVAILABLE`, `retryable: false`, and the same
|
|
518
|
+
reason in `details.message` on stderr; the default exit code is 1. `--json`
|
|
519
|
+
compacts the error as usual; `exitCodes` and `errorHint` apply. The refusal precedes
|
|
520
|
+
command argument parsing, stdin, context, hooks and the handler; `--dry-run` cannot
|
|
521
|
+
turn it into success. A known unavailable `--wait` target returns the same refusal;
|
|
522
|
+
an unknown target keeps `NOT_FOUND`.
|
|
523
|
+
|
|
524
|
+
This isolates only reserved/global **field** conflicts in `runtimeTools`.
|
|
525
|
+
Native commands and contract-defined `services` still fail at startup for those
|
|
526
|
+
conflicts. Duplicate or reserved command names and invalid local aliases,
|
|
527
|
+
positionals or passthrough policies remain configuration errors for every entry.
|
|
528
|
+
`createCliInvoker` receives globals and command input separately and has no argv
|
|
529
|
+
projection, so its own `caller` or `json` input stays callable without renaming.
|
|
530
|
+
See [ADR 0273](../decisions/0273-a-runtime-cli-field-conflict-is-one-commands-unavailability.md).
|
|
531
|
+
|
|
500
532
|
## When the managed surface cannot resolve
|
|
501
533
|
|
|
502
534
|
A CLI whose commands come from a running server declares them with a factory,
|
|
@@ -622,7 +654,7 @@ would answer a question nobody asked.
|
|
|
622
654
|
|
|
623
655
|
Three rules worth knowing before you rely on them:
|
|
624
656
|
|
|
625
|
-
- **A field the result does not carry is
|
|
657
|
+
- **A field the result does not carry is a presentation error.** A group of zero
|
|
626
658
|
over a misspelled field is indistinguishable from a true empty answer, and the
|
|
627
659
|
caller reads it as data. The message names the fields that *are* there.
|
|
628
660
|
- **An aggregate needs a collection** — the result itself when it is an array,
|
|
@@ -636,6 +668,17 @@ Three rules worth knowing before you rely on them:
|
|
|
636
668
|
A failed call still reports its own error and exit code. An aggregate over an
|
|
637
669
|
error is not an answer to the question that was asked.
|
|
638
670
|
|
|
671
|
+
If the command succeeds but its requested view cannot be built, the CLI writes
|
|
672
|
+
the complete original result to stdout, using the normal JSON format (`--json`
|
|
673
|
+
still selects compact output). It writes a separate structured
|
|
674
|
+
`CLI_PRESENTATION_FAILED` diagnostic to stderr and exits with `8`, configurable
|
|
675
|
+
through `exitCodes`. Its `details.operationSucceeded: true` and `retryable: false`
|
|
676
|
+
distinguish a presentation refusal from a failed operation; `details.cause`
|
|
677
|
+
preserves the safe reason. Recover the answer from stdout instead of repeating
|
|
678
|
+
the mutation. Domain retry hints are not appended to this diagnostic. CLI
|
|
679
|
+
argument errors detected before execution still exit with `2`
|
|
680
|
+
and have no successful result to preserve.
|
|
681
|
+
|
|
639
682
|
## Named profiles
|
|
640
683
|
|
|
641
684
|
A CLI that talks to a deployed server needs an address and a credential per
|
|
@@ -681,284 +724,3 @@ await createCli({
|
|
|
681
724
|
Files are written `0600` in a `0700` directory — and a profile file other users
|
|
682
725
|
can read is refused with the `chmod` that fixes it, because it holds a
|
|
683
726
|
credential.
|
|
684
|
-
|
|
685
|
-
## Distribution and self-update
|
|
686
|
-
|
|
687
|
-
`createCli` ships no executable, and that is right — but the step after the
|
|
688
|
-
executable is not application logic either. It is the same problem for every
|
|
689
|
-
consumer, with the same three traps:
|
|
690
|
-
|
|
691
|
-
1. **The installer cannot parse the manifest.** A `curl … | sh` runs on a
|
|
692
|
-
machine where nothing is installed yet, including `jq`. So the installer is
|
|
693
|
-
generated *from* the manifest, server-side, with the URL and digest already
|
|
694
|
-
substituted — it parses no JSON at all.
|
|
695
|
-
2. **Replacing a running binary is a rename, not a write.** Anything else can
|
|
696
|
-
leave a half-written executable on someone's PATH when the connection drops.
|
|
697
|
-
3. **The digest covers the decompressed bytes** — the file that will actually be
|
|
698
|
-
executed, not the archive that was transferred.
|
|
699
|
-
|
|
700
|
-
The framework owns the manifest shape, the installer generation and the update
|
|
701
|
-
primitive. The application owns where the assets live, which platforms it
|
|
702
|
-
publishes and who may download them.
|
|
703
|
-
|
|
704
|
-
```ts
|
|
705
|
-
import {
|
|
706
|
-
CliBuildManifestSchema, assertCliPublishable, renderCliInstaller,
|
|
707
|
-
selectCliBuildAsset, checkCliUpdate, applyCliUpdate,
|
|
708
|
-
} from 'stitchkit/cli'
|
|
709
|
-
|
|
710
|
-
// Publishing: refuse to republish one version from a different commit —
|
|
711
|
-
// otherwise everyone who already installed it never receives the fix.
|
|
712
|
-
assertCliPublishable(previous, next)
|
|
713
|
-
|
|
714
|
-
// Serving: omit `asset` and one script covers every published target, selecting
|
|
715
|
-
// by uname at run time — otherwise that dispatch is the last hand-written piece
|
|
716
|
-
// of the install path, and every publisher writes the same x86_64 → x64 table.
|
|
717
|
-
renderCliInstaller({ manifest, binaryName: 'myapp' })
|
|
718
|
-
// Or pin one target explicitly:
|
|
719
|
-
renderCliInstaller({ manifest, asset: selectCliBuildAsset(manifest, target), binaryName: 'myapp' })
|
|
720
|
-
|
|
721
|
-
// Checking: bounded, at most once per interval, silent on any failure.
|
|
722
|
-
const check = await checkCliUpdate({ manifestUrl, currentVersion, lastCheckedAt })
|
|
723
|
-
if (check.status === 'outdated' && check.asset) {
|
|
724
|
-
// Replacing is always an explicit command, never a side effect of a check.
|
|
725
|
-
await applyCliUpdate({ asset: check.asset })
|
|
726
|
-
}
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
`checkCliUpdate` has **four** answers, not three: `skipped`, `current`,
|
|
730
|
-
`outdated` and `unknown`. "Could not ask" is not "up to date" — collapsing them
|
|
731
|
-
is how a tool goes quiet about its own staleness for months. It never throws,
|
|
732
|
-
and a command still exits with the code it earned.
|
|
733
|
-
|
|
734
|
-
Carry the build stamp inside the binary (`CliBuildStampSchema`,
|
|
735
|
-
`formatCliBuildStamp`) so the tool can say what it is rather than leaving the
|
|
736
|
-
reader to infer it from behaviour.
|
|
737
|
-
|
|
738
|
-
#### A manifest inside your own network
|
|
739
|
-
|
|
740
|
-
The check goes through the same SSRF boundary every outbound fetch does, so a
|
|
741
|
-
manifest served from `10.x`, `localhost` or a `.internal` name is refused and
|
|
742
|
-
the answer is `unknown`. For a self-hosted deployment that is a wrong answer
|
|
743
|
-
dressed as a careful one, and the refusal says so — it names the field that
|
|
744
|
-
lifts it rather than reading like a timeout:
|
|
745
|
-
|
|
746
|
-
```text
|
|
747
|
-
refusing to fetch an internal host — the manifest endpoint is not public.
|
|
748
|
-
Set `allowPrivateHosts` if it is your own deployment; the download keeps its
|
|
749
|
-
own setting, because an asset URL comes from the document.
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
```ts
|
|
753
|
-
const check = await checkCliUpdate({ manifestUrl, currentVersion, allowPrivateHosts: true })
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
Set it from whether **your** endpoint is private, not as a blanket switch, and
|
|
757
|
-
leave `applyCliUpdate` at its default. The two are not the same decision: the
|
|
758
|
-
manifest URL is configuration you wrote, while an asset URL arrives inside a
|
|
759
|
-
document the endpoint served, and a document is exactly what an SSRF boundary
|
|
760
|
-
exists to distrust.
|
|
761
|
-
|
|
762
|
-
### Publishing a complete version
|
|
763
|
-
|
|
764
|
-
`publishCli` is an opt-in publisher for a local application-owned distribution
|
|
765
|
-
directory. It commits every target and its version manifest before atomically
|
|
766
|
-
replacing the public `manifest.json`. It ships from its own evolving entrypoint,
|
|
767
|
-
`stitchkit/cli/publish`: release infrastructure follows how distributions are
|
|
768
|
-
published, so it is not held to the stable promise of `stitchkit/cli`
|
|
769
|
-
([ADR 0245](../decisions/0245-cli-publication-is-an-evolving-leaf.md)). The
|
|
770
|
-
manifest, signatures, installer and updater it writes for stay in
|
|
771
|
-
`stitchkit/cli`. Your application selects the CLI source identity and admits
|
|
772
|
-
that snapshot at each phase:
|
|
773
|
-
|
|
774
|
-
```ts
|
|
775
|
-
import { publishCli } from 'stitchkit/cli/publish'
|
|
776
|
-
|
|
777
|
-
const result = await publishCli({
|
|
778
|
-
storageRoot: '/srv/myapp/cli',
|
|
779
|
-
name: 'myapp', version: cliVersion, commit: selectedCliCommit,
|
|
780
|
-
baseUrl: 'https://downloads.example.com/cli/',
|
|
781
|
-
targets: [{ platform: 'linux', arch: 'x64' }],
|
|
782
|
-
signal,
|
|
783
|
-
admit: ({ phase, identity, signal }) => assertSelectedSource({ phase, identity, signal }),
|
|
784
|
-
build: ({ target, stamp, signal }) => compileSelectedSource({ target, stamp, signal }),
|
|
785
|
-
})
|
|
786
|
-
// result.outcome: 'published' | 'existing'; result.manifest is ready to serve.
|
|
787
|
-
```
|
|
788
|
-
|
|
789
|
-
The builder returns `Uint8Array` or `ReadableStream<Uint8Array>` containing the
|
|
790
|
-
executable. Every target receives the same immutable `{ version, commit,
|
|
791
|
-
builtAt }` stamp; carry it inside the binary. The publisher owns staging,
|
|
792
|
-
gzip assets, paths and manifests. Callbacks receive the cancellation signal and
|
|
793
|
-
run under the publication lock. Admission phases are `prepare`, each target's
|
|
794
|
-
`build`, `commit`, and `promote`; validate the selected source through commit
|
|
795
|
-
and promotion. A backend release can explicitly select an unchanged older CLI
|
|
796
|
-
identity. The helper never substitutes a different commit or rebuilds an
|
|
797
|
-
existing version under a new identity.
|
|
798
|
-
|
|
799
|
-
Assets live at `<version>/<name>-<platform>-<arch>.gz`, with an immutable
|
|
800
|
-
`<version>/manifest.json` and a public `manifest.json`. A verified repeat keeps
|
|
801
|
-
the exact bytes and `builtAt`, requires the same target set and base URL, and
|
|
802
|
-
returns `existing` without invoking the builder. A version committed before a
|
|
803
|
-
failed promotion is recovered and promoted without rebuilding. Conflicting
|
|
804
|
-
same-version commits, downgrades, unsafe components, corrupt manifests and
|
|
805
|
-
assets are refused. Version/name/target components are validated before any
|
|
806
|
-
build; `storageRoot` is absolute and its existing parent must be trusted.
|
|
807
|
-
|
|
808
|
-
All limits are finite positive safe integers; `limits` overrides these defaults:
|
|
809
|
-
|
|
810
|
-
| Field | Default | Enforced boundary |
|
|
811
|
-
| --- | --- | --- |
|
|
812
|
-
| `maxTargets` | 16 | Requested and verified target count |
|
|
813
|
-
| `maxManifestBytes` | 256 KiB | Read and written manifest bytes |
|
|
814
|
-
| `maxAssetBytes` | 256 MiB | Builder and decompressed executable bytes |
|
|
815
|
-
| `maxCompressedBytes` | 256 MiB | Archived asset bytes |
|
|
816
|
-
| `maxDirectoryEntries` | 128 | Version layout, root scan and commit space |
|
|
817
|
-
| `maxStoredVersions` | 16 | Verified publisher-owned history |
|
|
818
|
-
| `lockTimeoutMs` | 10,000 | Wait to acquire the lock |
|
|
819
|
-
| `timeoutMs` | 600,000 | Whole publication deadline |
|
|
820
|
-
|
|
821
|
-
`retention` defaults to 2 and keeps the current and previous published versions.
|
|
822
|
-
It must be at least 2 and less than `maxStoredVersions` (at least 3), leaving a
|
|
823
|
-
recovery slot. Older versions are pruned only after their own identity, exact
|
|
824
|
-
layout, raw size and digest are verified. Runtime folders and foreign semver
|
|
825
|
-
directories remain untouched. A full or unverifiable store refuses further
|
|
826
|
-
publication; repeated completed candidates stay inside the owned history cap.
|
|
827
|
-
Cancelled or failed incomplete staging is removed only while its root and
|
|
828
|
-
directory generations still match. The shared asynchronous gzip decoder also
|
|
829
|
-
bounds updater expansion by the declared raw size and configured byte cap.
|
|
830
|
-
|
|
831
|
-
The publisher rejects symlink roots/versions/assets, non-regular files and
|
|
832
|
-
multiply linked files, and checks held-lock, root and prior-pointer generations
|
|
833
|
-
before commit and promotion. This is cooperative publication under a trusted
|
|
834
|
-
parent directory. Portable path checks do not provide an atomic comparison with
|
|
835
|
-
a hostile process replacing ancestors between syscalls. Durability uses file
|
|
836
|
-
and directory sync on POSIX; Windows publication is refused. A deadline stops
|
|
837
|
-
callback/stream waits and blocks late promotion; trusted callbacks remain
|
|
838
|
-
responsible for cancelling their own subprocesses and external effects.
|
|
839
|
-
|
|
840
|
-
A failure after an immutable commit reports that the publication outcome must
|
|
841
|
-
be checked. A failure after public-pointer rename may already have published;
|
|
842
|
-
`AtomicFilePublicationError.published` remains available when the atomic writer
|
|
843
|
-
reports that boundary. Read the durable manifest and retry the admitted same
|
|
844
|
-
identity to recover; an error does not promise rollback. Optional `signing` and
|
|
845
|
-
`trust` reuse the Ed25519 contract below. The generated shell installer checks
|
|
846
|
-
raw digest and size; the updater checks manifest authorship when given a trust
|
|
847
|
-
root.
|
|
848
|
-
|
|
849
|
-
### Proving who built it — signing the manifest
|
|
850
|
-
|
|
851
|
-
The asset digest proves the bytes that arrived are the bytes the manifest named.
|
|
852
|
-
It proves nothing about who named them: the manifest and the assets come from
|
|
853
|
-
one origin, so whoever can replace one can replace the other. Authorship needs a
|
|
854
|
-
key the build carries and the server never holds.
|
|
855
|
-
|
|
856
|
-
```ts
|
|
857
|
-
// Publishing — the only side that holds a private key.
|
|
858
|
-
const signature = signCliManifest(manifest, { keyId: 'release-2026', privateKey })
|
|
859
|
-
publish({ ...manifest, signature })
|
|
860
|
-
|
|
861
|
-
// Installing — the trust root is compiled into the binary. One fetched at run
|
|
862
|
-
// time from the same origin as the manifest would prove nothing.
|
|
863
|
-
const trust = { keys: { 'release-2026': PUBLISHED_KEY } }
|
|
864
|
-
const check = await checkCliUpdate({ manifestUrl, currentVersion, trust })
|
|
865
|
-
if (check.status === 'outdated' && check.asset) {
|
|
866
|
-
await applyCliUpdate({ asset: check.asset, manifest: check.manifest, trust, backupPath })
|
|
867
|
-
}
|
|
868
|
-
```
|
|
869
|
-
|
|
870
|
-
The signature covers `{name, version, commit, builtAt, assets[]}` with **every
|
|
871
|
-
asset's digest**, so the chain closes on the file that will execute rather than
|
|
872
|
-
on the document describing it; moving an asset to a new URL does not invalidate
|
|
873
|
-
it, changing what an asset contains does.
|
|
874
|
-
|
|
875
|
-
Five verdicts, and `unenforced` is the one that earns its keep: a build with no
|
|
876
|
-
pinned key keeps updating, and the fact that nothing was checked is *visible*
|
|
877
|
-
rather than assumed. `missing`, `unknown-key` and `invalid` are distinguished
|
|
878
|
-
because they need different answers — a key you forgot to pin is not a forged
|
|
879
|
-
signature.
|
|
880
|
-
|
|
881
|
-
A bad verdict does not produce a fifth status. `outdated` is an instruction to
|
|
882
|
-
install, and a manifest that failed its signature has not established that there
|
|
883
|
-
is a newer build worth installing — only that a document claims one. The check
|
|
884
|
-
answers `unknown` with the verdict in its reason, so it is never mistaken for a
|
|
885
|
-
network failure. `applyCliUpdate` checks again and refuses **before** the
|
|
886
|
-
download: a signature verified after the bytes are on the machine produces a
|
|
887
|
-
cleanup problem rather than a refusal.
|
|
888
|
-
|
|
889
|
-
### A way back — `backupPath` and `rollbackCliUpdate`
|
|
890
|
-
|
|
891
|
-
Rolling back is a property of updating, not of the application:
|
|
892
|
-
|
|
893
|
-
```ts
|
|
894
|
-
const applied = await applyCliUpdate({ asset, targetPath, backupPath })
|
|
895
|
-
// …the new build turns out to be wrong. `backupSha256` is absent when there was
|
|
896
|
-
// nothing to keep — a first install — and that is the case with nothing to roll
|
|
897
|
-
// back to:
|
|
898
|
-
if (applied.backupSha256) {
|
|
899
|
-
rollbackCliUpdate({ targetPath, backupPath, expectedSha256: applied.backupSha256 })
|
|
900
|
-
}
|
|
901
|
-
```
|
|
902
|
-
|
|
903
|
-
The copy is taken between "the new bytes verified" and the replacement — earlier
|
|
904
|
-
would preserve a binary about to be replaced by a download that then failed its
|
|
905
|
-
digest, later has nothing left to copy — and it carries the target's file mode,
|
|
906
|
-
because a backup that cannot be executed is not a way back. A first install has
|
|
907
|
-
nothing to keep, which is not a failure.
|
|
908
|
-
|
|
909
|
-
`expectedSha256` is required, not optional. A rollback that installs whatever
|
|
910
|
-
happens to be at the backup path is a second install of an unverified binary,
|
|
911
|
-
and the day it is used is the day nobody is in a position to check.
|
|
912
|
-
|
|
913
|
-
### Run it before it replaces anything — `verify`
|
|
914
|
-
|
|
915
|
-
A digest proves the bytes are the ones published, not that they load. A tool
|
|
916
|
-
that updates itself unattended — a daemon — and installs a build that does not
|
|
917
|
-
start takes its own updater down with it, and every machine is then repaired by
|
|
918
|
-
hand. `verify` is the step between "digest matched" and "target replaced":
|
|
919
|
-
|
|
920
|
-
```ts
|
|
921
|
-
await applyCliUpdate({
|
|
922
|
-
asset, targetPath, backupPath,
|
|
923
|
-
verify: async (candidate, signal) => {
|
|
924
|
-
const child = Bun.spawn(['bun', candidate, 'version'], { signal, stdout: 'pipe' })
|
|
925
|
-
const printed = (await new Response(child.stdout).text()).trim()
|
|
926
|
-
if ((await child.exited) !== 0 || printed !== check.version) {
|
|
927
|
-
throw new Error(`printed ${printed}, expected ${check.version}`)
|
|
928
|
-
}
|
|
929
|
-
},
|
|
930
|
-
verifyTimeoutMs: 15_000, // default 30 000
|
|
931
|
-
})
|
|
932
|
-
```
|
|
933
|
-
|
|
934
|
-
The verified bytes are written beside the target as an executable candidate
|
|
935
|
-
with the target's extension, and `verify` gets its path — a path rather than a
|
|
936
|
-
command, because a JavaScript bundle is started through its runtime. It runs
|
|
937
|
-
before the backup and the replacement. If it throws or outlives
|
|
938
|
-
`verifyTimeoutMs` (its `signal` aborts at the deadline), the candidate is
|
|
939
|
-
removed, the target and any earlier backup are untouched, and the update is
|
|
940
|
-
refused with the reason as its cause. The target is written from the checked
|
|
941
|
-
bytes in memory, never from the candidate file, so nothing the candidate did
|
|
942
|
-
while it ran can change what is installed.
|
|
943
|
-
|
|
944
|
-
### Two release tracks
|
|
945
|
-
|
|
946
|
-
`assertCliPublishable` takes one manifest **or every manifest already
|
|
947
|
-
published**:
|
|
948
|
-
|
|
949
|
-
```ts
|
|
950
|
-
assertCliPublishable([stableManifest, betaManifest], next)
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
With a single argument and two tracks the check is silently useless: `1.2.3`
|
|
954
|
-
goes to beta from one commit and to stable from another, the function is called
|
|
955
|
-
with the track being published and sees no conflict, and the person who
|
|
956
|
-
installed the beta is told they are current forever. Nothing on their machine
|
|
957
|
-
looks wrong.
|
|
958
|
-
|
|
959
|
-
**The channel itself is not an argument of the framework, and will not become
|
|
960
|
-
one.** The framework has no model of how your URLs are built and should not: an
|
|
961
|
-
asset's address comes from the manifest, and the manifest's address belongs to
|
|
962
|
-
your server. A channel is two documents at two addresses — which of them a build
|
|
963
|
-
consults is your decision, and `assertCliPublishable` is told about all of them
|
|
964
|
-
rather than taught the shape of any.
|
package/llms/server.4.txt
CHANGED
|
@@ -553,7 +553,7 @@ core never models them (ADR 0002). But stitchkit itself emits a set of its own:
|
|
|
553
553
|
`FILE_INSPECTION_REJECTED`, `FILE_TOO_LARGE`, `FILE_EXISTS`,
|
|
554
554
|
`REALTIME_CONTRACT_VIOLATION`, `APPLICATION_NOT_ACCEPTING`, `NOT_IMPLEMENTED`,
|
|
555
555
|
`WAIT_TIMEOUT`,
|
|
556
|
-
`WAIT_FAILED`, `DOWNLOAD_NOT_FOUND`, `VIEW_HTTP_ERROR`,
|
|
556
|
+
`WAIT_FAILED`, `CLI_COMMAND_UNAVAILABLE`, `CLI_PRESENTATION_FAILED`, `DOWNLOAD_NOT_FOUND`, `VIEW_HTTP_ERROR`,
|
|
557
557
|
`CONNECTION_TIMEOUT`, `CONNECTION_RESPONSE_TOO_LARGE`,
|
|
558
558
|
`CONNECTION_REQUEST_FAILED`, `UPSTREAM_TOOL_ERROR`,
|
|
559
559
|
`OPERATION_NOT_SUCCEEDED`, `STREAM_ITEM_INVALID`, `STREAM_FRAME_TOO_LARGE`,
|
package/llms-full.txt
CHANGED
|
@@ -10012,9 +10012,11 @@ const doctor = defineCliCommand({
|
|
|
10012
10012
|
it the canonical JSON output is unchanged. `exitCode` classifies a successfully
|
|
10013
10013
|
validated result and must return an integer in `0..255`; failed `ToolResult`
|
|
10014
10014
|
envelopes and the application-wide `exitCodes` mapping remain authoritative for
|
|
10015
|
-
failures. A throwing
|
|
10016
|
-
|
|
10017
|
-
|
|
10015
|
+
failures. A throwing or invalid exit-status callback becomes a normalized
|
|
10016
|
+
`INTERNAL_SERVER_ERROR`. A throwing or non-string `present` preserves the validated
|
|
10017
|
+
result on stdout and reports `CLI_PRESENTATION_FAILED` separately, as described
|
|
10018
|
+
under [Aggregate views](#aggregate-views). Void native commands cannot declare
|
|
10019
|
+
either callback.
|
|
10018
10020
|
|
|
10019
10021
|
`--version`, a selected native command and its command help run before
|
|
10020
10022
|
`resolveAuth`, services, context or runtime-tool factories. Top-level help is
|
|
@@ -10229,6 +10231,7 @@ newline-terminated JSON record on its respective stream; progress and CLI usage
|
|
|
10229
10231
|
diagnostics remain ordinary stderr text. This keeps stdout pipeable and
|
|
10230
10232
|
`2>/dev/null` clean. The process exit code carries the error class (`0` ok,
|
|
10231
10233
|
`VALIDATION_ERROR → 1`, `UNAUTHORIZED → 2`, `FORBIDDEN → 3`, `NOT_FOUND → 4`,
|
|
10234
|
+
`CLI_PRESENTATION_FAILED → 8`,
|
|
10232
10235
|
…) — override per app with `exitCodes`.
|
|
10233
10236
|
|
|
10234
10237
|
### A narrower question than "all of them"
|
|
@@ -10289,13 +10292,42 @@ command name — validated against the declared schema, and kept out of every
|
|
|
10289
10292
|
operation's arguments: `handoff_read` above receives `{ handoffId: 'u' }` and
|
|
10290
10293
|
nothing else. The values reach `resolveAuth(globals)`, `context(auth, globals)`
|
|
10291
10294
|
and a native command's `globals`. An invalid value is an argument error naming
|
|
10292
|
-
the flag
|
|
10293
|
-
|
|
10295
|
+
the flag. An application global that collides with a framework option is refused
|
|
10296
|
+
at startup. Field collisions in authored commands are configuration errors;
|
|
10297
|
+
runtime catalogue entries are isolated as described below. Past a bare
|
|
10294
10298
|
`--` every token is a literal, so a positional value that reads like a global
|
|
10295
10299
|
survives intact.
|
|
10296
10300
|
|
|
10297
10301
|
Both help levels list them under `Application options:`.
|
|
10298
10302
|
|
|
10303
|
+
## When one runtime command cannot be represented in argv
|
|
10304
|
+
|
|
10305
|
+
A `runtimeTools` catalogue can change independently of the CLI application. An
|
|
10306
|
+
entry whose input declares a framework option such as `json`, or a field shadowed
|
|
10307
|
+
by an application global such as `caller`, cannot receive that field through argv.
|
|
10308
|
+
Only that entry becomes unavailable. Other commands still run, and top-level,
|
|
10309
|
+
filtered and command-specific help retain the entry and its full reason:
|
|
10310
|
+
|
|
10311
|
+
```text
|
|
10312
|
+
inspect Inspect an operation [unavailable]
|
|
10313
|
+
Unavailable: [stitchkit] CLI command "inspect" declares field(s) shadowed by application global options: caller
|
|
10314
|
+
```
|
|
10315
|
+
|
|
10316
|
+
Calling it returns `CLI_COMMAND_UNAVAILABLE`, `retryable: false`, and the same
|
|
10317
|
+
reason in `details.message` on stderr; the default exit code is 1. `--json`
|
|
10318
|
+
compacts the error as usual; `exitCodes` and `errorHint` apply. The refusal precedes
|
|
10319
|
+
command argument parsing, stdin, context, hooks and the handler; `--dry-run` cannot
|
|
10320
|
+
turn it into success. A known unavailable `--wait` target returns the same refusal;
|
|
10321
|
+
an unknown target keeps `NOT_FOUND`.
|
|
10322
|
+
|
|
10323
|
+
This isolates only reserved/global **field** conflicts in `runtimeTools`.
|
|
10324
|
+
Native commands and contract-defined `services` still fail at startup for those
|
|
10325
|
+
conflicts. Duplicate or reserved command names and invalid local aliases,
|
|
10326
|
+
positionals or passthrough policies remain configuration errors for every entry.
|
|
10327
|
+
`createCliInvoker` receives globals and command input separately and has no argv
|
|
10328
|
+
projection, so its own `caller` or `json` input stays callable without renaming.
|
|
10329
|
+
See [ADR 0273](../decisions/0273-a-runtime-cli-field-conflict-is-one-commands-unavailability.md).
|
|
10330
|
+
|
|
10299
10331
|
## When the managed surface cannot resolve
|
|
10300
10332
|
|
|
10301
10333
|
A CLI whose commands come from a running server declares them with a factory,
|
|
@@ -10421,7 +10453,7 @@ would answer a question nobody asked.
|
|
|
10421
10453
|
|
|
10422
10454
|
Three rules worth knowing before you rely on them:
|
|
10423
10455
|
|
|
10424
|
-
- **A field the result does not carry is
|
|
10456
|
+
- **A field the result does not carry is a presentation error.** A group of zero
|
|
10425
10457
|
over a misspelled field is indistinguishable from a true empty answer, and the
|
|
10426
10458
|
caller reads it as data. The message names the fields that *are* there.
|
|
10427
10459
|
- **An aggregate needs a collection** — the result itself when it is an array,
|
|
@@ -10435,6 +10467,17 @@ Three rules worth knowing before you rely on them:
|
|
|
10435
10467
|
A failed call still reports its own error and exit code. An aggregate over an
|
|
10436
10468
|
error is not an answer to the question that was asked.
|
|
10437
10469
|
|
|
10470
|
+
If the command succeeds but its requested view cannot be built, the CLI writes
|
|
10471
|
+
the complete original result to stdout, using the normal JSON format (`--json`
|
|
10472
|
+
still selects compact output). It writes a separate structured
|
|
10473
|
+
`CLI_PRESENTATION_FAILED` diagnostic to stderr and exits with `8`, configurable
|
|
10474
|
+
through `exitCodes`. Its `details.operationSucceeded: true` and `retryable: false`
|
|
10475
|
+
distinguish a presentation refusal from a failed operation; `details.cause`
|
|
10476
|
+
preserves the safe reason. Recover the answer from stdout instead of repeating
|
|
10477
|
+
the mutation. Domain retry hints are not appended to this diagnostic. CLI
|
|
10478
|
+
argument errors detected before execution still exit with `2`
|
|
10479
|
+
and have no successful result to preserve.
|
|
10480
|
+
|
|
10438
10481
|
## Named profiles
|
|
10439
10482
|
|
|
10440
10483
|
A CLI that talks to a deployed server needs an address and a credential per
|
|
@@ -13068,7 +13111,7 @@ core never models them (ADR 0002). But stitchkit itself emits a set of its own:
|
|
|
13068
13111
|
`FILE_INSPECTION_REJECTED`, `FILE_TOO_LARGE`, `FILE_EXISTS`,
|
|
13069
13112
|
`REALTIME_CONTRACT_VIOLATION`, `APPLICATION_NOT_ACCEPTING`, `NOT_IMPLEMENTED`,
|
|
13070
13113
|
`WAIT_TIMEOUT`,
|
|
13071
|
-
`WAIT_FAILED`, `DOWNLOAD_NOT_FOUND`, `VIEW_HTTP_ERROR`,
|
|
13114
|
+
`WAIT_FAILED`, `CLI_COMMAND_UNAVAILABLE`, `CLI_PRESENTATION_FAILED`, `DOWNLOAD_NOT_FOUND`, `VIEW_HTTP_ERROR`,
|
|
13072
13115
|
`CONNECTION_TIMEOUT`, `CONNECTION_RESPONSE_TOO_LARGE`,
|
|
13073
13116
|
`CONNECTION_REQUEST_FAILED`, `UPSTREAM_TOOL_ERROR`,
|
|
13074
13117
|
`OPERATION_NOT_SUCCEEDED`, `STREAM_ITEM_INVALID`, `STREAM_FRAME_TOO_LARGE`,
|
|
@@ -23593,7 +23636,7 @@ SDK nor the `ai` peer.
|
|
|
23593
23636
|
|
|
23594
23637
|
| Export | Kind | Summary |
|
|
23595
23638
|
|--------|------|---------|
|
|
23596
|
-
| `createCli` | function | build and run a CLI from contracts — [guide](../guide/cli.md) |
|
|
23639
|
+
| `createCli` | function | build and run a CLI from contracts; a runtime catalogue field conflicting with reserved/global argv options disables only that entry, retained in help with its reason; invocation returns `CLI_COMMAND_UNAVAILABLE` before side effects — [guide](../guide/cli.md#when-one-runtime-command-cannot-be-represented-in-argv) |
|
|
23597
23640
|
| `defineCliCommand` | function | define one Zod-typed CLI-only executable command with optional validated-result presentation/exit policy |
|
|
23598
23641
|
| `parseCliArgs` | function | argv → typed tool args against a schema; inline `--field:=JSON` assigns a whole JSON value, including null, while ordinary string flags remain lexical; named array options together with bare positionals require an explicit `positionals` policy (advanced) |
|
|
23599
23642
|
| `routeCliArgv` | function | select the command out of argv without duplicating the global-option grammar (advanced) |
|
package/llms.txt
CHANGED
|
@@ -18,7 +18,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
|
|
|
18
18
|
- **stitchkit/tools/contract**: [llms/tools-contract.txt](llms/tools-contract.txt) 0.2 KB
|
|
19
19
|
- **stitchkit/tools/invoker**: [llms/tools-invoker.txt](llms/tools-invoker.txt) 0.2 KB
|
|
20
20
|
- **stitchkit/tools/connections**: [llms/tools-connections.txt](llms/tools-connections.txt) 3.9 KB
|
|
21
|
-
- **stitchkit/cli**: [llms/cli.txt](llms/cli.txt)
|
|
21
|
+
- **stitchkit/cli**: [llms/cli.txt](llms/cli.txt) 33.9 KB → [llms/cli.2.txt](llms/cli.2.txt) 36.4 KB
|
|
22
22
|
- **stitchkit/cli/publish**: [llms/cli-publish.txt](llms/cli-publish.txt) 1.5 KB
|
|
23
23
|
- **stitchkit/remote**: [llms/remote.txt](llms/remote.txt) 1.0 KB
|
|
24
24
|
- **stitchkit/contract**: [llms/contract.txt](llms/contract.txt) 31.9 KB
|
package/package.json
CHANGED