@rulvar/core 1.36.0 → 1.37.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/dist/index.d.ts +10 -3
- package/dist/index.js +116 -13
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -6469,9 +6469,16 @@ declare class JsonlFileStore implements MetaLookupStore {
|
|
|
6469
6469
|
/**
|
|
6470
6470
|
* File-backed TranscriptStore (M6-T02): blobs (transcripts, checkpoints,
|
|
6471
6471
|
* persisted CompiledWorkflow sources) as one file per ref under `dir`,
|
|
6472
|
-
* so compiled runs resume across processes. Refs follow
|
|
6473
|
-
*
|
|
6474
|
-
*
|
|
6472
|
+
* so compiled runs resume across processes. Refs follow the
|
|
6473
|
+
* `<runId>/<name>` convention; nested segments become directories.
|
|
6474
|
+
*
|
|
6475
|
+
* Every ref is contained under `dir` (v1.36.0 review SEC-P1): each
|
|
6476
|
+
* segment must match `[A-Za-z0-9._-]` and be neither empty, '.', nor
|
|
6477
|
+
* '..', and the resolved path must stay under the resolved root. A '..'
|
|
6478
|
+
* segment used to pass the per-segment alphabet (dots are in it) and, via
|
|
6479
|
+
* `join`, escape the root; a caller passing an untrusted ref (or an
|
|
6480
|
+
* untrusted runId, which prefixes checkpoint and workflow-source refs)
|
|
6481
|
+
* could read, write, or delete `.bin` files outside `dir`.
|
|
6475
6482
|
*/
|
|
6476
6483
|
declare class FileTranscriptStore implements TranscriptStore {
|
|
6477
6484
|
private readonly dir;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, getRandomValues, randomUUID } from "node:crypto";
|
|
2
2
|
import { appendFileSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
5
5
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
6
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
@@ -2302,6 +2302,79 @@ function applyClaimOps(claims, ops) {
|
|
|
2302
2302
|
}
|
|
2303
2303
|
return next;
|
|
2304
2304
|
}
|
|
2305
|
+
/** A lowercase sha256 digest: 64 hex characters. */
|
|
2306
|
+
const HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
2307
|
+
const CLAIM_STATUSES = /* @__PURE__ */ new Set([
|
|
2308
|
+
"active",
|
|
2309
|
+
"stale",
|
|
2310
|
+
"superseded",
|
|
2311
|
+
"archived"
|
|
2312
|
+
]);
|
|
2313
|
+
/**
|
|
2314
|
+
* Structural issues of one PERSISTED claim (empty = sound). Distinct from
|
|
2315
|
+
* the editorial commit validator (claims.ts): a persisted snapshot
|
|
2316
|
+
* legitimately holds non-active statuses (stale, superseded, archived) and
|
|
2317
|
+
* carries no gate, so only shape and vocabulary are checked here. This is
|
|
2318
|
+
* the boundary that keeps a null or partial claim from reaching the card
|
|
2319
|
+
* render, where `claim.status` would throw an untyped TypeError (v1.36.0
|
|
2320
|
+
* review P2-6).
|
|
2321
|
+
*/
|
|
2322
|
+
function persistedClaimIssues(value, path) {
|
|
2323
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return [`${path}: expected a claim object`];
|
|
2324
|
+
const claim = value;
|
|
2325
|
+
const issues = [];
|
|
2326
|
+
if (typeof claim.id !== "string" || claim.id.length === 0) issues.push(`${path}.id: expected a non-empty string`);
|
|
2327
|
+
const subject = claim.subject;
|
|
2328
|
+
if (subject === null || typeof subject !== "object") issues.push(`${path}.subject: expected an object`);
|
|
2329
|
+
else if (typeof subject.model !== "string" || !subject.model.includes(":")) issues.push(`${path}.subject.model: expected a 'provider:model' string`);
|
|
2330
|
+
if (typeof claim.taskClass !== "string" || claim.taskClass.length === 0) issues.push(`${path}.taskClass: expected a non-empty string`);
|
|
2331
|
+
if (claim.polarity !== "strength" && claim.polarity !== "weakness") issues.push(`${path}.polarity: expected 'strength' or 'weakness'`);
|
|
2332
|
+
if (typeof claim.statement !== "string" || claim.statement.length === 0) issues.push(`${path}.statement: expected a non-empty string`);
|
|
2333
|
+
if (claim.class !== "eval-measured" && claim.class !== "human-editorial") issues.push(`${path}.class: expected 'eval-measured' or 'human-editorial'`);
|
|
2334
|
+
if (typeof claim.status !== "string" || !CLAIM_STATUSES.has(claim.status)) issues.push(`${path}.status: expected active, stale, superseded, or archived`);
|
|
2335
|
+
if (!Array.isArray(claim.evidence) || claim.evidence.length === 0) issues.push(`${path}.evidence: expected a non-empty array`);
|
|
2336
|
+
if (claim.confidence !== "high" && claim.confidence !== "medium" && claim.confidence !== "low") issues.push(`${path}.confidence: expected 'high', 'medium', or 'low'`);
|
|
2337
|
+
if (typeof claim.observedAt !== "string" || Number.isNaN(Date.parse(claim.observedAt))) issues.push(`${path}.observedAt: expected an ISO date`);
|
|
2338
|
+
if (typeof claim.expiresAt !== "string" || Number.isNaN(Date.parse(claim.expiresAt))) issues.push(`${path}.expiresAt: expected an ISO date`);
|
|
2339
|
+
const author = claim.author;
|
|
2340
|
+
if (author === null || typeof author !== "object") issues.push(`${path}.author: expected an object`);
|
|
2341
|
+
else {
|
|
2342
|
+
if (author.kind !== "eval-pipeline" && author.kind !== "human") issues.push(`${path}.author.kind: expected 'eval-pipeline' or 'human'`);
|
|
2343
|
+
if (typeof author.id !== "string" || author.id.length === 0) issues.push(`${path}.author.id: expected a non-empty string`);
|
|
2344
|
+
}
|
|
2345
|
+
return issues;
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* The single read boundary of the store (v1.36.0 review P2-6). A persisted
|
|
2349
|
+
* snapshot must hold a nonnegative integer version, a lowercase sha256
|
|
2350
|
+
* hash, structurally sound claims, and a hash that MATCHES its claims: the
|
|
2351
|
+
* KnowledgeSnapshot contract promises the hash is the deterministic
|
|
2352
|
+
* content hash of the claims, so a file edited without rehashing (a forged
|
|
2353
|
+
* version or hash, a torn write) is refused with a typed ConfigError
|
|
2354
|
+
* instead of flowing on to forge the audit trail or crash the render.
|
|
2355
|
+
*/
|
|
2356
|
+
function validateKnowledgeSnapshot(parsed, path) {
|
|
2357
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new ConfigError(`knowledge store file does not hold a KnowledgeSnapshot: ${path}`);
|
|
2358
|
+
const snapshot = parsed;
|
|
2359
|
+
const issues = [];
|
|
2360
|
+
const version = snapshot.version;
|
|
2361
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version < 0) issues.push(`version: expected a nonnegative integer; got ${String(version)}`);
|
|
2362
|
+
const hash = snapshot.hash;
|
|
2363
|
+
if (typeof hash !== "string" || !HASH_PATTERN.test(hash)) issues.push("hash: expected a lowercase sha256 digest of 64 hex characters");
|
|
2364
|
+
if (!Array.isArray(snapshot.claims)) issues.push("claims: expected an array");
|
|
2365
|
+
else snapshot.claims.forEach((claim, index) => {
|
|
2366
|
+
issues.push(...persistedClaimIssues(claim, `claims[${String(index)}]`));
|
|
2367
|
+
});
|
|
2368
|
+
if (issues.length > 0) throw new ConfigError(`knowledge store file is not a valid KnowledgeSnapshot (${path}):\n- ${issues.join("\n- ")}`);
|
|
2369
|
+
const claims = snapshot.claims;
|
|
2370
|
+
const recomputed = knowledgeHash(claims);
|
|
2371
|
+
if (hash !== recomputed) throw new ConfigError(`knowledge store hash does not match its claims (${path}): stored ${String(hash)}, computed ${recomputed}; the file was edited without rehashing`);
|
|
2372
|
+
return {
|
|
2373
|
+
version,
|
|
2374
|
+
hash,
|
|
2375
|
+
claims
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2305
2378
|
var FileModelKnowledgeStore = class {
|
|
2306
2379
|
path;
|
|
2307
2380
|
activeClaimsCap;
|
|
@@ -2330,13 +2403,7 @@ var FileModelKnowledgeStore = class {
|
|
|
2330
2403
|
} catch (cause) {
|
|
2331
2404
|
throw new ConfigError(`knowledge store file is not valid JSON: ${this.path}`, { cause });
|
|
2332
2405
|
}
|
|
2333
|
-
|
|
2334
|
-
if (snapshot === null || typeof snapshot.version !== "number" || typeof snapshot.hash !== "string" || !Array.isArray(snapshot.claims)) throw new ConfigError(`knowledge store file does not hold a KnowledgeSnapshot: ${this.path}`);
|
|
2335
|
-
return {
|
|
2336
|
-
version: snapshot.version,
|
|
2337
|
-
hash: snapshot.hash,
|
|
2338
|
-
claims: snapshot.claims
|
|
2339
|
-
};
|
|
2406
|
+
return validateKnowledgeSnapshot(parsed, this.path);
|
|
2340
2407
|
}
|
|
2341
2408
|
async current() {
|
|
2342
2409
|
return this.read();
|
|
@@ -6578,9 +6645,16 @@ const TRANSCRIPT_SUFFIX = ".bin";
|
|
|
6578
6645
|
/**
|
|
6579
6646
|
* File-backed TranscriptStore (M6-T02): blobs (transcripts, checkpoints,
|
|
6580
6647
|
* persisted CompiledWorkflow sources) as one file per ref under `dir`,
|
|
6581
|
-
* so compiled runs resume across processes. Refs follow
|
|
6582
|
-
*
|
|
6583
|
-
*
|
|
6648
|
+
* so compiled runs resume across processes. Refs follow the
|
|
6649
|
+
* `<runId>/<name>` convention; nested segments become directories.
|
|
6650
|
+
*
|
|
6651
|
+
* Every ref is contained under `dir` (v1.36.0 review SEC-P1): each
|
|
6652
|
+
* segment must match `[A-Za-z0-9._-]` and be neither empty, '.', nor
|
|
6653
|
+
* '..', and the resolved path must stay under the resolved root. A '..'
|
|
6654
|
+
* segment used to pass the per-segment alphabet (dots are in it) and, via
|
|
6655
|
+
* `join`, escape the root; a caller passing an untrusted ref (or an
|
|
6656
|
+
* untrusted runId, which prefixes checkpoint and workflow-source refs)
|
|
6657
|
+
* could read, write, or delete `.bin` files outside `dir`.
|
|
6584
6658
|
*/
|
|
6585
6659
|
var FileTranscriptStore = class {
|
|
6586
6660
|
dir;
|
|
@@ -6590,9 +6664,13 @@ var FileTranscriptStore = class {
|
|
|
6590
6664
|
}
|
|
6591
6665
|
blobPath(ref) {
|
|
6592
6666
|
const segments = ref.split("/");
|
|
6593
|
-
for (const segment of segments) if (!/^[A-Za-z0-9._-]+$/.test(segment)) throw new JournalOrderViolation(`FileTranscriptStore: ref segment '${segment}' is not filesystem-safe`);
|
|
6667
|
+
for (const segment of segments) if (segment === "" || segment === "." || segment === ".." || !/^[A-Za-z0-9._-]+$/.test(segment)) throw new JournalOrderViolation(`FileTranscriptStore: ref segment '${segment}' is not filesystem-safe`);
|
|
6594
6668
|
const name = segments.pop() ?? "";
|
|
6595
|
-
|
|
6669
|
+
const path = join(this.dir, ...segments, `${name}${TRANSCRIPT_SUFFIX}`);
|
|
6670
|
+
const root = resolve(this.dir);
|
|
6671
|
+
const resolved = resolve(path);
|
|
6672
|
+
if (resolved !== root && !resolved.startsWith(`${root}${sep}`)) throw new JournalOrderViolation(`FileTranscriptStore: ref '${ref}' resolves outside the configured root`);
|
|
6673
|
+
return path;
|
|
6596
6674
|
}
|
|
6597
6675
|
async put(ref, blob) {
|
|
6598
6676
|
const path = this.blobPath(ref);
|
|
@@ -6610,6 +6688,7 @@ var FileTranscriptStore = class {
|
|
|
6610
6688
|
}
|
|
6611
6689
|
}
|
|
6612
6690
|
async list(runId) {
|
|
6691
|
+
if (runId === "." || runId === "..") throw new JournalOrderViolation(`FileTranscriptStore: runId '${runId}' is not filesystem-safe`);
|
|
6613
6692
|
const root = join(this.dir, safeName(runId));
|
|
6614
6693
|
const refs = [];
|
|
6615
6694
|
const walk = (dir, prefix) => {
|
|
@@ -13525,6 +13604,29 @@ var EventBus = class {
|
|
|
13525
13604
|
}
|
|
13526
13605
|
};
|
|
13527
13606
|
//#endregion
|
|
13607
|
+
//#region src/l0/run-id.ts
|
|
13608
|
+
/**
|
|
13609
|
+
* Run id containment (v1.36.0 review SEC-P1). A runId becomes both a
|
|
13610
|
+
* journal path component (JsonlFileStore.safeName) and the PREFIX of every
|
|
13611
|
+
* transcript ref (checkpointRefFor, workflowSourceRef append `/...`). The
|
|
13612
|
+
* journal's whole-token regex rejects a separator, but a bare '.' or '..'
|
|
13613
|
+
* slips through as a single component there and, once a '/suffix' is
|
|
13614
|
+
* appended, becomes a real traversal segment at the transcript store. The
|
|
13615
|
+
* engine validates the runId at its boundary, before the first transcript
|
|
13616
|
+
* write, so an untrusted runId is refused with a typed ConfigError instead
|
|
13617
|
+
* of escaping the configured transcript root.
|
|
13618
|
+
*/
|
|
13619
|
+
/** Filesystem-safe token: the journal store's own alphabet. */
|
|
13620
|
+
const SAFE_RUN_ID = /^[A-Za-z0-9._-]+$/;
|
|
13621
|
+
/**
|
|
13622
|
+
* Throws a ConfigError unless runId is a filesystem-safe token: a
|
|
13623
|
+
* non-empty string over [A-Za-z0-9._-] that is neither '.' nor '..'. The
|
|
13624
|
+
* dot pair passes the alphabet on its own, so it is refused explicitly.
|
|
13625
|
+
*/
|
|
13626
|
+
function assertSafeRunId(runId, context) {
|
|
13627
|
+
if (typeof runId !== "string" || runId === "" || runId === "." || runId === ".." || !SAFE_RUN_ID.test(runId)) throw new ConfigError(`${context}: runId ${JSON.stringify(runId)} is not filesystem-safe (allowed: [A-Za-z0-9._-], and neither "." nor "..")`);
|
|
13628
|
+
}
|
|
13629
|
+
//#endregion
|
|
13528
13630
|
//#region src/runner/inprocess.ts
|
|
13529
13631
|
/**
|
|
13530
13632
|
* ScriptRunner SPI and InProcessRunner (M1-T11).
|
|
@@ -13751,6 +13853,7 @@ function createEngine(options) {
|
|
|
13751
13853
|
const compiled = wf.kind === "compiled-workflow" ? wf : void 0;
|
|
13752
13854
|
if (compiled !== void 0 && options.runners?.sandbox === void 0) throw new ConfigError("running a CompiledWorkflow requires a sandbox runner: pass createEngine({ runners: { sandbox: new WorkerSandboxRunner() } }) from @rulvar/planner ");
|
|
13753
13855
|
const runId = resumeCtx?.runId ?? opts?.runId ?? mintRunId();
|
|
13856
|
+
assertSafeRunId(runId, "engine.run");
|
|
13754
13857
|
const registry = buildDeriverRegistry(options.extraDerivers);
|
|
13755
13858
|
const segmentsBefore = resumeCtx?.segmentsBefore ?? 0;
|
|
13756
13859
|
const telemetryBase = segmentsBefore * EVENT_SEGMENT_STRIDE;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|