@klhapp/skillmux 1.12.0 → 1.13.0
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 +8 -0
- package/README.md +18 -9
- package/bin/skillmux.js +89 -0
- package/docs/README.md +4 -4
- package/docs/cli.md +14 -4
- package/docs/concepts.md +2 -2
- package/docs/configuration.md +18 -3
- package/docs/deployment.md +3 -3
- package/docs/getting-started.md +32 -7
- package/docs/releasing.md +17 -2
- package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
- package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
- package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
- package/docs/sdd/cli-remote-target-parity/think.md +183 -0
- package/docs/sdd/cli-surface-consistency/think.md +149 -0
- package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
- package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
- package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
- package/docs/sdd/feat-packaging-deployment/review.md +95 -0
- package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
- package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
- package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
- package/docs/sdd/npm-native-binaries/review.md +103 -0
- package/docs/sdd/npm-native-binaries/spec.md +148 -0
- package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
- package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
- package/docs/sdd/runtime-resource-hardening/review.md +86 -0
- package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
- package/docs/sdd/security-hardening-phase/think.md +129 -0
- package/docs/sdd/security-observability/review.md +96 -0
- package/docs/sdd/security-observability/spec.md +55 -0
- package/docs/sdd/skill-provenance-update/review.md +152 -0
- package/docs/sdd/skill-provenance-update/schema.json +193 -0
- package/docs/sdd/skill-provenance-update/spec.md +134 -0
- package/docs/sdd/supply-chain-hardening/review.md +50 -0
- package/docs/sdd/supply-chain-hardening/spec.md +42 -0
- package/docs/sdd/target-marker-rehome/spec.md +33 -0
- package/docs/troubleshooting.md +18 -2
- package/package.json +15 -8
- package/src/adapters.ts +0 -438
- package/src/audit.ts +0 -21
- package/src/cli.ts +0 -928
- package/src/clients.ts +0 -368
- package/src/commands/audit.ts +0 -85
- package/src/commands/config.ts +0 -224
- package/src/commands/context.ts +0 -104
- package/src/commands/core.ts +0 -56
- package/src/commands/doctor.ts +0 -97
- package/src/commands/eval.ts +0 -88
- package/src/commands/init.ts +0 -672
- package/src/commands/install.ts +0 -155
- package/src/commands/local-vault.ts +0 -60
- package/src/commands/models.ts +0 -10
- package/src/commands/outdated.ts +0 -115
- package/src/commands/project.ts +0 -580
- package/src/commands/report.ts +0 -66
- package/src/commands/scan.ts +0 -71
- package/src/commands/shared.ts +0 -38
- package/src/commands/skill.ts +0 -33
- package/src/commands/sync.ts +0 -233
- package/src/commands/target.ts +0 -225
- package/src/commands/update.ts +0 -277
- package/src/completions.ts +0 -196
- package/src/concurrency-limiter.ts +0 -61
- package/src/config-service.ts +0 -398
- package/src/config-watcher.ts +0 -253
- package/src/config.ts +0 -583
- package/src/context.ts +0 -188
- package/src/db-audit.ts +0 -286
- package/src/db-index.ts +0 -238
- package/src/db.ts +0 -3
- package/src/deployment.ts +0 -39
- package/src/doctor.ts +0 -205
- package/src/eval.ts +0 -302
- package/src/global-flags.ts +0 -46
- package/src/init-agents.ts +0 -329
- package/src/init-instructions.ts +0 -192
- package/src/init.ts +0 -390
- package/src/install.ts +0 -232
- package/src/lifecycle.ts +0 -51
- package/src/logger.ts +0 -26
- package/src/manifest.ts +0 -359
- package/src/mcp-registration.ts +0 -89
- package/src/metrics.ts +0 -121
- package/src/models.ts +0 -20
- package/src/output.ts +0 -214
- package/src/project-setup.ts +0 -36
- package/src/prompts.ts +0 -124
- package/src/provenance.ts +0 -99
- package/src/rate-limiter.ts +0 -137
- package/src/readiness.ts +0 -30
- package/src/redact.ts +0 -52
- package/src/router-core.ts +0 -666
- package/src/rrf.ts +0 -31
- package/src/scan.ts +0 -300
- package/src/server.ts +0 -930
- package/src/setup.ts +0 -145
- package/src/snapshot.ts +0 -135
- package/src/stats.ts +0 -340
- package/src/sync.ts +0 -482
- package/src/toml-writer.ts +0 -51
- package/src/types.ts +0 -212
- package/src/vault.ts +0 -207
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<!-- plane_project: SMX -->
|
|
2
|
+
<!-- plane_module: fetch-outcome-flywheel -->
|
|
3
|
+
|
|
4
|
+
# Spec: Fetch-outcome routing quality flywheel
|
|
5
|
+
|
|
6
|
+
## Goal
|
|
7
|
+
|
|
8
|
+
Skillmux records what it proposed and never what the agent took. `resolve_skill`
|
|
9
|
+
writes an audit row (`src/router-core.ts:406`, `src/db.ts:292`) holding the
|
|
10
|
+
query, retrieval capability, candidates, and latency. `fetch_skill` writes
|
|
11
|
+
nothing. Every quality number the project can produce today comes from the
|
|
12
|
+
hand-curated holdout set in `eval/queries.json`.
|
|
13
|
+
|
|
14
|
+
Close the loop. Log fetch outcomes, correlate each one to the resolve that
|
|
15
|
+
produced it, and turn real traffic into two things that do not exist yet: a
|
|
16
|
+
measured acceptance signal in `skillmux report`, and a path to grow the eval set
|
|
17
|
+
from observed behavior instead of hand authorship.
|
|
18
|
+
|
|
19
|
+
Because that correlation joins raw user queries to the skills people actually
|
|
20
|
+
opened, the audit store also gets its own file, a retention window, and a
|
|
21
|
+
cleanup path in this same change.
|
|
22
|
+
|
|
23
|
+
## Storage
|
|
24
|
+
|
|
25
|
+
Audit data moves out of `index.sqlite3` and into `audit.sqlite3`, in the same
|
|
26
|
+
`state_dir`. Both files sit under the directory operators already persist
|
|
27
|
+
(`docs/deployment.md:292`), so deployment and backup instructions gain a file
|
|
28
|
+
rather than a procedure.
|
|
29
|
+
|
|
30
|
+
The split is cheap because audit data is already self-contained. The only read
|
|
31
|
+
is a plain `SELECT ... FROM audit` (`src/stats.ts:150`) with no join to `skills`
|
|
32
|
+
or `vectors`, and `computeStats` derives per-skill counts from the stored
|
|
33
|
+
candidates JSON rather than from the index. The resolve-to-fetch join added here
|
|
34
|
+
is audit-to-audit, so both tables live in the new file and never reach across.
|
|
35
|
+
|
|
36
|
+
Separating the files buys four things that motivate doing it now rather than
|
|
37
|
+
later:
|
|
38
|
+
|
|
39
|
+
1. Copying or backing up the derived index no longer carries raw user queries.
|
|
40
|
+
2. Retention and file permissions can differ between telemetry and index state.
|
|
41
|
+
3. Reclaiming space after a prune no longer locks the index. The new file is
|
|
42
|
+
created with `PRAGMA auto_vacuum = INCREMENTAL`, which the existing file
|
|
43
|
+
cannot be given without a full rewrite.
|
|
44
|
+
4. Audit writes stop contending with an index rebuild for the single WAL writer.
|
|
45
|
+
|
|
46
|
+
Paths resolve as they do today: `~/.local/state/skillmux/audit.sqlite3` for a CLI
|
|
47
|
+
install (`src/config.ts:111`), `/data/audit.sqlite3` in a container
|
|
48
|
+
(`src/config.ts:255`).
|
|
49
|
+
|
|
50
|
+
## Acceptance Criteria
|
|
51
|
+
|
|
52
|
+
### Storage and migration
|
|
53
|
+
|
|
54
|
+
1. Audit data is read and written in `audit.sqlite3` under `state_dir`, opened
|
|
55
|
+
separately from `index.sqlite3`, with WAL journaling and
|
|
56
|
+
`auto_vacuum = INCREMENTAL` set at creation.
|
|
57
|
+
2. On first open after upgrade, existing `audit` rows in `index.sqlite3` are
|
|
58
|
+
copied into `audit.sqlite3` within a single transaction and the old table is
|
|
59
|
+
dropped. The migration is idempotent: a second run finds nothing to move and
|
|
60
|
+
changes nothing.
|
|
61
|
+
|
|
62
|
+
### Correlation
|
|
63
|
+
|
|
64
|
+
3. A `resolve_skill` response carries a `request_id` string, unique per request,
|
|
65
|
+
in both the text content and `structuredContent`. The audit row written for
|
|
66
|
+
that resolve stores the same value.
|
|
67
|
+
4. Audit rows written before this feature, which have no `request_id`, still
|
|
68
|
+
load, report, and render without error.
|
|
69
|
+
5. `fetch_skill` accepts an optional `request_id`. When it names a known resolve,
|
|
70
|
+
the recorded fetch links to that resolve's audit row.
|
|
71
|
+
6. `fetch_skill` called without a `request_id` succeeds and records an
|
|
72
|
+
uncorrelated fetch.
|
|
73
|
+
7. `fetch_skill` called with an unknown or malformed `request_id` succeeds and
|
|
74
|
+
records an uncorrelated fetch. Skill delivery never fails because telemetry
|
|
75
|
+
could not correlate.
|
|
76
|
+
8. Each recorded fetch stores timestamp, `skill_id`, `request_id` or null,
|
|
77
|
+
originating audit row id or null, and `rank_at_resolve`. `rank_at_resolve` is
|
|
78
|
+
the fetched skill's rank in that resolve's shortlist, or null when the fetch
|
|
79
|
+
is uncorrelated or the skill was absent from that shortlist.
|
|
80
|
+
|
|
81
|
+
### Reporting
|
|
82
|
+
|
|
83
|
+
9. `skillmux report` reports `acceptance_rate`, `observed_mrr`, and
|
|
84
|
+
`top1_acceptance_rate` over resolves that returned at least one candidate. A
|
|
85
|
+
resolve counts as accepted when at least one correlated fetch exists.
|
|
86
|
+
`observed_mrr` uses the reciprocal rank of the first fetched candidate.
|
|
87
|
+
10. When a window contains no correlated fetches, `report` marks the acceptance
|
|
88
|
+
signal unavailable rather than printing `0.000`, and states how many
|
|
89
|
+
uncorrelated fetches it saw.
|
|
90
|
+
11. `report` lists the top queries that returned candidates and received no
|
|
91
|
+
correlated fetch, as a list distinct from the existing empty-shortlist list.
|
|
92
|
+
|
|
93
|
+
### Retention and cleanup
|
|
94
|
+
|
|
95
|
+
12. Configuration exposes `audit.retention_days`, defaulting to 90. A value of
|
|
96
|
+
`0` disables pruning and retains rows indefinitely. Invalid values are
|
|
97
|
+
rejected at config load in the existing error style.
|
|
98
|
+
13. Pruning deletes resolve rows and fetch rows older than the window, each by
|
|
99
|
+
its own timestamp. A fetch whose resolve row has been pruned reports as
|
|
100
|
+
uncorrelated rather than erroring, reusing the path that already exists for
|
|
101
|
+
fetches that carried no `request_id`.
|
|
102
|
+
14. Pruning runs once at server startup and at most once per 24 hours while the
|
|
103
|
+
server runs. It never blocks readiness and never blocks a resolve.
|
|
104
|
+
15. `skillmux audit prune [--older-than <window>] [--dry-run] [--yes] [--json]`
|
|
105
|
+
prunes on demand and reports rows removed per table. `--dry-run` performs no
|
|
106
|
+
writes and reports what would be removed. `--older-than` accepts the window
|
|
107
|
+
syntax already used by `--since` (`src/stats.ts:4`).
|
|
108
|
+
16. After a prune, incremental vacuum reclaims file space, and the operation
|
|
109
|
+
never takes a lock on `index.sqlite3`.
|
|
110
|
+
|
|
111
|
+
### Eval promotion
|
|
112
|
+
|
|
113
|
+
17. `skillmux eval promote --since <window> --yes` writes eval cases shaped
|
|
114
|
+
`{ query, split: "observed", relevant_skill_ids }`, where the skill ids are
|
|
115
|
+
those actually fetched, deduplicated by normalized query. It writes to a path
|
|
116
|
+
other than `eval/queries.json` unless one is given explicitly, and warns that
|
|
117
|
+
promoted cases contain raw user queries. Output validates under
|
|
118
|
+
`parseEvalCases` (`src/eval.ts:15`).
|
|
119
|
+
18. `eval promote` never rewrites an existing case. A query already present in
|
|
120
|
+
the target file is skipped and counted in the summary.
|
|
121
|
+
|
|
122
|
+
### Cross-cutting
|
|
123
|
+
|
|
124
|
+
19. `report`, `audit prune`, and `eval promote` honor `--json` against the
|
|
125
|
+
documented output schema. Existing exit-code behavior is unchanged.
|
|
126
|
+
|
|
127
|
+
## Scope
|
|
128
|
+
|
|
129
|
+
- A separate `audit.sqlite3` store with an `openAudit(stateDir)` entry point
|
|
130
|
+
beside `openIndex`, and a one-time transactional migration of existing rows.
|
|
131
|
+
- `request_id` generation, storage, and wire exposure on `resolve_skill`.
|
|
132
|
+
- Optional `request_id` input on `fetch_skill` (`src/server.ts:112`) and a fetch
|
|
133
|
+
audit table, following the migration pattern already in `src/db.ts:52`.
|
|
134
|
+
- New quality metrics in `StatsResponse` (`src/stats.ts:23`), the text renderer,
|
|
135
|
+
and the JSON output.
|
|
136
|
+
- `audit.retention_days` configuration, automatic pruning, and a
|
|
137
|
+
`skillmux audit prune` subcommand.
|
|
138
|
+
- `skillmux eval promote` producing `EvalCase` records.
|
|
139
|
+
- Contract and docs updates: `docs/schema.json`, `docs/mcp-routing.md` (tool
|
|
140
|
+
contract, recommended agent behavior, audit data), `docs/cli.md` (report, the
|
|
141
|
+
new audit command, eval), and `docs/deployment.md` (the second state file).
|
|
142
|
+
|
|
143
|
+
## Out of Scope
|
|
144
|
+
|
|
145
|
+
- **Time-window heuristic correlation.** Correlation is by explicit `request_id`
|
|
146
|
+
only. Guessing which resolve a bare fetch belongs to manufactures signal that
|
|
147
|
+
reads as measurement, which is worse than a stated gap.
|
|
148
|
+
- **Query redaction and targeted deletion.** Retention is age-based only. There
|
|
149
|
+
is no facility to delete one user's queries or to hash queries at rest.
|
|
150
|
+
- Any use of observed data to change ranking automatically. No retraining, no
|
|
151
|
+
threshold tuning, no reweighting. This feature measures and proposes, and a
|
|
152
|
+
human promotes.
|
|
153
|
+
- Per-client or per-context routing profiles.
|
|
154
|
+
- Changes to the retrieval pipeline, fusion, or reranker behavior.
|
|
155
|
+
- Exporting audit data to an external log or metrics pipeline.
|
|
156
|
+
- Any dashboard or web view. `report` and `--json` are the surface.
|
|
157
|
+
|
|
158
|
+
## Tasks
|
|
159
|
+
|
|
160
|
+
Ordered so that later slices land in the new store rather than being moved
|
|
161
|
+
twice.
|
|
162
|
+
|
|
163
|
+
1. **Split the audit store.** Add `openAudit(stateDir)`, create `audit.sqlite3`
|
|
164
|
+
with WAL and incremental auto-vacuum, migrate existing rows out of
|
|
165
|
+
`index.sqlite3` transactionally, and repoint `src/stats.ts` and
|
|
166
|
+
`src/router-core.ts` at the new handle.
|
|
167
|
+
2. **`request_id` on resolve.** Add the column, generate the id in
|
|
168
|
+
`router-core`, thread it into the audit insert and both response channels,
|
|
169
|
+
and update `docs/schema.json`.
|
|
170
|
+
3. **Fetch outcome logging.** Add the fetch table and insert helper, the optional
|
|
171
|
+
`request_id` on the `fetch_skill` input schema, a correlation lookup that
|
|
172
|
+
degrades to uncorrelated on any miss, and rank resolution against the stored
|
|
173
|
+
candidates.
|
|
174
|
+
4. **Retention and cleanup.** Add `audit.retention_days`, the prune routine with
|
|
175
|
+
incremental vacuum, the startup and daily triggers, and the
|
|
176
|
+
`skillmux audit prune` subcommand.
|
|
177
|
+
5. **Quality metrics in report.** Extend `computeStats`, the renderer, and the
|
|
178
|
+
JSON shape with acceptance rate, observed MRR, top-1 acceptance, unused
|
|
179
|
+
shortlist queries, and the signal-unavailable case.
|
|
180
|
+
6. **`skillmux eval promote`.** Window parsing reusing `parseSince`, case
|
|
181
|
+
construction, dedupe against the target file, the `--yes` gate, `--dry-run`,
|
|
182
|
+
and `--json`.
|
|
183
|
+
7. **Docs pass.** MCP routing contract and audit section, CLI report, audit and
|
|
184
|
+
eval sections, deployment note on the second state file, and the standing note
|
|
185
|
+
that `fetch_skill` remains usable without a prior resolve.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Review — npm-native-binaries
|
|
2
|
+
|
|
3
|
+
Branch `feat/npm-native-binaries`, 7 commits, 24 files, +1243/-79.
|
|
4
|
+
Fan-out: compliance, security, correctness, performance, tests, build, suite.
|
|
5
|
+
|
|
6
|
+
## Verdict
|
|
7
|
+
|
|
8
|
+
Spec 15/15. `bunx tsc --noEmit` clean. Suite 954/954.
|
|
9
|
+
|
|
10
|
+
## AC Status
|
|
11
|
+
|
|
12
|
+
| Issue | Criterion | Status | Notes |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
| SMX-142 | build:binaries compiles five targets through the sharp stub | ✅ Done | `tests/build-binaries.test.ts` builds all five |
|
|
15
|
+
| SMX-143 | build:npm-packages emits platform packages with os, cpu, bin, 0o755 | ✅ Done | `tests/package-npm-binaries.test.ts` |
|
|
16
|
+
| SMX-144 | Platform packages carry onnxruntime libraries, darwin-x64 exempt | ✅ Done | Verified against the real payload, not a fixture |
|
|
17
|
+
| SMX-145 | Root package.json points bin at the launcher and pins five optional deps | ✅ Done | Verified from a packed tarball |
|
|
18
|
+
| SMX-146 | Polyglot shebang runs under bun-only and node-only | ✅ Done | Both directions covered with PATH narrowed |
|
|
19
|
+
| SMX-147 | Launcher resolves the package, forwards argv, exit code and signals | ✅ Done | Also verified in both global install layouts |
|
|
20
|
+
| SMX-148 | Launcher injects the library path so local inference works | ✅ Done | `embeddings: 1 backfilled` from the packaged binary |
|
|
21
|
+
| SMX-149 | SKILLMUX_BINARY overrides the resolved executable | ✅ Done | |
|
|
22
|
+
| SMX-150 | Missing platform package fails loudly | ✅ Done | |
|
|
23
|
+
| SMX-151 | Dockerfile entrypoint is the binary, healthcheck drops bun and node | ✅ Done | |
|
|
24
|
+
| SMX-152 | full ships libonnxruntime.so.1, slim stays lexical | ⚠️ Manual-verify | Verified on linux/arm64 only; amd64 needs CI |
|
|
25
|
+
| SMX-153 | docker-smoke.sh passes for slim and full on amd64 and arm64 | ⚠️ Manual-verify | Both arm64 lanes pass locally. amd64 verified only as far as the cross-compiled executable running under emulation; the full lane needs CI |
|
|
26
|
+
| SMX-154 | npm pack ships bin/skillmux.js and not src/ | ✅ Done | |
|
|
27
|
+
| SMX-155 | Release workflow publishes platform packages before the root | ✅ Done | Ordering asserted in `tests/release-workflow.test.ts` |
|
|
28
|
+
| SMX-156 | All five executables attached to the GitHub Release | ✅ Done | Added mid-implementation, see back-edge below |
|
|
29
|
+
|
|
30
|
+
Issue keys are listed so `sdd-cleanup` does not have to match criteria to
|
|
31
|
+
issues by title. Use this column rather than the titles.
|
|
32
|
+
|
|
33
|
+
The post-review run posted the two MANUAL-VERIFY verdicts and none of the
|
|
34
|
+
thirteen PASS ones. That was first read as a title-matching failure, which
|
|
35
|
+
was wrong. The comment timestamps show only two writes, two seconds apart,
|
|
36
|
+
both successful, so it was not a rate limit either; and SMX-152 matched
|
|
37
|
+
despite its criterion text not being a substring of its title, while SMX-149
|
|
38
|
+
went uncommented despite being a clean substring. The discriminator was the
|
|
39
|
+
verdict value, not the text: the recipe skipped every PASS row. Fixed in
|
|
40
|
+
sdd-cleanup on 2026-09-14, along with a verdict-line format violation and a
|
|
41
|
+
missing Plane rate-limit guard.
|
|
42
|
+
|
|
43
|
+
## Findings
|
|
44
|
+
|
|
45
|
+
### Accepted and fixed
|
|
46
|
+
|
|
47
|
+
**`bun run build` produced an executable that cannot embed.** The root `build`
|
|
48
|
+
script still ran a raw `bun build --compile`, which skips the sharp stub, so the
|
|
49
|
+
executable it produced silently lost local inference. `ci.yml` and `README.md`
|
|
50
|
+
both invoke it. `build` now routes through `scripts/build-binaries.ts host`.
|
|
51
|
+
|
|
52
|
+
The review reported this as "the test will fail", which was not true: the suite
|
|
53
|
+
was green before and after, because the script string still matched its
|
|
54
|
+
assertion. The defect was real; the stated symptom was not.
|
|
55
|
+
|
|
56
|
+
### Rejected
|
|
57
|
+
|
|
58
|
+
**`SKILLMUX_BINARY` is spawned without validation** (security, Medium). Not a
|
|
59
|
+
vulnerability. An attacker who can set environment variables for your shell can
|
|
60
|
+
already run code as you; `PATH` and `NODE_OPTIONS` are the same shape. esbuild
|
|
61
|
+
ships `ESBUILD_BINARY_PATH` with identical semantics. Validating it would remove
|
|
62
|
+
the escape hatch the troubleshooting docs point at without closing any path.
|
|
63
|
+
|
|
64
|
+
**The launcher misses the package under a global install** (correctness, High).
|
|
65
|
+
Tested rather than argued. Installed the packed tarball into a temporary npm
|
|
66
|
+
prefix and placed the platform package in both layouts npm produces, then ran
|
|
67
|
+
the launcher from `/` so the working directory could not contribute:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
佈局 A hoisted, lib/node_modules/@klhapp/skillmux-darwin-arm64 -> 1.12.0
|
|
71
|
+
佈局 B nested, .../skillmux/node_modules/@klhapp/... -> 1.12.0
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`require.resolve` with `paths: [here]` walks every ancestor `node_modules`, which
|
|
75
|
+
covers both. The finding also reviewed a stale diff, since the `build` fix was
|
|
76
|
+
still uncommitted when the pass ran.
|
|
77
|
+
|
|
78
|
+
## Needs decision
|
|
79
|
+
|
|
80
|
+
**`bin` in the platform packages.** Discovered while testing the global install:
|
|
81
|
+
installing a platform package globally alongside the root package fails with an
|
|
82
|
+
`EEXIST` on `$PREFIX/bin/skillmux`, because both declare `bin.skillmux`. The
|
|
83
|
+
normal flow never hits this, since npm links bins only for the package being
|
|
84
|
+
installed, not its dependencies.
|
|
85
|
+
|
|
86
|
+
esbuild declares `bin` in its platform packages so a package manager can link the
|
|
87
|
+
native executable directly and skip the JavaScript shim on machines without
|
|
88
|
+
Node. Our polyglot shebang already removes that need, so the field buys little
|
|
89
|
+
and can conflict. Removing it contradicts the spec, which names `bin` explicitly
|
|
90
|
+
in AC2, so this is left as written pending a decision.
|
|
91
|
+
|
|
92
|
+
## Back-edge to spec
|
|
93
|
+
|
|
94
|
+
One, recorded during implementation. The launcher's missing-package message
|
|
95
|
+
sends every platform to the releases page, but only the two Linux executables
|
|
96
|
+
were attached there. The spec gained an acceptance criterion for attaching all
|
|
97
|
+
five before the change was made, rather than widening scope silently.
|
|
98
|
+
|
|
99
|
+
## Test flake
|
|
100
|
+
|
|
101
|
+
One run of the suite reported a single failure that did not recur across three
|
|
102
|
+
subsequent clean runs, and the failing test name was not captured. Unresolved
|
|
103
|
+
and unattributed. Worth watching on CI rather than assuming it is gone.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
## Goal
|
|
2
|
+
|
|
3
|
+
Ship Skillmux as native per-platform executables on npm so that `npm install`,
|
|
4
|
+
`npx`, and `bun add -g` all work with no runtime prerequisite, while keeping
|
|
5
|
+
local ONNX embedding inference working on every platform that has an
|
|
6
|
+
onnxruntime build.
|
|
7
|
+
|
|
8
|
+
## Acceptance Criteria
|
|
9
|
+
|
|
10
|
+
- [ ] `bun run build:binaries` compiles five executables into `dist/bin/` for
|
|
11
|
+
`bun-darwin-arm64`, `bun-darwin-x64`, `bun-linux-x64`, `bun-linux-arm64`,
|
|
12
|
+
and `bun-windows-x64`, each built through a bundler plugin that resolves
|
|
13
|
+
`sharp` to `scripts/stubs/sharp.ts`.
|
|
14
|
+
- [ ] `bun run build:npm-packages` emits `dist/npm/@klhapp/skillmux-<platform>-<arch>/`
|
|
15
|
+
for each target, where `<platform>` is `process.platform` (`win32`, not
|
|
16
|
+
`windows`), each containing a `package.json` whose `version` equals the root
|
|
17
|
+
`version` and which declares `os`, `cpu`, and `bin`, plus the executable at
|
|
18
|
+
mode `0o755`.
|
|
19
|
+
- [ ] Each platform package except `@klhapp/skillmux-darwin-x64` also contains
|
|
20
|
+
that platform's onnxruntime runtime libraries; `darwin-x64` ships without
|
|
21
|
+
them because `onnxruntime-node` publishes no `darwin/x64` artifacts.
|
|
22
|
+
- [ ] Root `package.json` sets `bin` to `{ "skillmux": "./bin/skillmux.js" }`
|
|
23
|
+
and lists all five platform packages under `optionalDependencies` pinned to
|
|
24
|
+
the exact root version.
|
|
25
|
+
- [ ] `bin/skillmux.js` starts with a `#!/bin/sh` polyglot line that execs
|
|
26
|
+
whichever of `bun` or `node` exists, and runs correctly with neither `node`
|
|
27
|
+
nor `bun` absent from `PATH` in turn.
|
|
28
|
+
- [ ] The launcher resolves the platform package via `require.resolve` against
|
|
29
|
+
`__dirname`, `dirname(process.argv[1])`, and `process.cwd()`, forwards
|
|
30
|
+
`process.argv.slice(2)` with `stdio: "inherit"`, mirrors the child exit code,
|
|
31
|
+
and forwards `SIGINT` and `SIGTERM`.
|
|
32
|
+
- [ ] The launcher prepends the platform package's library directory to
|
|
33
|
+
`DYLD_FALLBACK_LIBRARY_PATH` on darwin, `LD_LIBRARY_PATH` on linux, and
|
|
34
|
+
`PATH` on win32, so that `skillmux index` against a local-inference config
|
|
35
|
+
reports `embeddings: N backfilled` rather than a dlopen failure.
|
|
36
|
+
- [ ] `SKILLMUX_BINARY` overrides the resolved executable path.
|
|
37
|
+
- [ ] With no matching platform package installed, the launcher exits non-zero
|
|
38
|
+
with a message naming the missing package and the standalone download
|
|
39
|
+
procedure, and never falls back to a partially working mode silently.
|
|
40
|
+
- [ ] `Dockerfile` sets `ENTRYPOINT ["/usr/local/bin/skillmux"]`, keeps
|
|
41
|
+
`CMD ["serve", "--transport", "http"]`, and its `HEALTHCHECK` probes
|
|
42
|
+
`http://127.0.0.1:3000/health/ready` without invoking `bun` or `node`.
|
|
43
|
+
- [ ] The `full` stage ships `libonnxruntime.so.1` with `LD_LIBRARY_PATH` set;
|
|
44
|
+
the `slim` stage does not ship it, preserving `retrieval_capability`
|
|
45
|
+
`hybrid` for full and `lexical` for slim.
|
|
46
|
+
- [ ] `bash tests/docker-smoke.sh` passes unchanged for both `slim` and `full`
|
|
47
|
+
on `linux/amd64` and `linux/arm64`.
|
|
48
|
+
- [ ] `npm pack --dry-run` on the root package lists `bin/skillmux.js` and does
|
|
49
|
+
not list `src/`.
|
|
50
|
+
- [ ] The release workflow publishes all five platform packages before the root
|
|
51
|
+
package, and GitHub Release assets keep the existing `skillmux-linux-amd64`
|
|
52
|
+
and `skillmux-linux-arm64` names.
|
|
53
|
+
- [ ] All five executables are attached to the GitHub Release, not only the two
|
|
54
|
+
Linux ones. The launcher's missing-package message sends users of every
|
|
55
|
+
platform to that page, so a macOS or Windows reader must find something
|
|
56
|
+
there.
|
|
57
|
+
|
|
58
|
+
## Scope
|
|
59
|
+
|
|
60
|
+
The binary build and packaging scripts, the launcher, the `sharp` build-time
|
|
61
|
+
stub, root `package.json` metadata, the `Dockerfile` migration to the compiled
|
|
62
|
+
binary, `tests/package-metadata.test.ts` and `tests/dockerfile.test.ts`
|
|
63
|
+
updates, release workflow changes, and installation documentation in
|
|
64
|
+
`README.md`, `docs/getting-started.md`, `docs/deployment.md`, and
|
|
65
|
+
`docs/troubleshooting.md`.
|
|
66
|
+
|
|
67
|
+
## Out of Scope
|
|
68
|
+
|
|
69
|
+
Porting the codebase off Bun to Node. Changing retrieval, routing, config, or
|
|
70
|
+
CLI behavior. Restoring local inference on `darwin-x64`. Publishing
|
|
71
|
+
`win32-arm64`, which `bun build --compile` does not target. Changing the
|
|
72
|
+
remote-inference or reranker paths. Altering the GHCR and Docker Hub tagging
|
|
73
|
+
scheme.
|
|
74
|
+
|
|
75
|
+
## Tasks
|
|
76
|
+
|
|
77
|
+
1. Add the `sharp` stub and `scripts/build-binaries.ts`, with a test asserting
|
|
78
|
+
all five executables are produced and the host one reports the root version.
|
|
79
|
+
2. Add `scripts/package-npm-binaries.ts` and the platform-package layout test,
|
|
80
|
+
including the onnxruntime library payload and `darwin-x64` exemption.
|
|
81
|
+
3. Write `bin/skillmux.js` and its launcher tests: runtime selection, argv and
|
|
82
|
+
exit-code forwarding, signal forwarding, library-path injection, override,
|
|
83
|
+
and the missing-package error.
|
|
84
|
+
4. Switch root `package.json` to the launcher and update
|
|
85
|
+
`tests/package-metadata.test.ts`.
|
|
86
|
+
5. Migrate the `Dockerfile` to the compiled binary and update
|
|
87
|
+
`tests/dockerfile.test.ts`; verify both smoke lanes.
|
|
88
|
+
6. Extend the release workflow to build and publish the platform packages.
|
|
89
|
+
This must also keep the root `optionalDependencies` pins in step with the
|
|
90
|
+
version Release Please writes, or every release ships a root package
|
|
91
|
+
pointing at the previous release's platform packages. The pins are exact,
|
|
92
|
+
and `tests/package-metadata.test.ts` fails when they drift, so a stale pin
|
|
93
|
+
turns the Release Please PR red rather than shipping quietly.
|
|
94
|
+
7. Update the installation and troubleshooting documentation. This must state
|
|
95
|
+
which platforms get local inference and which fall back to lexical recall,
|
|
96
|
+
and must say why `darwin-x64` is the exception, including that the exception
|
|
97
|
+
is expected to be temporary. See "darwin-x64 is an upstream regression"
|
|
98
|
+
below.
|
|
99
|
+
|
|
100
|
+
## Verified Constraints
|
|
101
|
+
|
|
102
|
+
These were established empirically on 2026-09-13 against Skillmux 1.12.0 with
|
|
103
|
+
Bun 1.4.2 and Node v24.21.0. They are the reason the design looks the way it
|
|
104
|
+
does; re-deriving them costs an hour.
|
|
105
|
+
|
|
106
|
+
- A bun-compiled binary cannot load `onnxruntime-node` unaided. The `.node`
|
|
107
|
+
addon is embedded and extracted to a temp path, but its dylib is not, and
|
|
108
|
+
`@rpath/libonnxruntime.1.24.3.dylib` then resolves to that temp directory.
|
|
109
|
+
Pointing `DYLD_FALLBACK_LIBRARY_PATH` at the real library directory fixes it.
|
|
110
|
+
- `--external` does not help. A compiled binary performs no `node_modules`
|
|
111
|
+
resolution at runtime; an external specifier fails as
|
|
112
|
+
`Cannot find package '<name>' imported from /$bunfs/root/<binary>`. The same
|
|
113
|
+
applies to a nested bare import reached through an absolute-path dynamic
|
|
114
|
+
import.
|
|
115
|
+
- `@huggingface/transformers` eagerly requires `sharp`, which text embedding
|
|
116
|
+
never uses. Replacing it with a throwing stub at bundle time is what makes
|
|
117
|
+
`models download` and `index` succeed in the compiled binary.
|
|
118
|
+
- `bun install` honours `os` and `cpu` for registry packages; installing
|
|
119
|
+
`esbuild` pulls only `@esbuild/darwin-arm64`. `npm` rejects a mismatched
|
|
120
|
+
platform package with `notsup`.
|
|
121
|
+
- `bun add -g` symlinks the `bin` JS file directly, so a `#!/usr/bin/env node`
|
|
122
|
+
shebang fails on a Bun-only machine. The `#!/bin/sh` polyglot line runs under
|
|
123
|
+
bun-only, node-only, and both.
|
|
124
|
+
|
|
125
|
+
## darwin-x64 is an upstream regression, not a permanent limit
|
|
126
|
+
|
|
127
|
+
`onnxruntime-node` 1.24.3 ships `bin/napi-v6/darwin/arm64` but no `darwin/x64`
|
|
128
|
+
directory at all, which is why that target needs the `onnxruntime-node` stub to
|
|
129
|
+
compile and loses local inference. This is not a deliberate platform drop.
|
|
130
|
+
It is tracked upstream as a packaging regression introduced in the 1.23.x
|
|
131
|
+
series, where the Intel macOS artifact is still built in CI but does not reach
|
|
132
|
+
the npm tarball:
|
|
133
|
+
|
|
134
|
+
- https://github.com/microsoft/onnxruntime/issues/27961
|
|
135
|
+
- https://github.com/huggingface/transformers.js/issues/1634
|
|
136
|
+
|
|
137
|
+
`onnxruntime-node` 1.23.2 is the last release that carries the Intel macOS
|
|
138
|
+
binding.
|
|
139
|
+
|
|
140
|
+
`hasNativeOnnxruntime` probes the filesystem rather than consulting a hardcoded
|
|
141
|
+
list, so if a future `onnxruntime-node` restores `darwin/x64`, that target stops
|
|
142
|
+
being stubbed and starts carrying its library payload with no code change. Keep
|
|
143
|
+
it that way. The alternative, vendoring the 1.23.2 binding into the darwin-x64
|
|
144
|
+
package alone, mixes ONNX Runtime versions across platforms and should only be
|
|
145
|
+
considered if the upstream regression proves permanent.
|
|
146
|
+
|
|
147
|
+
<!-- plane_project: SMX -->
|
|
148
|
+
<!-- plane_module: npm-native-binaries -->
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# PR 3 private implementation packet: calibration removal
|
|
2
|
+
|
|
3
|
+
This file is internal, ignored, and must never be staged or committed.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Remove Skillmux's obsolete threshold-calibration product surface and all code
|
|
8
|
+
used only to implement it. Ranking evaluation remains. Existing
|
|
9
|
+
`calibrate.sqlite3` files on operator disks are never opened, migrated, edited,
|
|
10
|
+
or deleted by the new code.
|
|
11
|
+
|
|
12
|
+
## Required behavior
|
|
13
|
+
|
|
14
|
+
1. `skillmux calibrate ...` is removed from command dispatch, help, Docker
|
|
15
|
+
command policy, completions, and public adapter interfaces. Invoking it must
|
|
16
|
+
fail non-zero with a concise 2.0 migration message that says threshold
|
|
17
|
+
calibration was removed and points users to `skillmux eval` for ranking
|
|
18
|
+
evaluation.
|
|
19
|
+
2. Remove calibration from local/remote capability types and responses. Delete
|
|
20
|
+
calibration admin endpoint declarations and handlers rather than returning
|
|
21
|
+
501 placeholders. Requests to the old routes follow the server's ordinary
|
|
22
|
+
unknown-route behavior.
|
|
23
|
+
3. Remove calibration-only implementation: dataset generator, dataset decision
|
|
24
|
+
parser, optimizer, certification gates, resume/checkpoint/persistence,
|
|
25
|
+
calibration timing/retry plumbing, apply/config mutation, and calibration
|
|
26
|
+
fingerprints or watchers that have no non-calibration consumer.
|
|
27
|
+
4. Remove decision-threshold code that has no remaining runtime consumer.
|
|
28
|
+
Do not alter ranked resolution, retrieval, reranking, evaluation, audit
|
|
29
|
+
reporting, or skill delivery behavior.
|
|
30
|
+
5. Remove `inference.calibration` from accepted configuration. A legacy
|
|
31
|
+
`[inference.calibration]` block must fail with an actionable message saying
|
|
32
|
+
it is obsolete in 2.0 and should be deleted. Existing obsolete threshold
|
|
33
|
+
errors remain actionable.
|
|
34
|
+
6. Remove calibration-only source files and tests. Update shared tests rather
|
|
35
|
+
than weakening unrelated assertions. Ranking-evaluation tests remain.
|
|
36
|
+
7. Remove calibration references from current CLI help, completions, server
|
|
37
|
+
capabilities, generated public schema, configuration examples, and focused
|
|
38
|
+
command/API documentation required for truthful current surfaces. Leave the
|
|
39
|
+
broad documentation rewrite and migration guide to PR 4 unless a statement
|
|
40
|
+
would otherwise advertise a command or endpoint that no longer exists.
|
|
41
|
+
8. Do not unlink, rename, migrate, truncate, or write any existing
|
|
42
|
+
`calibrate.sqlite3`. Add a regression test proving startup/doctor/eval leaves
|
|
43
|
+
a sentinel calibration database byte-for-byte unchanged.
|
|
44
|
+
9. Do not stage or commit anything under `docs/sdd/`. Preserve all ignored
|
|
45
|
+
local artifacts.
|
|
46
|
+
|
|
47
|
+
## Verification
|
|
48
|
+
|
|
49
|
+
- Start with failing public-interface tests for the removed command,
|
|
50
|
+
capabilities/routes, legacy config error, and untouched sentinel database.
|
|
51
|
+
- Work in red-green-refactor cycles and commit each logical cycle.
|
|
52
|
+
- Search tracked source, tests, schemas, examples, CLI help, and completions for
|
|
53
|
+
stale executable calibration surfaces.
|
|
54
|
+
- Run `bun test`, `bun x tsc --noEmit`, `npm run build`, and `git diff --check`.
|
|
55
|
+
- Return commits, deleted files, verification totals, and any intentionally
|
|
56
|
+
deferred documentation references.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
## Goal
|
|
2
|
+
|
|
3
|
+
Make deterministic built-in target directories an application rule rather than
|
|
4
|
+
duplicated absolute paths in the Skillmux manifest.
|
|
5
|
+
|
|
6
|
+
## Acceptance Criteria
|
|
7
|
+
|
|
8
|
+
- [ ] `agent-skills`, `claude-code`, and `codex` targets resolve their native
|
|
9
|
+
directories from their names when syncing, managing projects, or rendering
|
|
10
|
+
target information.
|
|
11
|
+
- [ ] New or rewritten manifest entries for those built-in targets omit `dir`.
|
|
12
|
+
- [ ] Existing manifests that contain a built-in target `dir` remain readable
|
|
13
|
+
and continue to resolve the built-in native directory, without using a
|
|
14
|
+
non-standard persisted path as an override.
|
|
15
|
+
- [ ] A custom target requires `dir`, and its configured directory remains the
|
|
16
|
+
path used by all target operations.
|
|
17
|
+
- [ ] The CLI exposes a deliberate migration path that removes redundant
|
|
18
|
+
built-in `dir` fields without changing target markers, managed links, or
|
|
19
|
+
project pins.
|
|
20
|
+
- [ ] Tests cover built-in resolution, legacy manifest compatibility, custom
|
|
21
|
+
target behavior, and migration idempotence.
|
|
22
|
+
|
|
23
|
+
## Scope
|
|
24
|
+
|
|
25
|
+
Manifest parsing, serialization, target-directory resolution, relevant CLI
|
|
26
|
+
output and operations, documentation, and migration tests.
|
|
27
|
+
|
|
28
|
+
## Out of Scope
|
|
29
|
+
|
|
30
|
+
Changing native directory conventions, moving target directories or markers,
|
|
31
|
+
retargeting skills, changing custom target paths, or automatically mutating a
|
|
32
|
+
manifest during ordinary sync.
|
|
33
|
+
|
|
34
|
+
## Tasks
|
|
35
|
+
|
|
36
|
+
1. Define the manifest and target-resolution contract, including compatibility
|
|
37
|
+
with legacy built-in `dir` fields.
|
|
38
|
+
2. Add tests for resolution and an explicit idempotent migration command.
|
|
39
|
+
3. Route target, sync, init, and project operations through the shared
|
|
40
|
+
resolver and update manifest serialization.
|
|
41
|
+
4. Document the built-in/custom distinction and migration workflow.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Review: Runtime Resource Hardening
|
|
2
|
+
|
|
3
|
+
Branch `feat/runtime-resource-hardening` vs `main`, 5 commits (a6ec66a, 933315c, 8b7d66b, ac27004,
|
|
4
|
+
654974c — the last is the post-review fix described below).
|
|
5
|
+
Reviewed manually — `goose run --recipe sdd-review` hit the same 24h Bifrost token-quota
|
|
6
|
+
exhaustion seen during slice 1 (`token limit exceeded (279006/250000, resets every 24h)`);
|
|
7
|
+
fell back to the skill's documented manual path (sequential focus passes + native test/build runs).
|
|
8
|
+
|
|
9
|
+
## Test Execution
|
|
10
|
+
|
|
11
|
+
`bun test`: **787 pass, 0 fail, 2006 expect() calls** (58 files, 24.14s) at initial review; **792 pass, 0 fail, 2013 expect() calls** after the post-review fix commit (5 new unit tests for `releaseOnStreamClose`).
|
|
12
|
+
|
|
13
|
+
## Build Check
|
|
14
|
+
|
|
15
|
+
`bunx tsc --noEmit`: **clean**, exit 0.
|
|
16
|
+
|
|
17
|
+
## Compliance
|
|
18
|
+
|
|
19
|
+
| Criterion | Status | Notes |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| AC1: default bounds (1 MiB body, 100 concurrent) apply with no `[server]` config | ✅ Done | `?? DEFAULT_MAX_BODY_BYTES` / `?? DEFAULT_MAX_CONCURRENT_REQUESTS`; the 413-default test runs against a config with no `[server]` section at all, confirming this is the true zero-config path, not an overridden one. |
|
|
22
|
+
| AC2: oversized body → 413 before full buffering | ✅ Done | Native `Bun.serve` `maxRequestBodySize`; verified empirically earlier this session (413 returned pre-handler). |
|
|
23
|
+
| AC3: once `max_concurrent_requests` in-flight, next request → 503 | ✅ Done | Fixed post-review (commit 654974c): `releaseOnStreamClose` now defers the release until the response body stream actually drains or is cancelled, so long-lived SSE connections hold their slot for their real duration. See Correctness finding below for detail and why HTTP-level round-trip coverage wasn't added. |
|
|
24
|
+
| AC4: both bounds configurable via `[server]` | ✅ Done | zod schema + wiring; tested with `max_body_bytes = 100`, `max_concurrent_requests = 0`. |
|
|
25
|
+
| AC5: `allowed_hosts` unset → embedding/reranker unaffected | ✅ Done | `assertHostAllowed` no-ops on `undefined`/`[]`; covered by the unset-allowlist reranker test and pre-existing embedding tests. |
|
|
26
|
+
| AC6: disallowed embedding host → `RemoteInferenceError(configuration)` pre-fetch | ✅ Done | Tested; asserts zero requests reached the mock fetch. |
|
|
27
|
+
| AC7: disallowed reranker host → same | ✅ Done | Tested; asserts zero requests reached the mock fetch. |
|
|
28
|
+
| AC8: allowed host → proceeds unchanged | ✅ Done | Tested for both embedding and reranker. |
|
|
29
|
+
|
|
30
|
+
No schema.json exists for this feature (config-shape-only change, not a new API surface) — consistent with the spec's own scope.
|
|
31
|
+
|
|
32
|
+
## Security
|
|
33
|
+
|
|
34
|
+
- Host-allowlist reuse (`assertHostAllowed`/`extractHost`) is unmodified from slice 1 (already reviewed then); no new bypass surface — exact-match on `URL.hostname`, so userinfo tricks (`http://evil@allowed.example.com/`) and suffix tricks (`allowed.example.com.evil.com`) don't bypass it.
|
|
35
|
+
- Reranker's host check is skipped when `docs.length === 0` (early return before the check) — harmless, since that path never reaches `fetch` either.
|
|
36
|
+
- Concurrency/body-size checks run before CORS, rate-limiting, and auth — correct ordering for resource protection (cheapest, most attacker-facing check first).
|
|
37
|
+
- The SSE connection-exhaustion gap (the "hold the connection open" attack a concurrency bound is meant to stop) had a real security dimension — see Correctness below; it's now fixed.
|
|
38
|
+
|
|
39
|
+
## Correctness — Resolved (was Needs Decision)
|
|
40
|
+
|
|
41
|
+
User chose option 1 (wrap the stream body to release on close/cancel). Implemented in commit
|
|
42
|
+
654974c as `releaseOnStreamClose` (`src/concurrency-limiter.ts`), wired into the fetch handler's
|
|
43
|
+
try/catch in place of the immediate `finally`-release. Verified with 5 new unit tests
|
|
44
|
+
(`tests/concurrency-limiter.test.ts`): held-while-open, released-on-full-drain,
|
|
45
|
+
released-on-cancel, no-body short-circuits immediately, release-at-most-once (idempotent).
|
|
46
|
+
|
|
47
|
+
An HTTP-level round-trip regression test (open a real SSE stream via the MCP transport, assert
|
|
48
|
+
503 while held, cancel, assert 200 after) was attempted and abandoned: Bun's own `fetch()` client,
|
|
49
|
+
called from the same process as the server under test, does not resolve with response headers
|
|
50
|
+
until the first body chunk is enqueued — so a deliberately-idle open SSE stream (no data pushed)
|
|
51
|
+
never lets the client-side `fetch()` promise resolve, and the test hangs. Confirmed via a minimal
|
|
52
|
+
Bun.serve probe that a real out-of-process client (`curl`) *does* receive headers immediately for
|
|
53
|
+
the same handler shape — this is a same-process test-harness limitation specific to Bun's `fetch()`,
|
|
54
|
+
not a gap in production behavior. Retained to Hindsight (`infra` bank) as a gotcha for future
|
|
55
|
+
streaming-endpoint tests in this repo.
|
|
56
|
+
|
|
57
|
+
**Original finding (for the record): concurrency slot released on Response construction, not stream closure, for SSE responses.**
|
|
58
|
+
|
|
59
|
+
`src/server.ts`'s wrapper:
|
|
60
|
+
```ts
|
|
61
|
+
if (!concurrencyLimiter.tryAcquire()) { ... }
|
|
62
|
+
try {
|
|
63
|
+
return await handleHttpRequest(req, server);
|
|
64
|
+
} finally {
|
|
65
|
+
concurrencyLimiter.release();
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
`handleHttpRequest` resolves as soon as it returns a `Response` — including a `Response(readableStream, { headers: {"Content-Type": "text/event-stream"} })` from the MCP SDK's `webStandardStreamableHttp.js` (used for both the GET SSE notification channel and POST streaming replies). The `finally` fires at that point, not when the stream is actually drained/canceled by the client disconnecting. Net effect: `max_concurrent_requests` bounds the *synchronous handling* portion of a request, not the *lifetime of an open streaming connection* — the code's own comment ("protects against connection exhaustion") oversells what's implemented, and an attacker opening many SSE streams isn't actually capped by this bound.
|
|
69
|
+
|
|
70
|
+
This wasn't caught by tests because the added HTTP-level test for AC3 deliberately uses `max_concurrent_requests = 0` (a synchronous-rejection check) rather than a real concurrent-streaming scenario — a reasonable choice to avoid a flaky test, but it means the streaming gap has no regression coverage either.
|
|
71
|
+
|
|
72
|
+
**Options** (design decision, not auto-fixed):
|
|
73
|
+
1. Wrap the returned `Response`'s body (when it's a `ReadableStream`) so `release()` fires on stream `cancel()`/close instead of on `Response` construction — closes the real gap, adds complexity to the wrapper.
|
|
74
|
+
2. Accept current behavior as "bounds concurrent request-*processing* work, not connection lifetime," and correct the comment/docs to say so precisely, deferring true streaming-connection bounding to a later slice.
|
|
75
|
+
3. Add a separate, explicit cap on concurrent SSE streams (distinct from the request-processing counter) if connection exhaustion is the primary threat model for this transport.
|
|
76
|
+
|
|
77
|
+
## Test Coverage (static)
|
|
78
|
+
|
|
79
|
+
Adequate. `releaseOnStreamClose` is covered directly at the unit level (5 tests); the wiring itself
|
|
80
|
+
(`res.body → releaseOnStreamClose(res.body, () => concurrencyLimiter.release())`) is a single line
|
|
81
|
+
verified by `tsc --noEmit` and the unchanged 413/503 HTTP tests staying green (buffered-body
|
|
82
|
+
responses drain through the wrapper almost instantly, so existing behavior is unaffected).
|
|
83
|
+
|
|
84
|
+
## Performance
|
|
85
|
+
|
|
86
|
+
No concerns — `ConcurrencyLimiter.tryAcquire`/`release` are O(1) synchronous counter ops; `maxRequestBodySize` is native to Bun with no manual buffering.
|