@sema-agent/settings-schema 1.0.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 +728 -0
- package/LICENSE +103 -0
- package/README.md +129 -0
- package/dist/api/auth-bridge.d.ts +331 -0
- package/dist/api/auth-bridge.js +210 -0
- package/dist/api/auth.d.ts +216 -0
- package/dist/api/auth.js +138 -0
- package/dist/api/scopes.d.ts +344 -0
- package/dist/api/scopes.js +222 -0
- package/dist/api/wire.d.ts +60 -0
- package/dist/api/wire.js +89 -0
- package/dist/bundle.d.ts +13 -0
- package/dist/bundle.js +67 -0
- package/dist/config-fns.d.ts +318 -0
- package/dist/config-fns.js +472 -0
- package/dist/cross-domain.d.ts +34 -0
- package/dist/cross-domain.js +118 -0
- package/dist/file-edit.d.ts +36 -0
- package/dist/file-edit.js +125 -0
- package/dist/file-store.d.ts +89 -0
- package/dist/file-store.js +238 -0
- package/dist/fleet.d.ts +498 -0
- package/dist/fleet.js +317 -0
- package/dist/hash.d.ts +32 -0
- package/dist/hash.js +59 -0
- package/dist/hooks.d.ts +5477 -0
- package/dist/hooks.js +627 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +26 -0
- package/dist/local-load.d.ts +42 -0
- package/dist/local-load.js +172 -0
- package/dist/migrate.d.ts +255 -0
- package/dist/migrate.js +542 -0
- package/dist/node.d.ts +11 -0
- package/dist/node.js +11 -0
- package/dist/reader.d.ts +18 -0
- package/dist/reader.js +1 -0
- package/dist/remote-exec.d.ts +274 -0
- package/dist/remote-exec.js +182 -0
- package/dist/resolve-roster.d.ts +28 -0
- package/dist/resolve-roster.js +108 -0
- package/dist/safety-merge-spec.d.ts +327 -0
- package/dist/safety-merge-spec.js +70 -0
- package/dist/scheduler-store-node.d.ts +72 -0
- package/dist/scheduler-store-node.js +119 -0
- package/dist/scheduler-store.d.ts +67 -0
- package/dist/scheduler-store.js +89 -0
- package/dist/secret-refs.d.ts +33 -0
- package/dist/secret-refs.js +48 -0
- package/dist/sha256.d.ts +16 -0
- package/dist/sha256.js +114 -0
- package/dist/skills-manifest.d.ts +12 -0
- package/dist/skills-manifest.js +54 -0
- package/dist/types.d.ts +13560 -0
- package/dist/types.js +2118 -0
- package/package.json +138 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sema-agent/settings-schema/scheduler-store — the PURE, dep-free CONTRACT for the self-wake scheduler store
|
|
3
|
+
* (R7 自唤醒). The shared seam between the WRITER (server-side `SchedulerCapability`, persists the model's
|
|
4
|
+
* CronCreate/Sleep intents) and the READER (TOC shell `SchedulerDaemon`, fires due intents). One canonical
|
|
5
|
+
* on-disk SHAPE + parse/serialize ⇒ the two independently-built sides never drift on the format.
|
|
6
|
+
*
|
|
7
|
+
* 🔴 Contract discipline (this is a third-party contract lib, not an impl dump):
|
|
8
|
+
* - ABSTRACT: this module is the type + the parse/serialize FORMAT + the `SchedulerStore` interface. It has
|
|
9
|
+
* ZERO runtime IO and ZERO deps (no `@sema-agent/*`, no `node:*`) so it lives in the browser-safe barrel.
|
|
10
|
+
* The concrete filesystem binding is `@sema-agent/settings-schema/node` (`loadSchedulerStore`/`saveSchedulerStore`).
|
|
11
|
+
* - EXTENSIBLE: `parseSchedulerStore` is FORWARD-COMPATIBLE — it validates only the required shape and
|
|
12
|
+
* PRESERVES unknown fields, so a newer writer can add fields without an older reader dropping them on
|
|
13
|
+
* round-trip. New optional `SchedulerRecord` fields are additive (no breaking change).
|
|
14
|
+
* - CONTRACTUAL: `when` is an inlined structural union kept byte-compatible with core `ScheduledIntent['when']`
|
|
15
|
+
* but NOT imported — the contract stands alone, independent of the engine.
|
|
16
|
+
*
|
|
17
|
+
* 🔴 `principal` is pinned by the writer from Runner-held ctx (never the model); persisted opaquely, passed
|
|
18
|
+
* through to `runTask` unchanged by the daemon (no privilege escalation).
|
|
19
|
+
*/
|
|
20
|
+
/** Structural validation of one record (required fields + a well-formed `when`). Unknown fields are allowed. */
|
|
21
|
+
export function isSchedulerRecord(v) {
|
|
22
|
+
if (typeof v !== 'object' || v === null)
|
|
23
|
+
return false;
|
|
24
|
+
const r = v;
|
|
25
|
+
if (typeof r.id !== 'string' || typeof r.scope !== 'string' || typeof r.prompt !== 'string' || typeof r.createdMs !== 'number')
|
|
26
|
+
return false;
|
|
27
|
+
const w = r.when;
|
|
28
|
+
if (typeof w !== 'object' || w === null)
|
|
29
|
+
return false;
|
|
30
|
+
if (w.kind === 'cron')
|
|
31
|
+
return typeof w.expr === 'string';
|
|
32
|
+
if (w.kind === 'at')
|
|
33
|
+
return typeof w.atMs === 'number';
|
|
34
|
+
if (w.kind === 'delay')
|
|
35
|
+
return typeof w.delaySec === 'number';
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse the durable store from its serialized form. FAIL-SOFT + FORWARD-COMPATIBLE: corrupt/non-array/non-JSON
|
|
40
|
+
* ⇒ `[]`; malformed individual records are dropped (the store self-heals on next save); well-formed records keep
|
|
41
|
+
* ALL their fields (incl. unknown future ones) so a newer writer's data survives an older reader's round-trip.
|
|
42
|
+
* `onWarning` (optional, additive — mirrors the `FileConfigStore`/`EffectiveReadWarning` precedent): called once
|
|
43
|
+
* per dropped record so a caller can surface the loss instead of it vanishing silently (C5/C6 — a dropped
|
|
44
|
+
* record must leave a trace reachable by whoever can act on it).
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* core 5.5.0 durable 键分叉的迁移半场(board [2439]/[2440]/[2444];server 裁 (a) 一次性迁移,拒读侧
|
|
48
|
+
* 双键容忍窗)。旧铸键链 `sessionId ?? principal ?? taskId ?? "default"` 让 durable 行的隔离键随会话
|
|
49
|
+
* 走——新会话新 sessionId ⇒ 旧 durable job 永远 not_found。core 5.5.0 起 durable 铸键=
|
|
50
|
+
* `principal ?? "default"`;存量旧键行由**本层**在 parse 时重铸(纯函数,幂等:重写后判据自不满足)。
|
|
51
|
+
*
|
|
52
|
+
* 为什么放 parse 而不是 loader:`loadSchedulerStore` 与 `mutateSchedulerStore` 都各自经
|
|
53
|
+
* `parseSchedulerStore`——归一放这里=两条读路径、两个读者(server 写侧 backend / shell daemon)
|
|
54
|
+
* 单点同愈,无第二机制。盘面在下一次 save 收敛(读侧恒一致,不做读时写放大)。
|
|
55
|
+
*
|
|
56
|
+
* 窄读判据:`lifetime` 不在本契约的显式键上(server 写侧作为 forward-compat 字段随行),按未知
|
|
57
|
+
* 字段窄读——`"session"` 行是会话链键,零触碰;其余(缺席=durable 默认)按 durable 归一。
|
|
58
|
+
*/
|
|
59
|
+
function normalizeDurableScope(r) {
|
|
60
|
+
if (r.lifetime === 'session')
|
|
61
|
+
return r;
|
|
62
|
+
const want = r.principal ?? 'default';
|
|
63
|
+
return r.scope === want ? r : { ...r, scope: want };
|
|
64
|
+
}
|
|
65
|
+
export function parseSchedulerStore(text, onWarning) {
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(parsed))
|
|
74
|
+
return [];
|
|
75
|
+
if (!onWarning)
|
|
76
|
+
return parsed.filter(isSchedulerRecord).map(normalizeDurableScope);
|
|
77
|
+
const out = [];
|
|
78
|
+
parsed.forEach((record, index) => {
|
|
79
|
+
if (isSchedulerRecord(record))
|
|
80
|
+
out.push(normalizeDurableScope(record));
|
|
81
|
+
else
|
|
82
|
+
onWarning({ index, kind: 'malformed-record', record });
|
|
83
|
+
});
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/** Serialize the store to its canonical on-disk form (stable, pretty JSON). The WRITE half of the format contract. */
|
|
87
|
+
export function serializeSchedulerStore(records) {
|
|
88
|
+
return JSON.stringify(records, null, 2);
|
|
89
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret-reference inventory (Phase-3 "密钥引用校验"). sema-registry stores only env-NAMEs, never secret
|
|
3
|
+
* values (the ENV_NAME boundary). This collects EVERY env-NAME the effective config references, grouped by
|
|
4
|
+
* name with where each is used — so an operator can audit "which env vars must each host/service provide?"
|
|
5
|
+
* before a worker/model fails at runtime.
|
|
6
|
+
*
|
|
7
|
+
* Note: sema-registry can't verify a name actually EXISTS in a consumer's environment (that's the consumer's
|
|
8
|
+
* own env). For workers, the reconciler already reports a missing one as a `SecretMissing` status condition
|
|
9
|
+
* (surfaced on /fleet). This inventory is the center-side half — every referenced env-NAME, incl. the general
|
|
10
|
+
* `secretEnv` passthrough, so a rotation runbook built on it isn't blind to app-specific tokens.
|
|
11
|
+
*/
|
|
12
|
+
import type { EffectiveConfig } from "./types.js";
|
|
13
|
+
export interface EnvRefUse {
|
|
14
|
+
domain: string;
|
|
15
|
+
detail: string;
|
|
16
|
+
}
|
|
17
|
+
export interface EnvRef {
|
|
18
|
+
envName: string;
|
|
19
|
+
refs: EnvRefUse[];
|
|
20
|
+
}
|
|
21
|
+
/** Every env-NAME referenced across models / workers / mcp, grouped + sorted by name.
|
|
22
|
+
* (integrations domain DELETED in 0.6.0 — OA/Gitea coordinates ride worker.env/secretEnv, EXPERT-REDESIGN §9.) */
|
|
23
|
+
export declare function collectEnvRefs(eff: EffectiveConfig): EnvRef[];
|
|
24
|
+
/**
|
|
25
|
+
* Names of workers that reference a given env-NAME — via any of the 4 structured `secretRefs` OR the general
|
|
26
|
+
* `secretEnv` passthrough. Drives batch-rotate (POST /secrets/<name>/rotate): after a credential write, recreate
|
|
27
|
+
* exactly the workers that project it. Reads the raw workers domain (the same shape collectEnvRefs would see).
|
|
28
|
+
*/
|
|
29
|
+
export declare function workersReferencingSecret(workers: {
|
|
30
|
+
name: string;
|
|
31
|
+
secretRefs?: Record<string, string | undefined>;
|
|
32
|
+
secretEnv?: Record<string, string>;
|
|
33
|
+
}[], envName: string): string[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Every env-NAME referenced across models / workers / mcp, grouped + sorted by name.
|
|
2
|
+
* (integrations domain DELETED in 0.6.0 — OA/Gitea coordinates ride worker.env/secretEnv, EXPERT-REDESIGN §9.) */
|
|
3
|
+
export function collectEnvRefs(eff) {
|
|
4
|
+
const map = new Map();
|
|
5
|
+
const add = (envName, domain, detail) => {
|
|
6
|
+
if (!envName)
|
|
7
|
+
return;
|
|
8
|
+
const list = map.get(envName) ?? [];
|
|
9
|
+
list.push({ domain, detail });
|
|
10
|
+
map.set(envName, list);
|
|
11
|
+
};
|
|
12
|
+
for (const m of eff.models.models)
|
|
13
|
+
add(m.apiKeyEnv, "models", `model "${m.name}".apiKeyEnv`);
|
|
14
|
+
for (const w of eff.workers.workers) {
|
|
15
|
+
const r = w.secretRefs;
|
|
16
|
+
add(r.apiKeyEnv, "workers", `worker "${w.name}".secretRefs.apiKeyEnv`);
|
|
17
|
+
add(r.serviceAuthTokenEnv, "workers", `worker "${w.name}".secretRefs.serviceAuthTokenEnv`);
|
|
18
|
+
add(r.gitTokenEnv, "workers", `worker "${w.name}".secretRefs.gitTokenEnv`);
|
|
19
|
+
add(r.tidbPasswordEnv, "workers", `worker "${w.name}".secretRefs.tidbPasswordEnv`);
|
|
20
|
+
// the general secretEnv passthrough also references env-NAMEs (worker-env.ts merges them) — include them
|
|
21
|
+
// so the rotation inventory isn't blind to app-specific tokens like OA_SERVICE_TOKEN→OA_CALLBACK_TOKEN.
|
|
22
|
+
for (const [key, envName] of Object.entries(w.secretEnv ?? {}))
|
|
23
|
+
add(envName, "workers", `worker "${w.name}".secretEnv.${key}`);
|
|
24
|
+
}
|
|
25
|
+
for (const s of eff.mcp.servers) {
|
|
26
|
+
const t = s.transport;
|
|
27
|
+
const refs = t.kind === "stdio" ? t.envRefs : t.headerRefs;
|
|
28
|
+
const kind = t.kind === "stdio" ? "envRefs" : "headerRefs";
|
|
29
|
+
for (const [key, envName] of Object.entries(refs ?? {}))
|
|
30
|
+
add(envName, "mcp", `server "${s.name}" ${kind}.${key}`);
|
|
31
|
+
}
|
|
32
|
+
return [...map.entries()].map(([envName, refs]) => ({ envName, refs })).sort((a, b) => a.envName.localeCompare(b.envName));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Names of workers that reference a given env-NAME — via any of the 4 structured `secretRefs` OR the general
|
|
36
|
+
* `secretEnv` passthrough. Drives batch-rotate (POST /secrets/<name>/rotate): after a credential write, recreate
|
|
37
|
+
* exactly the workers that project it. Reads the raw workers domain (the same shape collectEnvRefs would see).
|
|
38
|
+
*/
|
|
39
|
+
export function workersReferencingSecret(workers, envName) {
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const w of workers) {
|
|
42
|
+
const inStructured = Object.values(w.secretRefs ?? {}).includes(envName);
|
|
43
|
+
const inPassthrough = Object.values(w.secretEnv ?? {}).includes(envName);
|
|
44
|
+
if (inStructured || inPassthrough)
|
|
45
|
+
out.push(w.name);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
package/dist/sha256.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, dependency-free, **synchronous** SHA-256 over a UTF-8 string → lowercase hex.
|
|
3
|
+
*
|
|
4
|
+
* WHY a hand-rolled impl instead of `node:crypto`: this module sits in the PURE barrel
|
|
5
|
+
* (re-exported via skills-manifest.ts → resolve-roster.ts → index.ts). sema-registry's CLIENT components
|
|
6
|
+
* import that barrel for the zod types, so anything in it ships into the BROWSER bundle. A `node:crypto`
|
|
7
|
+
* import there breaks (or bloats with a polyfill) the client build — the exact blocker flagged in review.
|
|
8
|
+
* Web Crypto (`crypto.subtle.digest`) is async-only, but `skillContentHash` is a sync contract used in
|
|
9
|
+
* hot paths (the /effective manifest transform, the content-endpoint lookup), so we need a sync digest.
|
|
10
|
+
*
|
|
11
|
+
* Byte-for-byte identical to `createHash("sha256").update(s,"utf8").digest("hex")` — verified against
|
|
12
|
+
* node:crypto in the tests — so the `sha256:<hex>` content-address the service re-hashes after pulling
|
|
13
|
+
* (tamper detection) still matches. This is content-addressing/integrity, NOT a secret/MAC.
|
|
14
|
+
*/
|
|
15
|
+
/** SHA-256 of a UTF-8 string → 64-char lowercase hex. */
|
|
16
|
+
export declare function sha256Hex(input: string): string;
|
package/dist/sha256.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, dependency-free, **synchronous** SHA-256 over a UTF-8 string → lowercase hex.
|
|
3
|
+
*
|
|
4
|
+
* WHY a hand-rolled impl instead of `node:crypto`: this module sits in the PURE barrel
|
|
5
|
+
* (re-exported via skills-manifest.ts → resolve-roster.ts → index.ts). sema-registry's CLIENT components
|
|
6
|
+
* import that barrel for the zod types, so anything in it ships into the BROWSER bundle. A `node:crypto`
|
|
7
|
+
* import there breaks (or bloats with a polyfill) the client build — the exact blocker flagged in review.
|
|
8
|
+
* Web Crypto (`crypto.subtle.digest`) is async-only, but `skillContentHash` is a sync contract used in
|
|
9
|
+
* hot paths (the /effective manifest transform, the content-endpoint lookup), so we need a sync digest.
|
|
10
|
+
*
|
|
11
|
+
* Byte-for-byte identical to `createHash("sha256").update(s,"utf8").digest("hex")` — verified against
|
|
12
|
+
* node:crypto in the tests — so the `sha256:<hex>` content-address the service re-hashes after pulling
|
|
13
|
+
* (tamper detection) still matches. This is content-addressing/integrity, NOT a secret/MAC.
|
|
14
|
+
*/
|
|
15
|
+
const K = new Uint32Array([
|
|
16
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
17
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
18
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
19
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
20
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
21
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
22
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
23
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
24
|
+
]);
|
|
25
|
+
/** Encode a JS string to UTF-8 bytes (no Node Buffer / TextEncoder dependency required — but use
|
|
26
|
+
* TextEncoder when present for speed; it exists in both modern Node and browsers). */
|
|
27
|
+
function utf8Bytes(s) {
|
|
28
|
+
if (typeof TextEncoder !== "undefined")
|
|
29
|
+
return new TextEncoder().encode(s);
|
|
30
|
+
// Minimal fallback encoder (very old runtimes only).
|
|
31
|
+
const out = [];
|
|
32
|
+
for (let i = 0; i < s.length; i++) {
|
|
33
|
+
let c = s.charCodeAt(i);
|
|
34
|
+
if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length) {
|
|
35
|
+
const c2 = s.charCodeAt(i + 1);
|
|
36
|
+
if (c2 >= 0xdc00 && c2 <= 0xdfff) {
|
|
37
|
+
c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
|
|
38
|
+
i++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (c < 0x80)
|
|
42
|
+
out.push(c);
|
|
43
|
+
else if (c < 0x800)
|
|
44
|
+
out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
|
|
45
|
+
else if (c < 0x10000)
|
|
46
|
+
out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
47
|
+
else
|
|
48
|
+
out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
49
|
+
}
|
|
50
|
+
return Uint8Array.from(out);
|
|
51
|
+
}
|
|
52
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
53
|
+
/** SHA-256 of a UTF-8 string → 64-char lowercase hex. */
|
|
54
|
+
export function sha256Hex(input) {
|
|
55
|
+
const msg = utf8Bytes(input);
|
|
56
|
+
const bitLen = msg.length * 8;
|
|
57
|
+
// Pad: 0x80, then zeros, then 64-bit big-endian length, to a multiple of 64 bytes.
|
|
58
|
+
const withOne = msg.length + 1;
|
|
59
|
+
const total = withOne + ((56 - (withOne % 64) + 64) % 64) + 8;
|
|
60
|
+
const buf = new Uint8Array(total);
|
|
61
|
+
buf.set(msg);
|
|
62
|
+
buf[msg.length] = 0x80;
|
|
63
|
+
// 64-bit length, big-endian. bitLen fits in 53 bits safely for any realistic skill body.
|
|
64
|
+
const hi = Math.floor(bitLen / 0x100000000);
|
|
65
|
+
const lo = bitLen >>> 0;
|
|
66
|
+
buf[total - 8] = (hi >>> 24) & 0xff;
|
|
67
|
+
buf[total - 7] = (hi >>> 16) & 0xff;
|
|
68
|
+
buf[total - 6] = (hi >>> 8) & 0xff;
|
|
69
|
+
buf[total - 5] = hi & 0xff;
|
|
70
|
+
buf[total - 4] = (lo >>> 24) & 0xff;
|
|
71
|
+
buf[total - 3] = (lo >>> 16) & 0xff;
|
|
72
|
+
buf[total - 2] = (lo >>> 8) & 0xff;
|
|
73
|
+
buf[total - 1] = lo & 0xff;
|
|
74
|
+
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a;
|
|
75
|
+
let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
|
|
76
|
+
const w = new Uint32Array(64);
|
|
77
|
+
for (let off = 0; off < total; off += 64) {
|
|
78
|
+
for (let i = 0; i < 16; i++) {
|
|
79
|
+
w[i] = (buf[off + i * 4] << 24) | (buf[off + i * 4 + 1] << 16) | (buf[off + i * 4 + 2] << 8) | buf[off + i * 4 + 3];
|
|
80
|
+
}
|
|
81
|
+
for (let i = 16; i < 64; i++) {
|
|
82
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
83
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
84
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
|
|
85
|
+
}
|
|
86
|
+
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
|
|
87
|
+
for (let i = 0; i < 64; i++) {
|
|
88
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
89
|
+
const ch = (e & f) ^ (~e & g);
|
|
90
|
+
const t1 = (h + S1 + ch + K[i] + w[i]) | 0;
|
|
91
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
92
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
93
|
+
const t2 = (S0 + maj) | 0;
|
|
94
|
+
h = g;
|
|
95
|
+
g = f;
|
|
96
|
+
f = e;
|
|
97
|
+
e = (d + t1) | 0;
|
|
98
|
+
d = c;
|
|
99
|
+
c = b;
|
|
100
|
+
b = a;
|
|
101
|
+
a = (t1 + t2) | 0;
|
|
102
|
+
}
|
|
103
|
+
h0 = (h0 + a) | 0;
|
|
104
|
+
h1 = (h1 + b) | 0;
|
|
105
|
+
h2 = (h2 + c) | 0;
|
|
106
|
+
h3 = (h3 + d) | 0;
|
|
107
|
+
h4 = (h4 + e) | 0;
|
|
108
|
+
h5 = (h5 + f) | 0;
|
|
109
|
+
h6 = (h6 + g) | 0;
|
|
110
|
+
h7 = (h7 + h) | 0;
|
|
111
|
+
}
|
|
112
|
+
const toHex = (n) => (n >>> 0).toString(16).padStart(8, "0");
|
|
113
|
+
return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4) + toHex(h5) + toHex(h6) + toHex(h7);
|
|
114
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SkillsConfig, SkillsManifest } from "./types.js";
|
|
2
|
+
/** Content-address a skill body → algorithm-prefixed sha256 hex. Self-describing (future alg), immutable,
|
|
3
|
+
* and the SAME value carried in the manifest and used as the content-endpoint path. Memoized by content. */
|
|
4
|
+
export declare function skillContentHash(content: string): string;
|
|
5
|
+
/** Normalize a requested hash (bare 64-hex OR `sha256:<hex>`) to the canonical `sha256:<lowerhex>`, or null
|
|
6
|
+
* if malformed — so a junk/oversized path param is rejected before any scan. */
|
|
7
|
+
export declare function normalizeContentHash(raw: string): string | null;
|
|
8
|
+
/** /effective transform: strip each skill's `content`, replace with its `contentHash` (manifest shape). */
|
|
9
|
+
export declare function toSkillsManifest(cfg: SkillsConfig): SkillsManifest;
|
|
10
|
+
/** Content-addressed lookup: the body whose hash == `hash`, or undefined (unknown / malformed hash). Identical
|
|
11
|
+
* content across skills shares a hash → the first match's body is returned (bodies are identical by definition). */
|
|
12
|
+
export declare function findSkillContent(cfg: SkillsConfig, hash: string): string | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills content-addressing (docs/MCP-SKILLS.md §3.1, slice 2). The /effective payload ships skills as a
|
|
3
|
+
* MANIFEST — each skill's markdown `content` replaced by a `contentHash` — so the frequent 60s refresh stays
|
|
4
|
+
* small; the service pulls each body ONCE (at boot) via GET /api/config/skills/content/<hash>, caches it by
|
|
5
|
+
* hash, and re-pulls only when the hash changes (skills rarely change → big win). Bodies are immutable +
|
|
6
|
+
* deduped: identical content across skills shares one hash.
|
|
7
|
+
*
|
|
8
|
+
* sha256 here is for INTEGRITY + addressing (the service re-hashes after pulling to detect tampering/corruption)
|
|
9
|
+
* — distinct from the FNV `stableHash` in hash.ts, which is a fast non-crypto change-detector for ETags.
|
|
10
|
+
*
|
|
11
|
+
* The digest is a PURE-JS sha256 (./sha256.ts), NOT `node:crypto`: this module is re-exported through the
|
|
12
|
+
* package barrel, which sema-registry's CLIENT components import for the zod types — so a `node:crypto`
|
|
13
|
+
* import here would leak `node:*` into the browser bundle (the review-flagged blocker). Output is byte-identical to
|
|
14
|
+
* `createHash("sha256")`, so the `sha256:<hex>` content-address the service verifies still matches.
|
|
15
|
+
*/
|
|
16
|
+
import { sha256Hex } from "./sha256.js";
|
|
17
|
+
// Memoize content→hash: the /effective poll path (toSkillsManifest) and the content-endpoint lookup
|
|
18
|
+
// (findSkillContent) re-hash the same small, stable skill bodies repeatedly (× N workers × 60s). Skill
|
|
19
|
+
// bodies are few + rarely change, so this stays tiny; a cap guards against unbounded growth.
|
|
20
|
+
const _hashMemo = new Map();
|
|
21
|
+
const _HASH_MEMO_CAP = 512;
|
|
22
|
+
/** Content-address a skill body → algorithm-prefixed sha256 hex. Self-describing (future alg), immutable,
|
|
23
|
+
* and the SAME value carried in the manifest and used as the content-endpoint path. Memoized by content. */
|
|
24
|
+
export function skillContentHash(content) {
|
|
25
|
+
const hit = _hashMemo.get(content);
|
|
26
|
+
if (hit !== undefined)
|
|
27
|
+
return hit;
|
|
28
|
+
const h = `sha256:${sha256Hex(content)}`;
|
|
29
|
+
if (_hashMemo.size >= _HASH_MEMO_CAP)
|
|
30
|
+
_hashMemo.clear(); // bound — skills are few; simple evict-all is fine
|
|
31
|
+
_hashMemo.set(content, h);
|
|
32
|
+
return h;
|
|
33
|
+
}
|
|
34
|
+
/** Normalize a requested hash (bare 64-hex OR `sha256:<hex>`) to the canonical `sha256:<lowerhex>`, or null
|
|
35
|
+
* if malformed — so a junk/oversized path param is rejected before any scan. */
|
|
36
|
+
export function normalizeContentHash(raw) {
|
|
37
|
+
const m = /^(?:sha256:)?([0-9a-f]{64})$/i.exec(raw.trim());
|
|
38
|
+
return m ? `sha256:${m[1].toLowerCase()}` : null;
|
|
39
|
+
}
|
|
40
|
+
/** /effective transform: strip each skill's `content`, replace with its `contentHash` (manifest shape). */
|
|
41
|
+
export function toSkillsManifest(cfg) {
|
|
42
|
+
return { skills: cfg.skills.map(({ content, ...rest }) => ({ ...rest, contentHash: skillContentHash(content) })) };
|
|
43
|
+
}
|
|
44
|
+
/** Content-addressed lookup: the body whose hash == `hash`, or undefined (unknown / malformed hash). Identical
|
|
45
|
+
* content across skills shares a hash → the first match's body is returned (bodies are identical by definition). */
|
|
46
|
+
export function findSkillContent(cfg, hash) {
|
|
47
|
+
const want = normalizeContentHash(hash);
|
|
48
|
+
if (!want)
|
|
49
|
+
return undefined;
|
|
50
|
+
for (const s of cfg.skills)
|
|
51
|
+
if (skillContentHash(s.content) === want)
|
|
52
|
+
return s.content;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|