@klhapp/skillmux 1.11.2 → 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 +15 -0
- package/README.md +20 -10
- package/bin/skillmux.js +89 -0
- package/docs/README.md +4 -4
- package/docs/cli.md +15 -5
- package/docs/concepts.md +4 -3
- package/docs/configuration.md +22 -5
- 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/skill-management.md +1 -1
- 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 -389
- package/src/install.ts +0 -232
- package/src/lifecycle.ts +0 -51
- package/src/logger.ts +0 -26
- package/src/manifest.ts +0 -352
- 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,43 @@
|
|
|
1
|
+
<!-- source: docs/sdd/security-hardening-phase/think.md -->
|
|
2
|
+
<!-- plane_project: SMX -->
|
|
3
|
+
<!-- plane_module: runtime-resource-hardening -->
|
|
4
|
+
|
|
5
|
+
# Spec: Runtime Resource Hardening
|
|
6
|
+
|
|
7
|
+
Slice 2 of 3 from the security-hardening-phase (see `docs/sdd/security-hardening-phase/think.md`).
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Close the two remaining "unenforced" gaps from the security-hardening-phase's resource/egress area: (1) the HTTP transport has no positive bound on request body size or concurrent connections, and (2) remote-inference (embedding/reranker) calls ignore the `[egress] allowed_hosts` allowlist that slice 1 (supply-chain-hardening, merged as PR #160) already enforces for install/update git fetches.
|
|
12
|
+
|
|
13
|
+
## Acceptance Criteria
|
|
14
|
+
|
|
15
|
+
1. With no `[server]` body/concurrency config set at all, the `--transport http` listener still applies default bounds (1 MiB request body, 100 concurrent in-flight requests) — behavior is never unbounded by omission. Unlike the egress allowlist, these are *positive* defaults, not opt-in.
|
|
16
|
+
2. A request whose body exceeds `server.max_body_bytes` (or the 1 MiB default) is rejected with `413 Payload Too Large` before the full body is buffered into memory.
|
|
17
|
+
3. Once `server.max_concurrent_requests` (or the default of 100) in-flight requests are being handled, an additional concurrent request is rejected with `503 Service Unavailable` rather than queued or accepted.
|
|
18
|
+
4. Both bounds are configurable via `[server] max_body_bytes` / `max_concurrent_requests`.
|
|
19
|
+
5. With `[egress] allowed_hosts` unset, remote embedding/reranker calls are unaffected — unchanged, backward-compatible behavior.
|
|
20
|
+
6. With `allowed_hosts` set, an embedding-endpoint call to a disallowed host is rejected before the network fetch, surfaced as a `RemoteInferenceError` (kind `configuration`) so `resolve_skill` degrades exactly the way it already does for other embedding config failures — not a raw crash.
|
|
21
|
+
7. With `allowed_hosts` set, a reranker-endpoint call to a disallowed host is rejected the same way, before the network fetch.
|
|
22
|
+
8. With `allowed_hosts` set, an embedding/reranker host that *is* listed proceeds unchanged.
|
|
23
|
+
|
|
24
|
+
## Scope
|
|
25
|
+
|
|
26
|
+
- `[server] max_body_bytes` / `max_concurrent_requests` config, applied to the main `--transport http` `Bun.serve` listener (`src/server.ts`).
|
|
27
|
+
- Host-check in `src/clients.ts`'s `embed`/`rerank` fetch calls, reusing `assertHostAllowed`/`extractHost` from `src/install.ts` (the function slice 1 already built — not reimplemented).
|
|
28
|
+
|
|
29
|
+
## Out of Scope
|
|
30
|
+
|
|
31
|
+
- The `--stats-port` listener (GET-only, no body, lower risk) — no body-size bound needed there; concurrency bound not added this slice either, to keep scope to the primary HTTP transport attack surface.
|
|
32
|
+
- Supply-chain git-fetch egress allowlist enforcement (already shipped, slice 1).
|
|
33
|
+
- Centralized secret redaction, tamper-evident admin audit trail (slice 3: security-observability).
|
|
34
|
+
- Signer verification, SBOM/provenance, posture-visibility command, privacy purge (deferred phase-wide per `think.md`).
|
|
35
|
+
|
|
36
|
+
## Tasks
|
|
37
|
+
|
|
38
|
+
1. Config schema: add `max_body_bytes` / `max_concurrent_requests` to `ServerConfig`/`configSchema` (`src/types.ts`, `src/config.ts`), with code-level defaults matching the existing `config.server || {...}` fallback pattern, since `server` itself is optional.
|
|
39
|
+
2. Wire body-size bound into the http-transport `Bun.serve` call (`src/server.ts`).
|
|
40
|
+
3. Wire a concurrency counter (increment/decrement around the `fetch` handler) with a `max_concurrent_requests` cap (`src/server.ts`).
|
|
41
|
+
4. Host-check in `src/clients.ts`: call `assertHostAllowed` on `embedding.endpoint` and `reranker.endpoint` before each fetch, wrapping a thrown allowlist error as `RemoteInferenceError("configuration", ...)`.
|
|
42
|
+
5. Docs: extend `docs/configuration.md`'s `[server]` and `[egress]` sections.
|
|
43
|
+
6. Tests: default bounds apply without config, 413/503 behavior, config overrides, egress unset/blocked/allowed for both embedding and reranker.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Think: Security-Hardening Phase ("the big one")
|
|
2
|
+
|
|
3
|
+
**Mode:** Builder — hardening an existing, shipping system (skillmux), not a new product.
|
|
4
|
+
|
|
5
|
+
## Background
|
|
6
|
+
|
|
7
|
+
Sequenced 2026-07-21 as the roadmap phase after direction 4 (skill lifecycle
|
|
8
|
+
and distribution) and before the now-cancelled direction 5 (team/governance,
|
|
9
|
+
killed 2026-08-29 for lacking a concrete spec). Reconfirmed 2026-08-29 when
|
|
10
|
+
the user resolved the outstanding Security ASK from the skill-provenance-update
|
|
11
|
+
review and confirmed proceeding.
|
|
12
|
+
|
|
13
|
+
Original decision (2026-07-21) framed this phase as necessary because
|
|
14
|
+
acquiring and distributing agent instructions from git expands the trust
|
|
15
|
+
boundary, and listed 12 sub-areas: secure non-loopback exposure, fail-closed
|
|
16
|
+
separate MCP/admin authentication, positive resource bounds, filesystem
|
|
17
|
+
containment, source/inference egress policy, quarantine and hash-bound
|
|
18
|
+
approval, optional signer verification, centralized secret redaction,
|
|
19
|
+
privacy reporting/purge, tamper-evident mutation audit, security posture
|
|
20
|
+
visibility, and release SBOM/provenance/adversarial tests. Team accounts,
|
|
21
|
+
RBAC, SSO, and multi-tenancy were explicitly out of scope from the start
|
|
22
|
+
(that became the cancelled direction 5).
|
|
23
|
+
|
|
24
|
+
No think/spec doc existed for the phase as a whole before this session — a
|
|
25
|
+
meaningful slice shipped ad hoc as individual point-fix PRs first (#146-#158),
|
|
26
|
+
each addressing one concrete bug rather than working from a plan.
|
|
27
|
+
|
|
28
|
+
## Job-to-be-done
|
|
29
|
+
|
|
30
|
+
Both network-exposure safety and supply-chain safety are in scope, weighted
|
|
31
|
+
toward what benefits skillmux's OSS users broadly — this is the public repo,
|
|
32
|
+
and most users self-host, so the deliverable is "any user who runs
|
|
33
|
+
`skillmux serve --transport http` or installs from an unfamiliar git source
|
|
34
|
+
can trust the boundary held," not narrowly what benefits this user's own
|
|
35
|
+
Bifrost (stdio-transport) deployment. Bifrost's stdio connection to skillmux
|
|
36
|
+
never touches the network, so full-HTTP-surface hardening doesn't protect it
|
|
37
|
+
directly — the one exception is the `--stats-port` listener (PR #158), which
|
|
38
|
+
does open a narrow HTTP surface even under stdio transport and already
|
|
39
|
+
inherits the non-loopback bind-posture rule from #153.
|
|
40
|
+
|
|
41
|
+
## Landscape: what already exists (read during this session)
|
|
42
|
+
|
|
43
|
+
Reconciling the original 12-item scope against current `main` (commit
|
|
44
|
+
`97887fe`):
|
|
45
|
+
|
|
46
|
+
| # | Sub-area | Status |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| 1 | Secure non-loopback exposure | **Shipped** — #153 refuses non-loopback bind without auth; `--stats-port` inherits the same rule |
|
|
49
|
+
| 2 | Fail-closed, separate MCP/admin auth | **Shipped, already mature** — `/admin/v1/*` uses a distinct bearer token (`server.admin.token_env`), 403 if disabled, 401 if unauthorized, timing-safe compare (`safeTokenEquals`, #157), ETag-based optimistic concurrency on `PATCH /admin/v1/config`. Documented in `docs/README.md`, `concepts.md`, `cli.md`, `deployment.md`. **This was discovered mid-session — an earlier forcing question in this doc's own design process incorrectly assumed no admin HTTP surface existed. It does, and it's done.** |
|
|
50
|
+
| 3 | Positive resource bounds | **Partial** — `rate-limiter.ts` (LRU-bounded, #155) bounds *request count* per client. No bound on request body size or concurrent connections. |
|
|
51
|
+
| 4 | Filesystem containment | **Mostly shipped** — path-traversal / symlink guards across install (#146, #151), update (#150), sync (#152) |
|
|
52
|
+
| 5 | Source/inference egress policy | **Partial** — scp-like URL and `file://` source rejection shipped (#148, #156); no allowlist for install/update git hosts or remote-inference API hosts |
|
|
53
|
+
| 6 | Quarantine / hash-bound approval | **Shipped, discovered mid-`/sdd-tdd` for the supply-chain-hardening slice** — `src/provenance.ts` (`hashSkillContent`/`content_hash` on `.skillmux-origin`) + `src/commands/update.ts`'s `buildPlan` already bind every update to a local-drift check and an unconditional scan before write, tested in `tests/cli.test.ts`'s `skillmux update CLI` suite. This row originally said "Not started" — that was wrong, the same class of landscape-research miss as item 2 above (grep the source, don't trust the prior scope decision alone). |
|
|
54
|
+
| 7 | Optional signer verification | Not started — **deferred this phase** |
|
|
55
|
+
| 8 | Centralized secret redaction | **Not started** — zero `redact` hits across 99 log call-sites in `server.ts`/`cli.ts` |
|
|
56
|
+
| 9 | Privacy reporting/purge | Not started — **deferred this phase** |
|
|
57
|
+
| 10 | Tamper-evident mutation audit | **Not started, now well-scoped** — `PATCH /admin/v1/config` mutates live config with zero audit trail: no record of who/when/what-changed |
|
|
58
|
+
| 11 | Security posture visibility | Not started — **deferred this phase** |
|
|
59
|
+
| 12 | Release SBOM/provenance + adversarial tests | Not started — **deferred this phase** |
|
|
60
|
+
|
|
61
|
+
## Decisions made this session
|
|
62
|
+
|
|
63
|
+
1. **Scope weighting:** both network-exposure and supply-chain safety in
|
|
64
|
+
scope, weighted toward OSS users (public repo) over this user's own
|
|
65
|
+
stdio-only Bifrost deployment.
|
|
66
|
+
2. **Done-state:** ship the highest-risk half now (6 items), defer the rest
|
|
67
|
+
to the same evidence-of-demand bucket the deferred direction-5 items
|
|
68
|
+
(registry/distribution/versioning, RBAC, dashboards, marketplace,
|
|
69
|
+
autonomous optimization) already live in.
|
|
70
|
+
3. **Auth split (item 2) removed from the shipping list** — not because it
|
|
71
|
+
was deferred, but because it turned out to already be fully shipped.
|
|
72
|
+
Net effect on the plan is the same as the original "defer" answer, but
|
|
73
|
+
for a different, now-corrected reason.
|
|
74
|
+
4. **Quarantine (item 6) scoped narrowly:** hash-pin the scan verdict, not a
|
|
75
|
+
full staging/approval workflow. Content-hash the skill directory at scan
|
|
76
|
+
time, store the hash alongside `.skillmux-origin`; on install/update, a
|
|
77
|
+
changed hash forces a fresh scan. No new CLI subcommand, no staging
|
|
78
|
+
directory, `--yes` one-shot install flow is unchanged.
|
|
79
|
+
5. **Egress policy (item 5) scoped broadly:** one `[egress] allowed_hosts`
|
|
80
|
+
config list, unset by default (backward compatible, matches skillmux's
|
|
81
|
+
opt-in security posture elsewhere), enforced at both `resolveRepoSource`
|
|
82
|
+
(install/update git fetches) and remote-inference embedding/reranker
|
|
83
|
+
calls.
|
|
84
|
+
6. **Resource bounds (item 3) scoped systematically:** beyond the existing
|
|
85
|
+
per-client request-count limiter, add positive bounds on request body
|
|
86
|
+
size and concurrent connections on the HTTP transport.
|
|
87
|
+
7. **Tamper-evident mutation audit (item 10) scoped concretely:** log every
|
|
88
|
+
`PATCH /admin/v1/config` mutation (actor context available from the
|
|
89
|
+
request, changed keys, old/new values, resulting revision hash) to an
|
|
90
|
+
audit trail, reusing the existing audit-DB infrastructure from the
|
|
91
|
+
routing-quality flywheel (`src/db.ts`) rather than inventing a new store.
|
|
92
|
+
|
|
93
|
+
## Final shipping list (this phase)
|
|
94
|
+
|
|
95
|
+
1. Systematic resource bounds (request body size + concurrency caps) on the
|
|
96
|
+
HTTP transport.
|
|
97
|
+
2. Egress allowlist (`[egress] allowed_hosts`) covering install/update git
|
|
98
|
+
sources and remote-inference calls.
|
|
99
|
+
3. Hash-pinned scan verdicts (quarantine, narrow scope) for install/update.
|
|
100
|
+
4. Centralized secret redaction in logging output.
|
|
101
|
+
5. Tamper-evident audit trail for `PATCH /admin/v1/config` mutations.
|
|
102
|
+
|
|
103
|
+
## Deferred (evidence-of-demand bucket, alongside the already-deferred
|
|
104
|
+
direction-5 items)
|
|
105
|
+
|
|
106
|
+
- Fail-closed admin/MCP auth split — **removed, not deferred: already shipped.**
|
|
107
|
+
- Optional signer verification for installed skill content.
|
|
108
|
+
- Release SBOM/provenance + adversarial security tests.
|
|
109
|
+
- `skillmux security-status`-style posture visibility command.
|
|
110
|
+
- Privacy reporting/purge.
|
|
111
|
+
|
|
112
|
+
## Slicing for /sdd-spec
|
|
113
|
+
|
|
114
|
+
Three domain-grouped specs, each independently shippable, matching the
|
|
115
|
+
skill-provenance-update precedent (full spec → schema → TDD → review cycle
|
|
116
|
+
per slice):
|
|
117
|
+
|
|
118
|
+
1. **`supply-chain-hardening`** — egress allowlist for install/update
|
|
119
|
+
sources + hash-pinned scan verdicts. Both live in `install.ts` /
|
|
120
|
+
`update.ts` / `scan.ts` / the vault-ingestion path.
|
|
121
|
+
2. **`runtime-resource-hardening`** — positive resource bounds on the HTTP
|
|
122
|
+
transport + egress allowlist enforcement for remote-inference calls.
|
|
123
|
+
Both live in `server.ts` / `config.ts`.
|
|
124
|
+
3. **`security-observability`** — centralized secret redaction in logging +
|
|
125
|
+
tamper-evident audit trail for admin config mutations. Both live in the
|
|
126
|
+
logging surface and the `/admin/v1/config` handler, and both write to
|
|
127
|
+
observability-facing output.
|
|
128
|
+
|
|
129
|
+
Run `/sdd-spec` once per slice, starting with whichever the user picks next.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!-- source: docs/sdd/security-observability/spec.md -->
|
|
2
|
+
|
|
3
|
+
# Review: Security Observability
|
|
4
|
+
|
|
5
|
+
Branch `feat/security-observability` vs `main`, 12 commits (a7ec27b through 6cbf8e0).
|
|
6
|
+
Reviewed manually — `goose run --recipe sdd-review` hit the same 24h Bifrost
|
|
7
|
+
token-quota exhaustion seen repeatedly across this phase (`token limit
|
|
8
|
+
exceeded (279006/250000, resets every 24h)`) — this is now the fifth
|
|
9
|
+
occurrence across supply-chain-hardening, runtime-resource-hardening, and
|
|
10
|
+
this slice combined, hitting every SDD pipeline stage at least once. Fell
|
|
11
|
+
back to the skill's documented manual path (sequential focus passes + native
|
|
12
|
+
test/build runs).
|
|
13
|
+
|
|
14
|
+
## Test Execution
|
|
15
|
+
|
|
16
|
+
`bun test`: **808 pass, 0 fail, 2053 expect() calls** (61 files) — includes the
|
|
17
|
+
regression test added during this review for the `--verbose` finding below.
|
|
18
|
+
|
|
19
|
+
## Build Check
|
|
20
|
+
|
|
21
|
+
`bunx tsc --noEmit`: **clean**, exit 0.
|
|
22
|
+
|
|
23
|
+
## Compliance
|
|
24
|
+
|
|
25
|
+
| Criterion | Status | Notes |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| AC1: centralized `redact(text)` resolves `*_env` secret values | ✅ Done | `buildRedactor(config)` recursively walks the config tree for any `*_env`-suffixed key and resolves it against `process.env`. This also catches `server.auth_token_env` and `server.admin.token_env` (the MCP and admin bearer tokens), not just the inference `api_key_env` fields — verified by inspection, matching AC1's literal enumeration. |
|
|
28
|
+
| AC2: credential-bearing git URL redacted from clone/ls-remote failures | ✅ Done, with a scope note | The real leak (a user-typed `https://user:TOKEN@host/repo.git`, never named by any `*_env` key) can't be caught by AC1's config-driven mechanism alone, so `redact.ts` adds a second, unconditional URL-userinfo regex layered into the same `buildRedactor` closure. AC1's text describes "a single centralized `redact(text)` function" — satisfied at the API level (one function, one call site per consumer) but internally composed of two rules rather than one. Verified end-to-end against a real `git clone` failure against an unresolvable host. |
|
|
29
|
+
| AC3: CLI `handleError` redacts message in human and `--json` modes | ✅ Done (one finding fixed during this review — see Correctness) | Wired via a best-effort redactor built from `getEffectiveConfig()`, falling back to the URL-only layer if config itself fails to load, so a broken config never suppresses the original error report. |
|
|
30
|
+
| AC4: server-side `console.error` sites apply redaction | ✅ Done | 3 of 5 `console.error` call sites in `server.ts` interpolate exception/subprocess text (config reload error, runtime init error, audit prune error) and now route through `redactedErrorLog`. The other 2 sites are fixed diagnostic strings with no exception text, correctly left as-is. |
|
|
31
|
+
| AC5: redaction is a no-op with no `*_env` keys set | ✅ Done | Regression test asserts byte-identical output; passed immediately once AC1 landed (empty secrets list ⇒ identity), kept as a distinct behavioral lock rather than dropped as redundant. |
|
|
32
|
+
| AC6: `redact()` is a pure function, no network/subprocess needed to test | ✅ Done | `redact.ts` has zero I/O imports; test asserts determinism and non-Promise return. |
|
|
33
|
+
| AC7: successful `PATCH /admin/v1/config` appends an `admin_audit` row | ✅ Done | One row per PATCH call (not per changed key), recording ts, an array of `{key, old_value, new_value}`, and `resulting_revision`. Verified via a real HTTP round-trip against a running server, cross-checked against the actual pre/post config values. |
|
|
34
|
+
| AC8: `admin_audit` rows are hash-chained (tamper-evident) | ✅ Done | `row_hash = sha256(JSON({prev_row_hash, ts, changes, resulting_revision}))`, chained per row. No transaction wraps the read-prev/insert pair, but since `insertAdminAuditRow` contains no internal `await`, Bun's single-threaded event loop runs it to completion atomically before any other request handler resumes — checked specifically because two concurrent PATCH requests do have `await` points earlier in the handler (`setDottedKey` is async), but by the time `insertAdminAuditRow` itself runs, no interleaving is possible. |
|
|
35
|
+
| AC9: rejected `PATCH /admin/v1/config` writes no audit row | ✅ Done | Both rejection paths (stale `If-Match` → 409, `SKILLMUX_CONFIG_READONLY` → 409) return before the audit-insert code is reached; verified by asserting zero rows in each case. |
|
|
36
|
+
| AC10: `admin_audit` rows pruned by existing `audit.retention_days` | ✅ Done | `pruneAuditBefore`/`countPrunable` extended to include `admin_audit`; the one pre-existing test asserting the exact `PruneResult` shape was updated for the new field (only such test in the suite). |
|
|
37
|
+
| AC11: `verifyAdminAuditChain()` detects a broken hash chain | ✅ Done | Walks rows in `id` order recomputing each hash; a direct `UPDATE` on a row's `resulting_revision` (simulating out-of-band tampering) is detected and reported with the tampered row's id. |
|
|
38
|
+
|
|
39
|
+
Schema: no `schema.json` exists for this feature — config/data-shape-only
|
|
40
|
+
change with no new external API surface (the `PATCH /admin/v1/config`
|
|
41
|
+
request/response shape is unchanged; only server-side logging behavior is
|
|
42
|
+
new), consistent with slices 1 and 2 of this phase.
|
|
43
|
+
|
|
44
|
+
## AC Status
|
|
45
|
+
|
|
46
|
+
| Criterion | Status | Notes |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| AC1: centralized redact(text) resolves *_env secret values | ✅ Done | `buildRedactor(config)` recursively resolves every `*_env`-suffixed key against `process.env`. |
|
|
49
|
+
| AC2: credential-bearing git URL redacted from clone/ls-remote failures | ✅ Done | Second, unconditional URL-userinfo regex layered into the same `buildRedactor` closure — see Security section for the scope note. |
|
|
50
|
+
| AC3: CLI handleError redacts message in human and --json modes | ✅ Done | One finding fixed during review: `--verbose` stack trace was leaking the raw message — see Security section. |
|
|
51
|
+
| AC4: server-side console.error sites apply redaction | ✅ Done | 3 of 5 exception-bearing `console.error` sites in `server.ts` route through `redactedErrorLog`; the other 2 are fixed strings with no exception text. |
|
|
52
|
+
| AC5: redaction is a no-op with no *_env keys set | ✅ Done | Regression test asserts byte-identical output. |
|
|
53
|
+
| AC6: redact() is a pure function, no network/subprocess needed to test | ✅ Done | Zero I/O imports in `redact.ts`; determinism asserted directly. |
|
|
54
|
+
| AC7: successful PATCH /admin/v1/config appends an admin_audit row | ✅ Done | One row per PATCH call, verified via a real HTTP round-trip. |
|
|
55
|
+
| AC8: admin_audit rows are hash-chained (tamper-evident) | ✅ Done | `row_hash = sha256(JSON({prev_row_hash, ts, changes, resulting_revision}))`, chained per row; atomicity argument in Security section. |
|
|
56
|
+
| AC9: rejected PATCH /admin/v1/config writes no audit row | ✅ Done | Both rejection paths (stale If-Match, read-only config) verified to write zero rows. |
|
|
57
|
+
| AC10: admin_audit rows pruned by existing audit.retention_days | ✅ Done | `pruneAuditBefore`/`countPrunable` extended to include `admin_audit`. |
|
|
58
|
+
| AC11: verifyAdminAuditChain() detects a broken hash chain | ✅ Done | Direct out-of-band `UPDATE` on a row is detected and reported with the tampered row's id. |
|
|
59
|
+
|
|
60
|
+
## Security
|
|
61
|
+
|
|
62
|
+
- **Finding (fixed during this review): `--verbose` stack trace leaked the raw credential.** `handleError` redacted the primary error line via `redact(rawMsg)` but printed `err.stack` verbatim when `--verbose` was set — and `Error.stack`'s first line reproduces the *unredacted* `err.message`, so `skillmux install <credential-url> --verbose` reopened exactly the leak AC2/AC3 were meant to close. Confirmed via a real reproduction (`git clone failed for https://user:supersecret123@...` appearing in full in stderr), fixed by wrapping the stack print in the same `redact()` call, and locked in with a new regression test. This also required a one-line, directly-related fix to `install`'s arg parser, which rejected `--verbose` as an unknown option (it already special-cased `--json` the same way) — without that, the leak path was unreachable through the CLI to write a real test against.
|
|
63
|
+
- The URL-userinfo regex (`/:\/\/[^/\s@]*@/g`) is scoped to `scheme://user:pass@` — it does not touch bare `user@host` strings lacking a scheme, so it won't over-redact plain email addresses or similar text appearing elsewhere in error messages.
|
|
64
|
+
- `collectSecretValues` guards against empty-string secret values (`if (resolved) secrets.push(resolved)`), which matters: an unguarded `"".split("").join(PLACEHOLDER)` would have inserted the placeholder between every character of any string passed to `redact`.
|
|
65
|
+
- `handleError`'s config load is wrapped in try/catch with a fallback to the URL-only redaction layer — a broken config file can never suppress or crash the original error report, only reduce redaction to its unconditional layer.
|
|
66
|
+
- The admin audit trail records `old_value`/`new_value` per changed key, not the raw request/response bodies — it doesn't itself need redaction, since `PATCH /admin/v1/config` never accepts or returns `*_env`-masked keys (`setDottedKey` already throws on `isEnvMasked(key)`, pre-existing behavior unrelated to this diff).
|
|
67
|
+
|
|
68
|
+
## Correctness
|
|
69
|
+
|
|
70
|
+
- The one substantive finding (verbose stack-trace leak) is described above and fixed.
|
|
71
|
+
- `redactedErrorLog`'s tuple-spread wiring (`console.error(...redactedErrorLog(...))`) was verified against all three `server.ts` call sites in the diff, not just tested as an isolated helper.
|
|
72
|
+
- `verifyAdminAuditChain` on an empty table returns `{valid: true, broken_at_id: null}` — a sensible vacuous-truth default, not exercised by an explicit test but low-risk (single `if` branch, empty loop).
|
|
73
|
+
- No coverage for a `PATCH` with an empty `changes: {}` object — current behavior is to write no audit row (the `auditChanges.length > 0` guard), which is arguably correct (no real mutation occurred) but untested. Low-severity gap, not blocking.
|
|
74
|
+
|
|
75
|
+
## Performance
|
|
76
|
+
|
|
77
|
+
No concerns. `collectSecretValues`'s recursive config walk runs once per `buildRedactor()` call (once per CLI error, once at server startup) against a small config object — not a hot path. The `admin_audit` INSERT/prune additions are O(1)/indexed-scan SQLite operations consistent with the existing `audit`/`fetch` table patterns.
|
|
78
|
+
|
|
79
|
+
## Test Coverage (static)
|
|
80
|
+
|
|
81
|
+
Adequate. Every AC has at least one test; AC5/AC6/AC8/AC11 tests were written
|
|
82
|
+
as regression locks even though they passed immediately against the
|
|
83
|
+
preceding cycle's implementation (documented inline in commit messages
|
|
84
|
+
rather than treated as TDD false starts, per this session's established
|
|
85
|
+
practice of keeping a distinct test per spec-named criterion). The one
|
|
86
|
+
gap (empty-changes PATCH) is noted above.
|
|
87
|
+
|
|
88
|
+
## Reflect
|
|
89
|
+
|
|
90
|
+
Retained to Hindsight (`infra` bank): the `--verbose` stack-trace redaction
|
|
91
|
+
gap as a pattern worth checking on any future centralized-redaction work
|
|
92
|
+
(a single "redact the message" choke point is not the only place an error's
|
|
93
|
+
raw content can surface — `Error.stack`, `util.inspect` on an Error object
|
|
94
|
+
passed directly to `console.error`, and similar implicit stringification
|
|
95
|
+
paths need the same treatment), and the now-five-times-recurring Bifrost
|
|
96
|
+
24h token-quota exhaustion pattern across every SDD stage in this phase.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!-- source: docs/sdd/security-hardening-phase/think.md -->
|
|
2
|
+
<!-- plane_project: SMX -->
|
|
3
|
+
<!-- plane_module: security-observability -->
|
|
4
|
+
|
|
5
|
+
# Spec: Security Observability
|
|
6
|
+
|
|
7
|
+
Slice 3 of 3 from the security-hardening-phase (see `docs/sdd/security-hardening-phase/think.md`).
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Close the two remaining unenforced items from the security-hardening-phase's observability area: centralized secret redaction so resolved credential values never reach stdout/stderr/`--json` output or logs unredacted, and a tamper-evident, append-only audit trail for admin config mutations.
|
|
12
|
+
|
|
13
|
+
## Acceptance Criteria
|
|
14
|
+
|
|
15
|
+
### Secret redaction
|
|
16
|
+
|
|
17
|
+
1. A single centralized `redact(text)` function (`src/redact.ts`) knows, from the currently effective config, every env var name referenced by a `*_env` key (`api_key_env`, `token_env`, `auth_token_env`) and scrubs any occurrence of that env var's current value from a string, replacing it with a fixed placeholder.
|
|
18
|
+
2. A git clone/ls-remote/rev-parse failure whose error text embeds a credential-bearing URL is redacted before reaching CLI output — the credential substring never appears in stderr or the `--json` error envelope.
|
|
19
|
+
3. The CLI's top-level error handler (`handleError`) redacts `err.message` before printing to stderr and before including it in the JSON error envelope, in both human and `--json` modes.
|
|
20
|
+
4. The small number of server-side `console.error` calls that include exception or subprocess text apply the same redaction before writing.
|
|
21
|
+
5. With no `*_env` config keys set, redaction is a no-op — output is byte-identical to current behavior.
|
|
22
|
+
6. Redaction is a pure string function, testable in isolation given a config object and env vars, with no network call or subprocess spawn required.
|
|
23
|
+
|
|
24
|
+
### Tamper-evident admin audit trail
|
|
25
|
+
|
|
26
|
+
7. Every successful `PATCH /admin/v1/config` mutation appends one row to a new `admin_audit` table in the existing `audit.sqlite3`, recording timestamp, changed keys, old/new value per key, and the resulting config revision hash.
|
|
27
|
+
8. Each `admin_audit` row is tamper-evident: it carries a hash chained to the previous row's hash, so any row deleted or edited out-of-band breaks the chain.
|
|
28
|
+
9. A rejected `PATCH /admin/v1/config` (409 conflict, read-only, validation error) writes no audit row.
|
|
29
|
+
10. `admin_audit` rows are pruned by the existing `audit.retention_days` mechanism (SMX-69–71) — no separate retention config.
|
|
30
|
+
11. A `verifyAdminAuditChain()` utility walks the table and reports whether the hash chain is unbroken.
|
|
31
|
+
|
|
32
|
+
## Scope
|
|
33
|
+
|
|
34
|
+
- `src/redact.ts` (new): `buildRedactor(config)` → `(text: string) => string`.
|
|
35
|
+
- Wiring into `cli.ts`'s `handleError` and the ~6 server-side `console.error` sites that include exception/subprocess text.
|
|
36
|
+
- New `admin_audit` table + hash-chain writer in `src/db.ts`/`src/server.ts`, wired into the `PATCH /admin/v1/config` success path.
|
|
37
|
+
- A chain-verification utility (`verifyAdminAuditChain()`).
|
|
38
|
+
|
|
39
|
+
## Out of Scope
|
|
40
|
+
|
|
41
|
+
- Any new HTTP/CLI surface for querying admin audit history — overlaps with the phase-wide-deferred "security posture visibility" item.
|
|
42
|
+
- Redacting arbitrary user/skill content — only `*_env`-derived credential values are in scope.
|
|
43
|
+
- Per-admin actor identity — none exists (single shared admin bearer token via `SKILLMUX_ADMIN_TOKEN`/`admin.token_env`).
|
|
44
|
+
- Signer verification, SBOM/provenance, privacy purge (deferred phase-wide per `think.md`).
|
|
45
|
+
|
|
46
|
+
## Tasks
|
|
47
|
+
|
|
48
|
+
1. `src/redact.ts`: `buildRedactor(config)` resolving `*_env` config keys to current env values, returning a `redact(text)` closure.
|
|
49
|
+
2. Wire redactor into `cli.ts`'s `handleError` (both human and `--json` paths).
|
|
50
|
+
3. Wire redactor into the server-side `console.error` call sites that include exception/subprocess text (`src/server.ts`).
|
|
51
|
+
4. `admin_audit` table migration in `src/db.ts` (reusing the existing `audit.sqlite3` WAL + incremental auto-vacuum setup).
|
|
52
|
+
5. Hash-chain row builder + insert wired into the `PATCH /admin/v1/config` success path in `src/server.ts`.
|
|
53
|
+
6. `verifyAdminAuditChain()` utility + unit tests.
|
|
54
|
+
7. Docs: extend `docs/configuration.md` (or admin-API docs if present) noting the audit table and redaction behavior.
|
|
55
|
+
8. Tests: one per acceptance criterion above.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Review: Skill provenance + update
|
|
2
|
+
|
|
3
|
+
Reviewed diff: `c55e5ec..HEAD` on `main` (no feature branch was cut for this
|
|
4
|
+
work — all 9 commits landed directly on `main`, one per TDD task plus this
|
|
5
|
+
review's fixes).
|
|
6
|
+
|
|
7
|
+
**Method:** the `sdd-review` goose fan-out was attempted twice. Both runs
|
|
8
|
+
failed — the first with a peg-native format error from the model, the
|
|
9
|
+
second with Bifrost's daily token rate limit exceeded
|
|
10
|
+
(`1723501/1700000, resets every 24h`). Per this skill's "If goose errors
|
|
11
|
+
out" fallback, the five focus passes (security, correctness, performance,
|
|
12
|
+
tests, compliance) were run manually against the diff, plus `tsc --noEmit`
|
|
13
|
+
(build check) and `bun test` (test execution), both run directly rather than
|
|
14
|
+
through `test-runner`/`build-check` workers.
|
|
15
|
+
|
|
16
|
+
## Findings
|
|
17
|
+
|
|
18
|
+
### Correctness — FIXED
|
|
19
|
+
**A corrupt `.skillmux-origin` sidecar crashed the whole vault sweep
|
|
20
|
+
instead of being isolated per-skill.** `checkOutdated`'s loop called
|
|
21
|
+
`readSkillOrigin` unguarded; a malformed JSON file (or one failing
|
|
22
|
+
`validateOrigin`) threw past the loop and aborted `outdated`/`update`
|
|
23
|
+
entirely — for every other skill in the vault, not just the broken one.
|
|
24
|
+
AC3 promises no-sidecar skills are "not flagged as errors" and AC4 promises
|
|
25
|
+
a per-skill failure "does not abort the whole command," but both were
|
|
26
|
+
written with only *remote* failures in mind; a local parse failure hit the
|
|
27
|
+
same underlying risk (one skill's problem taking down the whole batch) and
|
|
28
|
+
wasn't covered. Reproduced directly (`bun run src/cli.ts outdated --json`
|
|
29
|
+
against a vault with one skill carrying `{not valid json` as its sidecar →
|
|
30
|
+
exit 2, raw `JSON Parse error`), then fixed: `checkOutdated` now catches
|
|
31
|
+
the exception and reports that skill as `check_failed` with a reason, same
|
|
32
|
+
as an unreachable remote. `update`'s single-skill path (which has no other
|
|
33
|
+
skills to protect) still surfaces the error but with a clearer message.
|
|
34
|
+
Regression test added: `tests/cli.test.ts` — "a malformed .skillmux-origin
|
|
35
|
+
sidecar is reported as check_failed, not a whole-command crash".
|
|
36
|
+
|
|
37
|
+
### Tests — FIXED
|
|
38
|
+
`update`'s `--fail-on` validation and `<skill-id>` argument-count limit had
|
|
39
|
+
no CLI-level test, unlike the equivalent checks on `install`/`audit prune`.
|
|
40
|
+
Added two tests for parity.
|
|
41
|
+
|
|
42
|
+
### Security — ASK, later FIXED (commit b2430a8, post-merge)
|
|
43
|
+
`update`'s confirmation prompt lists only skill IDs
|
|
44
|
+
(`update ${toWrite.map(i => i.skillId).join(", ")}?`), never the
|
|
45
|
+
`source_url` each one will be cloned from. Since `source_url` is read back
|
|
46
|
+
from a file written at `install` time — not re-typed by the user at
|
|
47
|
+
`update` time — a user who once installed from an attacker-controlled repo
|
|
48
|
+
and later runs `skillmux update --yes` (batch, non-interactive, e.g. in a
|
|
49
|
+
cron job or CI) re-triggers `git clone`/`git ls-remote` against that same
|
|
50
|
+
URL with no per-run visibility into which URLs are being fetched. This
|
|
51
|
+
isn't a new vulnerability class — `install` already accepts arbitrary git
|
|
52
|
+
URLs from the user directly — but `update` is designed to be re-run
|
|
53
|
+
unattended and batches multiple skills' fetches behind one `--yes`, which
|
|
54
|
+
meaningfully raises the value of showing what's about to be fetched.
|
|
55
|
+
**Proposed fix:** include `source_url` alongside `skill_id` in the
|
|
56
|
+
confirmation prompt and in `--dry-run` output. Left as a decision for the
|
|
57
|
+
human at review time rather than auto-fixed, since it was a UX/trust-surface
|
|
58
|
+
design choice rather than a bug. User approved applying it as the first
|
|
59
|
+
slice of the security-improvement phase; applied in commit `b2430a8` —
|
|
60
|
+
`buildConfirmPrompt` now renders `<skill_id> <- <source_url>` per line, and
|
|
61
|
+
`source_url` was added to the `skills[]` JSON/dry-run output. Covered by a
|
|
62
|
+
new unit test on `buildConfirmPrompt` and an updated AC9 dry-run assertion
|
|
63
|
+
in `tests/cli.test.ts`.
|
|
64
|
+
|
|
65
|
+
**Security escalation — RUN (commit fc3e083, 6506663, post-merge).** goose's
|
|
66
|
+
recipe-backed `/security-review` failed twice on Bifrost's daily token rate
|
|
67
|
+
limit (same limit hit during the original review); ran the audit manually
|
|
68
|
+
against `install.ts`, `provenance.ts`, `commands/update.ts`, and
|
|
69
|
+
`commands/outdated.ts` per the skill's fallback. Findings:
|
|
70
|
+
|
|
71
|
+
- **HIGH, fixed (fc3e083):** `readSkillOrigin`/`validateOrigin` accepted any
|
|
72
|
+
non-empty `source_url`. `update`/`outdated` read `source_url` straight from
|
|
73
|
+
the on-disk sidecar and pass it directly into `git clone`/`git ls-remote`,
|
|
74
|
+
bypassing the `isGitUrl` protocol allowlist that gates the `install` CLI
|
|
75
|
+
entry point. A tampered sidecar (e.g. a malicious edit to a shared,
|
|
76
|
+
git-backed vault checkout) could route those subprocess calls through
|
|
77
|
+
git's `ext::` transport or argument injection. Fixed by exporting
|
|
78
|
+
`isGitUrl` from `install.ts` and enforcing it inside `validateOrigin`, so
|
|
79
|
+
a non-conforming `source_url` fails closed — isolated per-skill by the
|
|
80
|
+
existing `checkOutdated`/`resolveCandidateOrigins` error handling.
|
|
81
|
+
- **MEDIUM, informational only (no code change):** `confirmIfNeeded`
|
|
82
|
+
(`shared.ts:40`) returns `true` immediately when `--yes` is passed, before
|
|
83
|
+
`buildConfirmPrompt` (the b2430a8 fix) is ever built or shown. That fix
|
|
84
|
+
only helps the interactive path — it does not cover the review's original
|
|
85
|
+
highest-risk scenario, unattended `skillmux update --yes` in cron/CI. The
|
|
86
|
+
HIGH fix above is the actual mitigation for that path.
|
|
87
|
+
- **LOW, fixed (6506663):** `resolveSkillDir` joined an origin-supplied
|
|
88
|
+
`skill_path` into `cloneDir` with no traversal check; a tampered sidecar's
|
|
89
|
+
`skill_path` could point content-fetch outside the intended clone dir.
|
|
90
|
+
Fixed by rejecting `..` segments and absolute paths.
|
|
91
|
+
|
|
92
|
+
Both fixes are TDD cycles with regression tests
|
|
93
|
+
(`tests/provenance.test.ts`, `tests/install.test.ts`); full suite green
|
|
94
|
+
(700/0) after both.
|
|
95
|
+
|
|
96
|
+
### Performance — NITPICK (not fixed)
|
|
97
|
+
`buildPlan` clones every candidate skill's source *before* checking local
|
|
98
|
+
drift, even though the drift check (`hashSkillContent(skillDir) !==
|
|
99
|
+
origin.content_hash`) is purely local and needs no network access. A
|
|
100
|
+
drifted skill (which will be skipped) still pays for a full `git clone`.
|
|
101
|
+
Reordering this safely would require making `cloneDir`/`fetchedDir`
|
|
102
|
+
nullable on `UpdatePlanItem` and guarding the cleanup `finally` loop
|
|
103
|
+
accordingly — a real but non-trivial type change, and the actual cost (one
|
|
104
|
+
extra shallow clone in the uncommon drift-skip path, for a manually-invoked
|
|
105
|
+
CLI command) is low. Left as-is; worth revisiting if drift turns out to be
|
|
106
|
+
common in practice.
|
|
107
|
+
|
|
108
|
+
### Compliance
|
|
109
|
+
All 13 acceptance criteria are met after the correctness fix above (AC3/AC4
|
|
110
|
+
were the ones exposed by the crash bug). See AC Status below.
|
|
111
|
+
|
|
112
|
+
### Build check
|
|
113
|
+
`bunx tsc --noEmit -p .` — PASS (clean, run directly, four times across the
|
|
114
|
+
session as code changed).
|
|
115
|
+
|
|
116
|
+
### Test execution
|
|
117
|
+
`bun test` — 697 pass, 0 fail, 1836 `expect()` calls across 55 files (run
|
|
118
|
+
directly; includes this review's two new regression tests).
|
|
119
|
+
|
|
120
|
+
## AC Status
|
|
121
|
+
|
|
122
|
+
| Criterion | Status | Notes |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| AC1: `install` writes a provenance sidecar (source, subpath, commit, timestamp, content hash); `--force` overwrites it | ✅ Done | Tested at unit (provenance.test.ts) and CLI level (cli.test.ts) |
|
|
125
|
+
| AC2: sidecar has its own filename, excluded from scan.ts, `listSupportingFiles`, and sync.ts's managed-entries bookkeeping | ✅ Done | Exclusion lives in `listSupportingFiles`; sync.ts needs no change since its bookkeeping operates one directory level up (target roots, not skill dirs) — confirmed via AC12 test |
|
|
126
|
+
| AC3: `skillmux outdated [--json]` lists sidecar-carrying skills as up-to-date/outdated via `git ls-remote`; no-sidecar skills omitted, not flagged | ✅ Done | Fixed during review: a corrupt sidecar is now `check_failed`, not a crash |
|
|
127
|
+
| AC4: a per-skill remote check failure doesn't abort the rest; drives `checks_failed` and exit code | ✅ Done | Extended during review to cover local parse failures, not just network ones |
|
|
128
|
+
| AC5: `update [skill-id]` supports single-skill and batch (every sidecar-carrying skill `outdated` would report as outdated) | ✅ Done | Tested both modes |
|
|
129
|
+
| AC6: re-fetches from the sidecar's recorded repo URL + subpath, no repo argument needed | ✅ Done | `resolveCandidateOrigins` reads `origin.source_url`/`skill_path` directly |
|
|
130
|
+
| AC7: refuses a locally-drifted skill unless `--force` | ✅ Done | Tested refusal and `--force` override |
|
|
131
|
+
| AC8: runs the install-time scan gate per skill before any write; a scan failure skips that skill without blocking others | ✅ Done | Tested with `--fail-on high` |
|
|
132
|
+
| AC9: `--dry-run` reports old/new SHA and whether content changed, for both single and batch | ✅ Done | Tested both modes |
|
|
133
|
+
| AC10: a real update requires `--yes` non-interactively/`--json`, or an interactive prompt; confirmation covers the post-filter write set; sidecar rewritten on success | ✅ Done | Confirmation set is exactly `plan.filter(kind === "update")`, matching AC10's "after drift and scan filtering" |
|
|
134
|
+
| AC11: `update <skill-id>` with no sidecar fails clearly; bare `update` with nothing outdated succeeds as a no-op | ✅ Done | Tested both |
|
|
135
|
+
| AC12: neither command touches `skillmux.toml`-pinned core/project skills or the `.skillmux`/`SkillmuxMarker` sync machinery | ✅ Done | Explicit regression test against a live sync target |
|
|
136
|
+
| AC13: `docs/cli.md` documents `outdated`/`update`; `docs/schema.json` documents the sidecar shape | ✅ Done (with a documented deviation) | Docs landed in `docs/skill-management.md` instead of `docs/cli.md`, matching where `install`/`scan` are actually documented (cli.md's real scope is global CLI mechanics, not the skill-lifecycle command family). `docs/schema.json` was deliberately left unchanged: its own scope is the MCP wire contract + config.toml — the prior fetch-outcome-flywheel feature set this precedent by never merging its own CLI-only output types (`PruneOutput`, `ReportOutput`, etc.) there either, and this feature adds no MCP tool or config.toml surface |
|
|
137
|
+
|
|
138
|
+
## Back-edge notes
|
|
139
|
+
|
|
140
|
+
No back-edge to `/sdd-spec` or `/sdd-schema` was required — the crash bug
|
|
141
|
+
was a robustness gap in an edge case the spec didn't anticipate (a
|
|
142
|
+
*corrupted local file*, distinct from AC3's "no sidecar" and AC4's
|
|
143
|
+
"unreachable remote"), not a case where the spec's stated criteria were
|
|
144
|
+
wrong or the implementation contradicted them. Fixed forward, consistent
|
|
145
|
+
with AC3/AC4's stated intent. Worth a light AC3/AC4 wording touch-up next
|
|
146
|
+
time this spec is revisited, to name "corrupt sidecar" explicitly alongside
|
|
147
|
+
"unreachable remote" — not urgent enough to reopen the spec now.
|
|
148
|
+
|
|
149
|
+
`docs/schema.json` scope (see AC13 above) is a considered interpretation
|
|
150
|
+
correction, not a back-edge: AC13's literal text assumed the `.skillmux`
|
|
151
|
+
marker and CLI output shapes were already documented there, which a check
|
|
152
|
+
during `/sdd-schema` showed was never true even for the precedent feature.
|