@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.
Files changed (103) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +18 -9
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +14 -4
  6. package/docs/concepts.md +2 -2
  7. package/docs/configuration.md +18 -3
  8. package/docs/deployment.md +3 -3
  9. package/docs/getting-started.md +32 -7
  10. package/docs/releasing.md +17 -2
  11. package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
  12. package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
  13. package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
  14. package/docs/sdd/cli-remote-target-parity/think.md +183 -0
  15. package/docs/sdd/cli-surface-consistency/think.md +149 -0
  16. package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
  17. package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
  18. package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
  19. package/docs/sdd/feat-packaging-deployment/review.md +95 -0
  20. package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
  21. package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
  22. package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
  23. package/docs/sdd/npm-native-binaries/review.md +103 -0
  24. package/docs/sdd/npm-native-binaries/spec.md +148 -0
  25. package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
  26. package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
  27. package/docs/sdd/runtime-resource-hardening/review.md +86 -0
  28. package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
  29. package/docs/sdd/security-hardening-phase/think.md +129 -0
  30. package/docs/sdd/security-observability/review.md +96 -0
  31. package/docs/sdd/security-observability/spec.md +55 -0
  32. package/docs/sdd/skill-provenance-update/review.md +152 -0
  33. package/docs/sdd/skill-provenance-update/schema.json +193 -0
  34. package/docs/sdd/skill-provenance-update/spec.md +134 -0
  35. package/docs/sdd/supply-chain-hardening/review.md +50 -0
  36. package/docs/sdd/supply-chain-hardening/spec.md +42 -0
  37. package/docs/sdd/target-marker-rehome/spec.md +33 -0
  38. package/docs/troubleshooting.md +18 -2
  39. package/package.json +15 -8
  40. package/src/adapters.ts +0 -438
  41. package/src/audit.ts +0 -21
  42. package/src/cli.ts +0 -928
  43. package/src/clients.ts +0 -368
  44. package/src/commands/audit.ts +0 -85
  45. package/src/commands/config.ts +0 -224
  46. package/src/commands/context.ts +0 -104
  47. package/src/commands/core.ts +0 -56
  48. package/src/commands/doctor.ts +0 -97
  49. package/src/commands/eval.ts +0 -88
  50. package/src/commands/init.ts +0 -672
  51. package/src/commands/install.ts +0 -155
  52. package/src/commands/local-vault.ts +0 -60
  53. package/src/commands/models.ts +0 -10
  54. package/src/commands/outdated.ts +0 -115
  55. package/src/commands/project.ts +0 -580
  56. package/src/commands/report.ts +0 -66
  57. package/src/commands/scan.ts +0 -71
  58. package/src/commands/shared.ts +0 -38
  59. package/src/commands/skill.ts +0 -33
  60. package/src/commands/sync.ts +0 -233
  61. package/src/commands/target.ts +0 -225
  62. package/src/commands/update.ts +0 -277
  63. package/src/completions.ts +0 -196
  64. package/src/concurrency-limiter.ts +0 -61
  65. package/src/config-service.ts +0 -398
  66. package/src/config-watcher.ts +0 -253
  67. package/src/config.ts +0 -583
  68. package/src/context.ts +0 -188
  69. package/src/db-audit.ts +0 -286
  70. package/src/db-index.ts +0 -238
  71. package/src/db.ts +0 -3
  72. package/src/deployment.ts +0 -39
  73. package/src/doctor.ts +0 -205
  74. package/src/eval.ts +0 -302
  75. package/src/global-flags.ts +0 -46
  76. package/src/init-agents.ts +0 -329
  77. package/src/init-instructions.ts +0 -192
  78. package/src/init.ts +0 -390
  79. package/src/install.ts +0 -232
  80. package/src/lifecycle.ts +0 -51
  81. package/src/logger.ts +0 -26
  82. package/src/manifest.ts +0 -359
  83. package/src/mcp-registration.ts +0 -89
  84. package/src/metrics.ts +0 -121
  85. package/src/models.ts +0 -20
  86. package/src/output.ts +0 -214
  87. package/src/project-setup.ts +0 -36
  88. package/src/prompts.ts +0 -124
  89. package/src/provenance.ts +0 -99
  90. package/src/rate-limiter.ts +0 -137
  91. package/src/readiness.ts +0 -30
  92. package/src/redact.ts +0 -52
  93. package/src/router-core.ts +0 -666
  94. package/src/rrf.ts +0 -31
  95. package/src/scan.ts +0 -300
  96. package/src/server.ts +0 -930
  97. package/src/setup.ts +0 -145
  98. package/src/snapshot.ts +0 -135
  99. package/src/stats.ts +0 -340
  100. package/src/sync.ts +0 -482
  101. package/src/toml-writer.ts +0 -51
  102. package/src/types.ts +0 -212
  103. package/src/vault.ts +0 -207
package/src/provenance.ts DELETED
@@ -1,99 +0,0 @@
1
- import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
2
- import { basename, dirname, join } from "node:path";
3
- import { isGitUrl } from "./install";
4
- import { SKILLMUX_ORIGIN_FILENAME, listSupportingFiles } from "./vault";
5
-
6
- export { SKILLMUX_ORIGIN_FILENAME };
7
-
8
- export interface SkillOrigin {
9
- schema_version: 1;
10
- source_url: string;
11
- skill_path?: string;
12
- commit: string;
13
- installed_at: string;
14
- content_hash: string;
15
- }
16
-
17
- function validateOrigin(origin: SkillOrigin, path: string): SkillOrigin {
18
- if (origin.schema_version !== 1) throw new Error(`${path}: unsupported .skillmux-origin schema_version`);
19
- if (typeof origin.source_url !== "string" || origin.source_url.length === 0) {
20
- throw new Error(`${path}: .skillmux-origin is missing source_url`);
21
- }
22
- if (!isGitUrl(origin.source_url)) {
23
- throw new Error(`${path}: .skillmux-origin has a source_url that is not a recognized git protocol`);
24
- }
25
- if (typeof origin.commit !== "string" || !/^[0-9a-f]{40}$/.test(origin.commit)) {
26
- throw new Error(`${path}: .skillmux-origin has an invalid commit`);
27
- }
28
- if (typeof origin.installed_at !== "string") throw new Error(`${path}: .skillmux-origin is missing installed_at`);
29
- if (typeof origin.content_hash !== "string" || !/^[a-f0-9]{64}$/.test(origin.content_hash)) {
30
- throw new Error(`${path}: .skillmux-origin has an invalid content_hash`);
31
- }
32
- return origin;
33
- }
34
-
35
- /** Same defense-in-depth as readSkill/hashSkillContent: a tampered vault entry
36
- * (shared git-backed vault pulled in, or a hand-edit) could symlink the sidecar
37
- * itself to an arbitrary host file — refuse to follow it rather than feeding
38
- * that file's bytes into JSON.parse and, on a schema-shaped coincidence, into
39
- * the git commands `outdated`/`update` run against `source_url`. */
40
- export function readSkillOrigin(dir: string): SkillOrigin | null {
41
- // The skill directory itself must be checked separately from the sidecar's own
42
- // leaf check below: `lstat` only refuses to follow the *final* path component,
43
- // so a symlinked skill directory containing a real, non-symlink sidecar at its
44
- // target would otherwise resolve straight through to arbitrary host content.
45
- if (existsSync(dir) && lstatSync(dir).isSymbolicLink()) {
46
- throw new Error(`${dir}: refusing to read .skillmux-origin — the skill directory is a symlink`);
47
- }
48
- const path = join(dir, SKILLMUX_ORIGIN_FILENAME);
49
- if (!existsSync(path)) return null;
50
- if (lstatSync(path).isSymbolicLink()) {
51
- throw new Error(`${path}: refusing to read .skillmux-origin — it is a symlink`);
52
- }
53
- return validateOrigin(JSON.parse(readFileSync(path, "utf-8")), path);
54
- }
55
-
56
- export function writeSkillOrigin(
57
- dir: string,
58
- params: {
59
- source_url: string;
60
- skill_path?: string;
61
- commit: string;
62
- installed_at: string;
63
- content_hash: string;
64
- },
65
- ): void {
66
- const origin: SkillOrigin = { schema_version: 1, ...params };
67
- writeFileSync(join(dir, SKILLMUX_ORIGIN_FILENAME), JSON.stringify(origin, null, 2));
68
- }
69
-
70
- /** Deterministic sha256 over every file in a skill directory (SKILL.md plus every
71
- * file listSupportingFiles returns), used for local-drift detection. listSupportingFiles
72
- * already excludes symlinks it finds, but SKILL.md is read separately (same reason
73
- * readSkill/deliverSkill guard it independently in vault.ts/router-core.ts) — this is
74
- * a local, already-installed vault dir, not the freshly cloned candidate that
75
- * validateSkillCandidate has vetted, so a swapped-in symlinked SKILL.md must be
76
- * refused here rather than followed. `dir` itself is checked too: every per-file
77
- * lstat below only refuses to follow a symlinked *leaf*, so a symlinked `dir`
78
- * containing real (non-symlink) files at its target would otherwise resolve
79
- * straight through to arbitrary host content. */
80
- export function hashSkillContent(dir: string): string {
81
- const vaultPath = dirname(dir);
82
- const skillId = basename(dir);
83
- if (lstatSync(dir).isSymbolicLink()) {
84
- throw new Error(`refusing to hash ${skillId}: the skill directory is a symlink`);
85
- }
86
- const hasher = new Bun.CryptoHasher("sha256");
87
- const files = ["SKILL.md", ...listSupportingFiles(vaultPath, skillId)];
88
- for (const rel of files) {
89
- const path = join(dir, rel);
90
- if (lstatSync(path).isSymbolicLink()) {
91
- throw new Error(`refusing to hash ${skillId}/${rel}: it is a symlink`);
92
- }
93
- hasher.update(rel);
94
- hasher.update("\0");
95
- hasher.update(readFileSync(path));
96
- hasher.update("\0");
97
- }
98
- return hasher.digest("hex");
99
- }
@@ -1,137 +0,0 @@
1
- interface Bucket {
2
- tokens: number;
3
- lastRefillMs: number;
4
- }
5
-
6
- // SMX-93: bounds memory even when an attacker (behind a trust_proxy-honored
7
- // reverse proxy) mints unbounded distinct X-Forwarded-For values, or a
8
- // long-running deployment simply accumulates many distinct legitimate
9
- // clients over time. Past this many entries, the least-recently-used
10
- // bucket is evicted to make room — active clients are never evicted ahead
11
- // of idle ones.
12
- const DEFAULT_MAX_BUCKETS = 10_000;
13
-
14
- export interface RateLimitCheckInput {
15
- nowMs: number;
16
- auth_enabled: boolean;
17
- req: Request;
18
- server: {
19
- requestIP(request: Request): { address: string } | null;
20
- };
21
- }
22
-
23
- export interface RateLimitCheckResult {
24
- allowed: boolean;
25
- retryAfterSeconds?: number;
26
- headers: Record<string, string>;
27
- }
28
-
29
- export class RateLimiter {
30
- private enabled: boolean;
31
- private requests_per_minute: number;
32
- private trust_proxy: boolean;
33
- private max_buckets: number;
34
- private buckets = new Map<string, Bucket>();
35
-
36
- constructor(config: {
37
- enabled: boolean;
38
- requests_per_minute: number;
39
- trust_proxy?: boolean;
40
- max_buckets?: number;
41
- }) {
42
- this.enabled = config.enabled;
43
- this.requests_per_minute = config.requests_per_minute;
44
- this.trust_proxy = config.trust_proxy ?? false;
45
- this.max_buckets = config.max_buckets ?? DEFAULT_MAX_BUCKETS;
46
- }
47
-
48
- check(input: RateLimitCheckInput): RateLimitCheckResult {
49
- if (!this.enabled) {
50
- return { allowed: true, headers: {} };
51
- }
52
-
53
- // 1. Resolve identifier
54
- let id = "127.0.0.1";
55
- if (input.auth_enabled) {
56
- const authHeader = input.req.headers.get("authorization") || "";
57
- const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
58
- if (token) {
59
- id = token;
60
- }
61
- } else {
62
- const ipAddr = input.server.requestIP(input.req)?.address;
63
- if (ipAddr) {
64
- id = ipAddr;
65
- } else if (this.trust_proxy) {
66
- // X-Forwarded-For is client-supplied and spoofable; only honor it
67
- // when trust_proxy opts in (i.e. a trusted reverse proxy sets it).
68
- const xff = input.req.headers.get("x-forwarded-for");
69
- if (xff) {
70
- id = xff.split(",", 1)[0]?.trim() || id;
71
- }
72
- }
73
- }
74
-
75
- // 2. Retrieve or initialize bucket
76
- let bucket = this.buckets.get(id);
77
- if (bucket) {
78
- // Map iteration order is insertion order, so re-inserting on touch
79
- // marks this entry as most-recently-used and moves it out of the
80
- // eviction path below.
81
- this.buckets.delete(id);
82
- this.buckets.set(id, bucket);
83
- } else {
84
- if (this.buckets.size >= this.max_buckets) {
85
- const oldestId = this.buckets.keys().next().value;
86
- if (oldestId !== undefined) {
87
- this.buckets.delete(oldestId);
88
- }
89
- }
90
- bucket = {
91
- tokens: this.requests_per_minute,
92
- lastRefillMs: input.nowMs,
93
- };
94
- this.buckets.set(id, bucket);
95
- }
96
-
97
- // 3. Refill tokens
98
- const elapsedMs = input.nowMs - bucket.lastRefillMs;
99
- if (elapsedMs > 0) {
100
- const refillRatePerMs = this.requests_per_minute / (60 * 1000);
101
- const refilled = elapsedMs * refillRatePerMs;
102
- bucket.tokens = Math.min(this.requests_per_minute, bucket.tokens + refilled);
103
- bucket.lastRefillMs = input.nowMs;
104
- }
105
-
106
- // 4. Determine allowed status
107
- let allowed = false;
108
- if (bucket.tokens >= 1) {
109
- bucket.tokens -= 1;
110
- allowed = true;
111
- }
112
-
113
- // Calculate headers
114
- const headers: Record<string, string> = {
115
- "X-RateLimit-Limit": this.requests_per_minute.toString(),
116
- "X-RateLimit-Remaining": Math.floor(bucket.tokens).toString(),
117
- };
118
-
119
- const refillRatePerSec = this.requests_per_minute / 60;
120
- const missingTokens = this.requests_per_minute - bucket.tokens;
121
- const resetTimeSeconds = Math.ceil(input.nowMs / 1000 + (missingTokens / refillRatePerSec));
122
- headers["X-RateLimit-Reset"] = resetTimeSeconds.toString();
123
-
124
- if (allowed) {
125
- return { allowed, headers };
126
- } else {
127
- const neededTokens = 1 - bucket.tokens;
128
- const retryAfterSeconds = Math.ceil(neededTokens / refillRatePerSec);
129
- headers["Retry-After"] = retryAfterSeconds.toString();
130
- return {
131
- allowed,
132
- retryAfterSeconds,
133
- headers,
134
- };
135
- }
136
- }
137
- }
package/src/readiness.ts DELETED
@@ -1,30 +0,0 @@
1
- import type { RetrievalCapability } from "./types";
2
-
3
- export interface ReadinessSnapshot {
4
- status: "starting" | "ready" | "not_ready" | "stopping";
5
- retrieval: RetrievalCapability | null;
6
- skills: number;
7
- index_current: boolean;
8
- embedding: "pending" | "ready" | "unavailable";
9
- reranker: "not_configured" | "pending" | "ready" | "unavailable";
10
- error?: string;
11
- }
12
-
13
- export class ReadinessState {
14
- private snapshot: ReadinessSnapshot = {
15
- status: "starting",
16
- retrieval: null,
17
- skills: 0,
18
- index_current: false,
19
- embedding: "pending",
20
- reranker: "not_configured",
21
- };
22
-
23
- get(): ReadinessSnapshot {
24
- return { ...this.snapshot };
25
- }
26
-
27
- set(snapshot: ReadinessSnapshot): void {
28
- this.snapshot = { ...snapshot };
29
- }
30
- }
package/src/redact.ts DELETED
@@ -1,52 +0,0 @@
1
- const PLACEHOLDER = "[REDACTED]";
2
-
3
- // Strips userinfo (user:pass@) from URLs unconditionally, since a
4
- // credential-bearing git URL (private-repo PAT auth) is typed by the user
5
- // directly and never resolved from a config `*_env` key, so it can't be
6
- // caught by the config-driven scrub below.
7
- const URL_USERINFO = /:\/\/[^/\s@]*@/g;
8
-
9
- function redactUrlCredentials(text: string): string {
10
- return text.replace(URL_USERINFO, `://${PLACEHOLDER}@`);
11
- }
12
-
13
- /**
14
- * Walks the config tree for every string-valued key ending in `_env`
15
- * (api_key_env, token_env, auth_token_env, and any future one) and resolves
16
- * each to its current environment value.
17
- */
18
- function collectSecretValues(value: unknown): string[] {
19
- const secrets: string[] = [];
20
- const visit = (node: unknown): void => {
21
- if (Array.isArray(node)) {
22
- for (const item of node) visit(item);
23
- return;
24
- }
25
- if (node === null || typeof node !== "object") return;
26
- for (const [key, val] of Object.entries(node as Record<string, unknown>)) {
27
- if (key.endsWith("_env") && typeof val === "string") {
28
- const resolved = process.env[val];
29
- if (resolved) secrets.push(resolved);
30
- } else {
31
- visit(val);
32
- }
33
- }
34
- };
35
- visit(value);
36
- return secrets;
37
- }
38
-
39
- /**
40
- * Builds a redactor bound to the currently effective config: a pure
41
- * `(text) => text` closure that scrubs any resolved `*_env` secret value.
42
- */
43
- export function buildRedactor(config: unknown): (text: string) => string {
44
- const secrets = collectSecretValues(config);
45
- return (text: string): string => {
46
- let result = redactUrlCredentials(text);
47
- for (const secret of secrets) {
48
- result = result.split(secret).join(PLACEHOLDER);
49
- }
50
- return result;
51
- };
52
- }