@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +20 -10
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +15 -5
  6. package/docs/concepts.md +4 -3
  7. package/docs/configuration.md +22 -5
  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/skill-management.md +1 -1
  39. package/docs/troubleshooting.md +18 -2
  40. package/package.json +15 -8
  41. package/src/adapters.ts +0 -438
  42. package/src/audit.ts +0 -21
  43. package/src/cli.ts +0 -928
  44. package/src/clients.ts +0 -368
  45. package/src/commands/audit.ts +0 -85
  46. package/src/commands/config.ts +0 -224
  47. package/src/commands/context.ts +0 -104
  48. package/src/commands/core.ts +0 -56
  49. package/src/commands/doctor.ts +0 -97
  50. package/src/commands/eval.ts +0 -88
  51. package/src/commands/init.ts +0 -672
  52. package/src/commands/install.ts +0 -155
  53. package/src/commands/local-vault.ts +0 -60
  54. package/src/commands/models.ts +0 -10
  55. package/src/commands/outdated.ts +0 -115
  56. package/src/commands/project.ts +0 -580
  57. package/src/commands/report.ts +0 -66
  58. package/src/commands/scan.ts +0 -71
  59. package/src/commands/shared.ts +0 -38
  60. package/src/commands/skill.ts +0 -33
  61. package/src/commands/sync.ts +0 -233
  62. package/src/commands/target.ts +0 -225
  63. package/src/commands/update.ts +0 -277
  64. package/src/completions.ts +0 -196
  65. package/src/concurrency-limiter.ts +0 -61
  66. package/src/config-service.ts +0 -398
  67. package/src/config-watcher.ts +0 -253
  68. package/src/config.ts +0 -583
  69. package/src/context.ts +0 -188
  70. package/src/db-audit.ts +0 -286
  71. package/src/db-index.ts +0 -238
  72. package/src/db.ts +0 -3
  73. package/src/deployment.ts +0 -39
  74. package/src/doctor.ts +0 -205
  75. package/src/eval.ts +0 -302
  76. package/src/global-flags.ts +0 -46
  77. package/src/init-agents.ts +0 -329
  78. package/src/init-instructions.ts +0 -192
  79. package/src/init.ts +0 -389
  80. package/src/install.ts +0 -232
  81. package/src/lifecycle.ts +0 -51
  82. package/src/logger.ts +0 -26
  83. package/src/manifest.ts +0 -352
  84. package/src/mcp-registration.ts +0 -89
  85. package/src/metrics.ts +0 -121
  86. package/src/models.ts +0 -20
  87. package/src/output.ts +0 -214
  88. package/src/project-setup.ts +0 -36
  89. package/src/prompts.ts +0 -124
  90. package/src/provenance.ts +0 -99
  91. package/src/rate-limiter.ts +0 -137
  92. package/src/readiness.ts +0 -30
  93. package/src/redact.ts +0 -52
  94. package/src/router-core.ts +0 -666
  95. package/src/rrf.ts +0 -31
  96. package/src/scan.ts +0 -300
  97. package/src/server.ts +0 -930
  98. package/src/setup.ts +0 -145
  99. package/src/snapshot.ts +0 -135
  100. package/src/stats.ts +0 -340
  101. package/src/sync.ts +0 -482
  102. package/src/toml-writer.ts +0 -51
  103. package/src/types.ts +0 -212
  104. 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
- }