@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
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
|
-
}
|
package/src/rate-limiter.ts
DELETED
|
@@ -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
|
-
}
|