@openshain/core 0.1.0 → 0.2.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 (47) hide show
  1. package/dist/config/load.d.ts +10 -0
  2. package/dist/config/load.js +66 -0
  3. package/dist/config/schema.d.ts +84 -0
  4. package/dist/config/schema.js +96 -0
  5. package/dist/errors.d.ts +10 -0
  6. package/dist/errors.js +30 -0
  7. package/dist/ids.d.ts +11 -0
  8. package/dist/ids.js +21 -0
  9. package/dist/index.d.ts +21 -0
  10. package/dist/index.js +20 -0
  11. package/dist/model/types.d.ts +57 -0
  12. package/dist/model/types.js +0 -0
  13. package/dist/runtime.d.ts +46 -0
  14. package/dist/runtime.js +144 -0
  15. package/dist/schemas.d.ts +9 -0
  16. package/dist/schemas.js +44 -0
  17. package/dist/tool/load-module.d.ts +6 -0
  18. package/dist/tool/load-module.js +42 -0
  19. package/dist/tool/paths.d.ts +17 -0
  20. package/dist/tool/paths.js +82 -0
  21. package/dist/tool/registry.d.ts +30 -0
  22. package/dist/tool/registry.js +68 -0
  23. package/dist/tool/types.d.ts +44 -0
  24. package/dist/tool/types.js +15 -0
  25. package/dist/tool/validate.d.ts +14 -0
  26. package/dist/tool/validate.js +68 -0
  27. package/dist/uuid.d.ts +1 -0
  28. package/dist/uuid.js +33 -0
  29. package/dist/work/artifacts.d.ts +7 -0
  30. package/dist/work/artifacts.js +20 -0
  31. package/dist/work/event-log.d.ts +28 -0
  32. package/dist/work/event-log.js +140 -0
  33. package/dist/work/events.d.ts +311 -0
  34. package/dist/work/events.js +344 -0
  35. package/dist/work/lock.d.ts +13 -0
  36. package/dist/work/lock.js +80 -0
  37. package/dist/work/projection.d.ts +31 -0
  38. package/dist/work/projection.js +130 -0
  39. package/dist/work/store.d.ts +58 -0
  40. package/dist/work/store.js +174 -0
  41. package/dist/work/work.d.ts +86 -0
  42. package/dist/work/work.js +149 -0
  43. package/package.json +16 -5
  44. package/src/ids.ts +3 -2
  45. package/src/index.ts +1 -0
  46. package/src/uuid.ts +33 -0
  47. package/src/work/projection.ts +1 -1
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { ConfigFileSchema } from "./config/schema.js";
3
+ import { EventFileSchema, payloadFileSchemas } from "./work/events.js";
4
+ import { WorkFileSchema } from "./work/work.js";
5
+ /**
6
+ * The JSON Schemas (draft 2020-12) of the files openshain reads and writes, derived from the zod
7
+ * schemas that validate them. `spec/schemas/` holds this output; `bun run schemas` regenerates it.
8
+ * Conditions zod expresses as refinements, such as "provider or module, not both", have no JSON
9
+ * Schema form and are absent here.
10
+ */
11
+ export function jsonSchemas() {
12
+ return {
13
+ "config.v1": describe(ConfigFileSchema, "openshain.yaml", "The company workspace manifest as written on disk."),
14
+ "events.v1": eventsSchema(),
15
+ "work.v1": describe(WorkFileSchema, "work.json", "The state of a work as projected from its event log. Never the source of truth."),
16
+ };
17
+ }
18
+ /**
19
+ * One line of events.jsonl: the strict envelope with the payload of its type. A type this
20
+ * version does not know is accepted with any payload, as the runtime accepts it, so that a log
21
+ * written by a newer runtime still validates.
22
+ */
23
+ function eventsSchema() {
24
+ const known = Object.keys(payloadFileSchemas);
25
+ const options = Object.entries(payloadFileSchemas).map(([type, payload]) => EventFileSchema.extend({ type: z.literal(type), payload }));
26
+ const { $schema, oneOf } = toJsonSchema(z.discriminatedUnion("type", options));
27
+ const unknownType = toJsonSchema(EventFileSchema);
28
+ delete unknownType.$schema;
29
+ unknownType.properties.type = { type: "string", not: { enum: known } };
30
+ return {
31
+ $schema,
32
+ title: "events.jsonl line",
33
+ description: "One line of work/<id>/events.jsonl: the envelope, which is strict, and the payload of its type, which may carry fields this version does not know. An unknown type is accepted with any payload.",
34
+ oneOf: [...oneOf, unknownType],
35
+ };
36
+ }
37
+ function describe(schema, title, description) {
38
+ const { $schema, ...rest } = toJsonSchema(schema);
39
+ return { $schema, title, description, ...rest };
40
+ }
41
+ function toJsonSchema(schema) {
42
+ // The input shape: a field with a default is optional in the file, and the reader fills it in.
43
+ return z.toJSONSchema(schema, { target: "draft-2020-12", io: "input" });
44
+ }
@@ -0,0 +1,6 @@
1
+ import type { ToolProvider } from "./types.ts";
2
+ /**
3
+ * Loads a third-party tool provider from a module inside the workspace.
4
+ * The module's default export must be a ToolProvider.
5
+ */
6
+ export declare function loadToolModule(workspaceRoot: string, modulePath: string): Promise<ToolProvider>;
@@ -0,0 +1,42 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { pathToFileURL } from "node:url";
10
+ import { OpenshainError } from "../errors.js";
11
+ import { resolveWorkspacePath } from "./paths.js";
12
+ /**
13
+ * Loads a third-party tool provider from a module inside the workspace.
14
+ * The module's default export must be a ToolProvider.
15
+ */
16
+ export async function loadToolModule(workspaceRoot, modulePath) {
17
+ let file;
18
+ try {
19
+ file = await resolveWorkspacePath(workspaceRoot, modulePath);
20
+ }
21
+ catch (cause) {
22
+ throw new OpenshainError("config", `tool module "${modulePath}" must be inside the workspace: ${cause.message}`, { cause });
23
+ }
24
+ let loaded;
25
+ try {
26
+ loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(file).href));
27
+ }
28
+ catch (cause) {
29
+ throw new OpenshainError("config", `cannot load tool module "${modulePath}": ${cause.message}`, { cause });
30
+ }
31
+ const candidate = loaded.default;
32
+ if (!isToolProvider(candidate)) {
33
+ throw new OpenshainError("config", `tool module "${modulePath}" must default-export a ToolProvider with id, listTools and call`);
34
+ }
35
+ return candidate;
36
+ }
37
+ function isToolProvider(value) {
38
+ if (typeof value !== "object" || value === null)
39
+ return false;
40
+ const v = value;
41
+ return (typeof v.id === "string" && typeof v.listTools === "function" && typeof v.call === "function");
42
+ }
@@ -0,0 +1,17 @@
1
+ /** Paths the runtime keeps for itself. Tools may not read or write them. */
2
+ export declare const RESERVED_PATHS: readonly ["openshain.yaml", "work"];
3
+ /**
4
+ * Turns a tool-supplied relative path into an absolute path inside the workspace.
5
+ *
6
+ * Rejects absolute paths, `..` escapes, the reserved paths, every hidden entry
7
+ * (a segment starting with `.`, which covers `.git`, `.github`, `.env` and the
8
+ * like) and symbolic links that lead outside. Links are followed one hop at a
9
+ * time by reading them, so a link whose target does not exist yet is judged by
10
+ * where it points, not by what happens to exist. The target itself may not
11
+ * exist yet. Every failure is an OpenshainError.
12
+ *
13
+ * The result is a string: nothing stops the filesystem from changing between
14
+ * this check and the file operation. Tools that write should open with
15
+ * O_NOFOLLOW where the platform allows it.
16
+ */
17
+ export declare function resolveWorkspacePath(root: string, input: string): Promise<string>;
@@ -0,0 +1,82 @@
1
+ import { lstat, readlink, realpath } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
3
+ import { OpenshainError } from "../errors.js";
4
+ /** Paths the runtime keeps for itself. Tools may not read or write them. */
5
+ export const RESERVED_PATHS = ["openshain.yaml", "work"];
6
+ const MAX_SYMLINK_HOPS = 32;
7
+ /**
8
+ * Turns a tool-supplied relative path into an absolute path inside the workspace.
9
+ *
10
+ * Rejects absolute paths, `..` escapes, the reserved paths, every hidden entry
11
+ * (a segment starting with `.`, which covers `.git`, `.github`, `.env` and the
12
+ * like) and symbolic links that lead outside. Links are followed one hop at a
13
+ * time by reading them, so a link whose target does not exist yet is judged by
14
+ * where it points, not by what happens to exist. The target itself may not
15
+ * exist yet. Every failure is an OpenshainError.
16
+ *
17
+ * The result is a string: nothing stops the filesystem from changing between
18
+ * this check and the file operation. Tools that write should open with
19
+ * O_NOFOLLOW where the platform allows it.
20
+ */
21
+ export async function resolveWorkspacePath(root, input) {
22
+ if (input === "")
23
+ throw new OpenshainError("invalid_path", 'empty path: ""');
24
+ if (isAbsolute(input)) {
25
+ throw new OpenshainError("invalid_path", `absolute paths are not allowed: "${input}"`);
26
+ }
27
+ let rootReal;
28
+ try {
29
+ rootReal = await realpath(root);
30
+ }
31
+ catch (cause) {
32
+ throw new OpenshainError("invalid_path", `workspace root is not accessible: "${root}"`, {
33
+ cause,
34
+ });
35
+ }
36
+ return walk(rootReal, normalize(input), input, 0);
37
+ }
38
+ async function walk(rootReal, rel, input, hops) {
39
+ if (rel === ".." || rel.startsWith(`..${sep}`)) {
40
+ throw new OpenshainError("outside_workspace", `path escapes the workspace: "${input}"`);
41
+ }
42
+ const segments = rel === "." ? [] : rel.split(sep);
43
+ for (const segment of segments) {
44
+ if (segment === segments[0] &&
45
+ RESERVED_PATHS.includes(segment.toLowerCase())) {
46
+ throw new OpenshainError("reserved_path", `reserved path: "${input}"`);
47
+ }
48
+ if (segment.startsWith(".")) {
49
+ throw new OpenshainError("reserved_path", `hidden paths are reserved: "${input}"`);
50
+ }
51
+ }
52
+ let current = rootReal;
53
+ for (let i = 0; i < segments.length; i++) {
54
+ const candidate = join(current, segments[i] ?? "");
55
+ let stats;
56
+ try {
57
+ stats = await lstat(candidate);
58
+ }
59
+ catch (err) {
60
+ const code = err.code;
61
+ if (code === "ENOENT")
62
+ return join(candidate, ...segments.slice(i + 1));
63
+ throw new OpenshainError("invalid_path", `cannot resolve "${input}": ${code ?? "error"}`, {
64
+ cause: err,
65
+ });
66
+ }
67
+ if (stats.isSymbolicLink()) {
68
+ if (hops >= MAX_SYMLINK_HOPS) {
69
+ throw new OpenshainError("invalid_path", `too many symbolic links: "${input}"`);
70
+ }
71
+ const target = resolve(dirname(candidate), await readlink(candidate));
72
+ const rest = segments.slice(i + 1);
73
+ const next = normalize(join(relative(rootReal, target) || ".", ...rest));
74
+ return walk(rootReal, next, input, hops + 1);
75
+ }
76
+ if (i < segments.length - 1 && !stats.isDirectory()) {
77
+ throw new OpenshainError("invalid_path", `not a directory: "${input}"`);
78
+ }
79
+ current = candidate;
80
+ }
81
+ return current;
82
+ }
@@ -0,0 +1,30 @@
1
+ import { type ToolDefinition, type ToolEffect, type ToolProvider } from "./types.ts";
2
+ import { type InputValidation } from "./validate.ts";
3
+ export interface RegisteredTool {
4
+ definition: ToolDefinition;
5
+ providerId: string;
6
+ provider: ToolProvider;
7
+ validate: (input: unknown) => InputValidation;
8
+ }
9
+ /** A tool a provider offers that an allow list left out. Not callable; shown so the person knows it exists. */
10
+ export interface HiddenTool {
11
+ name: string;
12
+ providerId: string;
13
+ effect: ToolEffect;
14
+ }
15
+ export interface RegisterOptions {
16
+ /** Only these tools of the provider are registered. Every name must exist. */
17
+ allow?: readonly string[];
18
+ }
19
+ /** Every tool the runtime can offer, across providers. Names are unique. */
20
+ export declare class ToolRegistry {
21
+ private readonly tools;
22
+ private readonly hidden;
23
+ register(provider: ToolProvider, options?: RegisterOptions): Promise<void>;
24
+ /** True for a tool the provider offers but an allow list left out. */
25
+ isHidden(name: string): boolean;
26
+ /** Tools that providers offer but allow lists left out. */
27
+ hiddenTools(): HiddenTool[];
28
+ list(): RegisteredTool[];
29
+ get(name: string): RegisteredTool | undefined;
30
+ }
@@ -0,0 +1,68 @@
1
+ import { OpenshainError } from "../errors.js";
2
+ import { RESERVED_TOOL_NAMES, TOOL_NAME_PATTERN, } from "./types.js";
3
+ import { compileInputValidator } from "./validate.js";
4
+ /** Every tool the runtime can offer, across providers. Names are unique. */
5
+ export class ToolRegistry {
6
+ tools = new Map();
7
+ hidden = [];
8
+ async register(provider, options = {}) {
9
+ const definitions = await provider.listTools();
10
+ const provided = new Set(definitions.map((d) => d.name));
11
+ for (const name of options.allow ?? []) {
12
+ if (!provided.has(name)) {
13
+ throw new OpenshainError("config", `provider "${provider.id}" has no tool named "${name}"; it provides: ${[...provided].join(", ")}`);
14
+ }
15
+ }
16
+ const selected = options.allow
17
+ ? definitions.filter((d) => options.allow?.includes(d.name))
18
+ : definitions;
19
+ // Check everything before registering anything, so a bad provider changes nothing.
20
+ const prepared = new Map();
21
+ for (const definition of selected) {
22
+ const { name } = definition;
23
+ if (!TOOL_NAME_PATTERN.test(name)) {
24
+ throw new OpenshainError("invalid_tool", `tool name "${name}" from provider "${provider.id}" must match ${TOOL_NAME_PATTERN}`);
25
+ }
26
+ if (RESERVED_TOOL_NAMES.includes(name)) {
27
+ throw new OpenshainError("invalid_tool", `tool name "${name}" from provider "${provider.id}" is reserved for the runtime`);
28
+ }
29
+ const existing = this.tools.get(name) ?? prepared.get(name);
30
+ if (existing) {
31
+ throw new OpenshainError("duplicate_tool", `tool "${name}" is provided by both "${existing.providerId}" and "${provider.id}"`);
32
+ }
33
+ let validate;
34
+ try {
35
+ validate = compileInputValidator(definition.inputSchema);
36
+ }
37
+ catch (cause) {
38
+ throw new OpenshainError("invalid_tool", `tool "${name}" from provider "${provider.id}": ${cause.message}`, { cause });
39
+ }
40
+ prepared.set(name, { definition, providerId: provider.id, provider, validate });
41
+ }
42
+ for (const [name, tool] of prepared)
43
+ this.tools.set(name, tool);
44
+ for (const definition of definitions) {
45
+ if (!prepared.has(definition.name)) {
46
+ this.hidden.push({
47
+ name: definition.name,
48
+ providerId: provider.id,
49
+ effect: definition.effect,
50
+ });
51
+ }
52
+ }
53
+ }
54
+ /** True for a tool the provider offers but an allow list left out. */
55
+ isHidden(name) {
56
+ return !this.tools.has(name) && this.hidden.some((h) => h.name === name);
57
+ }
58
+ /** Tools that providers offer but allow lists left out. */
59
+ hiddenTools() {
60
+ return this.hidden.filter((h) => !this.tools.has(h.name)).map((h) => ({ ...h }));
61
+ }
62
+ list() {
63
+ return [...this.tools.values()];
64
+ }
65
+ get(name) {
66
+ return this.tools.get(name);
67
+ }
68
+ }
@@ -0,0 +1,44 @@
1
+ import type { WorkId } from "../ids.ts";
2
+ import type { Artifact, ToolContent } from "../work/events.ts";
3
+ /** A JSON Schema (draft 2020-12) object. Validated by ajv at registration. */
4
+ export type JsonSchema = Record<string, unknown>;
5
+ export type ToolEffect = "observe" | "mutate";
6
+ export declare const TOOL_NAME_PATTERN: RegExp;
7
+ /** The tool the runtime itself provides. No provider may define it. */
8
+ export declare const ASK_USER_TOOL_NAME = "ask_user";
9
+ /** Names no provider may use: the runtime's own tool and the MCP server's work tools. */
10
+ export declare const RESERVED_TOOL_NAMES: readonly string[];
11
+ export interface ToolDefinition {
12
+ /** Unique across all providers. Matches TOOL_NAME_PATTERN. */
13
+ name: string;
14
+ description: string;
15
+ inputSchema: JsonSchema;
16
+ effect: ToolEffect;
17
+ }
18
+ export interface ToolCall {
19
+ id: string;
20
+ name: string;
21
+ input: unknown;
22
+ }
23
+ export interface ToolContext {
24
+ workId: WorkId;
25
+ principalId: string;
26
+ workspaceRoot: string;
27
+ signal?: AbortSignal;
28
+ }
29
+ export interface ToolResult {
30
+ content: ToolContent[];
31
+ isError?: boolean;
32
+ /** Where the observation came from and when it was retrieved. */
33
+ observation?: {
34
+ source: string;
35
+ retrievedAt: string;
36
+ };
37
+ /** For mutate tools: the files as they are after the call. */
38
+ after?: Artifact[];
39
+ }
40
+ export interface ToolProvider {
41
+ readonly id: string;
42
+ listTools(): Promise<ToolDefinition[]>;
43
+ call(call: ToolCall, ctx: ToolContext): Promise<ToolResult>;
44
+ }
@@ -0,0 +1,15 @@
1
+ export const TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
2
+ /** The tool the runtime itself provides. No provider may define it. */
3
+ export const ASK_USER_TOOL_NAME = "ask_user";
4
+ /** Names no provider may use: the runtime's own tool and the MCP server's work tools. */
5
+ export const RESERVED_TOOL_NAMES = [
6
+ ASK_USER_TOOL_NAME,
7
+ "work_create",
8
+ "work_select",
9
+ "work_get",
10
+ "work_list",
11
+ "work_complete",
12
+ "work_fail",
13
+ "work_run",
14
+ "work_show",
15
+ ];
@@ -0,0 +1,14 @@
1
+ import type { JsonSchema } from "./types.ts";
2
+ export type InputValidation = {
3
+ ok: true;
4
+ } | {
5
+ ok: false;
6
+ reason: string;
7
+ };
8
+ /**
9
+ * Compiles a tool's input schema once. Unknown keywords are tolerated because
10
+ * third-party schemas may carry vendor extensions; the schema itself must be
11
+ * valid, must describe an object, and may not contain a regular expression
12
+ * that can be made to backtrack catastrophically (the model controls the input).
13
+ */
14
+ export declare function compileInputValidator(schema: JsonSchema): (input: unknown) => InputValidation;
@@ -0,0 +1,68 @@
1
+ import Ajv2020 from "ajv/dist/2020.js";
2
+ import safeRegex from "safe-regex2";
3
+ import { OpenshainError } from "../errors.js";
4
+ /**
5
+ * Compiles a tool's input schema once. Unknown keywords are tolerated because
6
+ * third-party schemas may carry vendor extensions; the schema itself must be
7
+ * valid, must describe an object, and may not contain a regular expression
8
+ * that can be made to backtrack catastrophically (the model controls the input).
9
+ */
10
+ export function compileInputValidator(schema) {
11
+ if (schema.type !== "object") {
12
+ throw new OpenshainError("invalid_tool", 'input schema must have "type": "object"');
13
+ }
14
+ const unsafe = findUnsafePattern(schema);
15
+ if (unsafe !== undefined) {
16
+ throw new OpenshainError("invalid_tool", `input schema contains a regular expression that can backtrack catastrophically: ${unsafe}`);
17
+ }
18
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
19
+ let validate;
20
+ try {
21
+ validate = ajv.compile(schema);
22
+ }
23
+ catch (cause) {
24
+ throw new OpenshainError("invalid_tool", `input schema does not compile: ${cause.message}`, { cause });
25
+ }
26
+ return (input) => validate(input) ? { ok: true } : { ok: false, reason: describe(validate.errors ?? []) };
27
+ }
28
+ /** Walks the schema and returns the first `pattern` or `patternProperties` key that is unsafe. */
29
+ function findUnsafePattern(node) {
30
+ if (Array.isArray(node)) {
31
+ for (const item of node) {
32
+ const found = findUnsafePattern(item);
33
+ if (found !== undefined)
34
+ return found;
35
+ }
36
+ return undefined;
37
+ }
38
+ if (node === null || typeof node !== "object")
39
+ return undefined;
40
+ for (const [key, value] of Object.entries(node)) {
41
+ if (key === "pattern" && typeof value === "string" && !safeRegex(value))
42
+ return value;
43
+ if (key === "patternProperties" && value !== null && typeof value === "object") {
44
+ for (const pattern of Object.keys(value)) {
45
+ if (!safeRegex(pattern))
46
+ return pattern;
47
+ }
48
+ }
49
+ const found = findUnsafePattern(value);
50
+ if (found !== undefined)
51
+ return found;
52
+ }
53
+ return undefined;
54
+ }
55
+ function describe(errors) {
56
+ return errors
57
+ .map((error) => {
58
+ const where = error.instancePath || "/";
59
+ const params = error.params;
60
+ const detail = error.keyword === "additionalProperties"
61
+ ? ` (${String(params.additionalProperty)})`
62
+ : error.keyword === "required"
63
+ ? ` (${String(params.missingProperty)})`
64
+ : "";
65
+ return `${where} ${error.message ?? error.keyword}${detail}`;
66
+ })
67
+ .join("; ");
68
+ }
package/dist/uuid.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function uuidv7(now?: number): string;
package/dist/uuid.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * UUID v7 (RFC 9562) without a runtime-specific API, so the same code runs on Node and Bun.
3
+ * Ids made in one process within the same millisecond stay in creation order: the 12 random
4
+ * bits after the timestamp act as a counter until the clock moves on.
5
+ */
6
+ let lastMs = 0;
7
+ let counter = 0;
8
+ export function uuidv7(now = Date.now()) {
9
+ const bytes = new Uint8Array(16);
10
+ crypto.getRandomValues(bytes);
11
+ if (now > lastMs) {
12
+ lastMs = now;
13
+ counter = ((bytes[6] & 0x07) << 8) | bytes[7];
14
+ }
15
+ else {
16
+ now = lastMs;
17
+ counter = (counter + 1) & 0x0fff;
18
+ if (counter === 0) {
19
+ lastMs = now = lastMs + 1;
20
+ }
21
+ }
22
+ bytes[0] = Math.floor(now / 2 ** 40) & 0xff;
23
+ bytes[1] = Math.floor(now / 2 ** 32) & 0xff;
24
+ bytes[2] = (now >>> 24) & 0xff;
25
+ bytes[3] = (now >>> 16) & 0xff;
26
+ bytes[4] = (now >>> 8) & 0xff;
27
+ bytes[5] = now & 0xff;
28
+ bytes[6] = 0x70 | (counter >>> 8);
29
+ bytes[7] = counter & 0xff;
30
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
31
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
32
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
33
+ }
@@ -0,0 +1,7 @@
1
+ import type { Artifact } from "./events.ts";
2
+ /**
3
+ * The artifact as it is now. The runtime computes the hash rather than taking anyone's word.
4
+ * When the file cannot be read, because a later call moved or deleted it or because nobody
5
+ * wrote it, the artifact keeps the hash that was reported and is marked missing.
6
+ */
7
+ export declare function verifyArtifact(root: string, path: string, reported: string): Promise<Artifact>;
@@ -0,0 +1,20 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolveWorkspacePath } from "../tool/paths.js";
4
+ /**
5
+ * The artifact as it is now. The runtime computes the hash rather than taking anyone's word.
6
+ * When the file cannot be read, because a later call moved or deleted it or because nobody
7
+ * wrote it, the artifact keeps the hash that was reported and is marked missing.
8
+ */
9
+ export async function verifyArtifact(root, path, reported) {
10
+ try {
11
+ const resolved = await resolveWorkspacePath(root, path);
12
+ const sha256 = createHash("sha256")
13
+ .update(await readFile(resolved))
14
+ .digest("hex");
15
+ return { path, sha256 };
16
+ }
17
+ catch {
18
+ return { path, sha256: reported, missing: true };
19
+ }
20
+ }
@@ -0,0 +1,28 @@
1
+ import { type WorkId } from "../ids.ts";
2
+ import { type AnyEvent, type Event, type EventPayloads, type EventType } from "./events.ts";
3
+ export declare const EVENTS_FILE_NAME = "events.jsonl";
4
+ export interface NewEvent<T extends EventType = EventType> {
5
+ type: T;
6
+ payload: EventPayloads[T];
7
+ /** When the thing happened. Defaults to the time of recording. */
8
+ occurredAt?: string;
9
+ }
10
+ /**
11
+ * Append-only log of one work's events. The file is the source of truth.
12
+ *
13
+ * Every line is checked on open and on read; a line that cannot be read stops
14
+ * the reader. Every event is checked on append by reading its own line back
15
+ * before it is written, so what is written can always be read. A change to the
16
+ * file by someone else between two operations of this instance is refused.
17
+ */
18
+ export declare class EventLog {
19
+ private readonly path;
20
+ private readonly workId;
21
+ private nextSeq;
22
+ private size;
23
+ private constructor();
24
+ /** Opens (creating the directory if needed) and checks the existing log end to end. */
25
+ static open(dir: string, workId: WorkId): Promise<EventLog>;
26
+ append<T extends EventType>(input: NewEvent<T>): Promise<Event<T>>;
27
+ read(): Promise<AnyEvent[]>;
28
+ }