@vincemakes/kiso-runtime 0.1.9 → 0.1.10

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.
@@ -10,6 +10,15 @@
10
10
  */
11
11
  import type { KisoExtension } from "@vincemakes/kiso-core";
12
12
  export type { KisoExtension };
13
+ /**
14
+ * E3 — project-level extensions: load <dir>/.kiso/extensions/*.mjs AFTER
15
+ * the trust gate (the CLI decides trust; this loader only loads). A name
16
+ * that exists in BOTH the user level and the project level is a LOUD
17
+ * startup error — silent shadowing of a user-level extension by a
18
+ * project-level one would change behavior without anyone noticing.
19
+ * `existing` are the already-loaded user-level extensions.
20
+ */
21
+ export declare function loadProjectExtensions(dir: string, existing?: readonly KisoExtension[]): Promise<KisoExtension[]>;
13
22
  export declare function loadExtensions(dir: string): Promise<KisoExtension[]>;
14
23
  /**
15
24
  * 发现#8 (P1): dispose every extension's external resources — each call
@@ -11,6 +11,23 @@
11
11
  import { readdir } from "node:fs/promises";
12
12
  import { join } from "node:path";
13
13
  import { pathToFileURL } from "node:url";
14
+ /**
15
+ * E3 — project-level extensions: load <dir>/.kiso/extensions/*.mjs AFTER
16
+ * the trust gate (the CLI decides trust; this loader only loads). A name
17
+ * that exists in BOTH the user level and the project level is a LOUD
18
+ * startup error — silent shadowing of a user-level extension by a
19
+ * project-level one would change behavior without anyone noticing.
20
+ * `existing` are the already-loaded user-level extensions.
21
+ */
22
+ export async function loadProjectExtensions(dir, existing = []) {
23
+ const projectExts = await loadExtensions(join(dir, ".kiso", "extensions"));
24
+ for (const ext of projectExts) {
25
+ if (existing.some((e) => e.name === ext.name)) {
26
+ throw new Error(`[extensions] extension name "${ext.name}" exists in both the user-level and the project-level extensions — refusing to shadow`);
27
+ }
28
+ }
29
+ return projectExts;
30
+ }
14
31
  export async function loadExtensions(dir) {
15
32
  let files;
16
33
  try {
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from "./agent.js";
2
2
  export * from "./session.js";
3
3
  export * from "./store.js";
4
4
  export * from "./extensions.js";
5
+ export * from "./trust.js";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export * from "./agent.js";
2
2
  export * from "./session.js";
3
3
  export * from "./store.js";
4
4
  export * from "./extensions.js";
5
+ export * from "./trust.js";
@@ -0,0 +1,67 @@
1
+ /**
2
+ * E3 — project-level capability is trusted by CONTENT DIGEST, not by
3
+ * directory (ADR-0037). A cloned repo's .kiso runs code on this machine
4
+ * (extensions load into the agent, mcp.json spawns servers, skills inject
5
+ * prompt text), so a trust decision is a decision about THE FILES, and it
6
+ * dies the moment the files change.
7
+ *
8
+ * projectArtifacts(cwd) discovers <cwd>/.kiso/{extensions/*.mjs, mcp.json,
9
+ * skills/<name>/SKILL.md} — the exact three artifact kinds — and returns a
10
+ * manifest (one sha256 per file) plus a BUNDLE digest: sha256 over the
11
+ * sorted relative paths and their contents. Any file change changes the
12
+ * bundle digest, which invalidates every prior trust record.
13
+ *
14
+ * The trust store (~/.kiso/trust.jsonl, KISO_HOME respected) is a simple
15
+ * append-only memo of human verdicts: {root, digest, decision, ts} per
16
+ * line, the LAST record matching (root, digest) wins. Its tolerance is
17
+ * deliberately different from the session store: trust.jsonl is NOT an
18
+ * event stream — a corrupt line means "no record" for that line (skip,
19
+ * never throw), because a lost grant only re-asks the human and a lost
20
+ * refusal also only re-asks; there is no trajectory to preserve.
21
+ */
22
+ export type TrustDecision = "granted" | "refused";
23
+ export interface TrustRecord {
24
+ /** realpath of the trusted .kiso directory — the thing being trusted. */
25
+ readonly root: string;
26
+ /** bundle sha256 of the project's .kiso artifacts at decision time. */
27
+ readonly digest: string;
28
+ readonly decision: TrustDecision;
29
+ /** ISO timestamp of the decision. */
30
+ readonly ts: string;
31
+ }
32
+ export interface ProjectArtifact {
33
+ /** path relative to the .kiso dir, e.g. "extensions/lint-rules.mjs". */
34
+ readonly path: string;
35
+ readonly kind: "extension" | "mcp" | "skill";
36
+ /** sha256 (hex) of this file's content — the listing shows a short prefix. */
37
+ readonly digest: string;
38
+ }
39
+ export interface ProjectArtifacts {
40
+ /** realpath of the .kiso directory — the trust record's root. */
41
+ readonly root: string;
42
+ readonly files: readonly ProjectArtifact[];
43
+ /** bundle sha256 (hex) over the sorted paths + contents. */
44
+ readonly digest: string;
45
+ }
46
+ /**
47
+ * Discover <cwd>/.kiso's artifacts. The skills scan is ONE level
48
+ * (<name>/SKILL.md) — the same scan the skills extension performs, so the
49
+ * digest covers exactly what gets loaded; anything deeper is inert and not
50
+ * part of the trust decision. Returns null when there is no .kiso dir or
51
+ * no recognized artifacts (an empty .kiso has nothing to gate).
52
+ */
53
+ export declare function projectArtifacts(cwd: string): Promise<ProjectArtifacts | null>;
54
+ /**
55
+ * The last record matching (root, digest) — append-only, last wins. A
56
+ * corrupt line is skipped (trust is a memo, not an event stream — see the
57
+ * module comment). Callers pass the realpath'd root from projectArtifacts.
58
+ */
59
+ export declare function trustFor(root: string, digest: string): TrustRecord | null;
60
+ /** Append one verdict. The same (root, digest) may be re-recorded — the
61
+ * newest record wins on read. */
62
+ export declare function recordTrust(record: {
63
+ root: string;
64
+ digest: string;
65
+ decision: TrustDecision;
66
+ ts?: string;
67
+ }): void;
package/dist/trust.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * E3 — project-level capability is trusted by CONTENT DIGEST, not by
3
+ * directory (ADR-0037). A cloned repo's .kiso runs code on this machine
4
+ * (extensions load into the agent, mcp.json spawns servers, skills inject
5
+ * prompt text), so a trust decision is a decision about THE FILES, and it
6
+ * dies the moment the files change.
7
+ *
8
+ * projectArtifacts(cwd) discovers <cwd>/.kiso/{extensions/*.mjs, mcp.json,
9
+ * skills/<name>/SKILL.md} — the exact three artifact kinds — and returns a
10
+ * manifest (one sha256 per file) plus a BUNDLE digest: sha256 over the
11
+ * sorted relative paths and their contents. Any file change changes the
12
+ * bundle digest, which invalidates every prior trust record.
13
+ *
14
+ * The trust store (~/.kiso/trust.jsonl, KISO_HOME respected) is a simple
15
+ * append-only memo of human verdicts: {root, digest, decision, ts} per
16
+ * line, the LAST record matching (root, digest) wins. Its tolerance is
17
+ * deliberately different from the session store: trust.jsonl is NOT an
18
+ * event stream — a corrupt line means "no record" for that line (skip,
19
+ * never throw), because a lost grant only re-asks the human and a lost
20
+ * refusal also only re-asks; there is no trajectory to preserve.
21
+ */
22
+ import { createHash } from "node:crypto";
23
+ import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
24
+ import { readFile, readdir, realpath, stat } from "node:fs/promises";
25
+ import { homedir } from "node:os";
26
+ import { join } from "node:path";
27
+ /**
28
+ * Discover <cwd>/.kiso's artifacts. The skills scan is ONE level
29
+ * (<name>/SKILL.md) — the same scan the skills extension performs, so the
30
+ * digest covers exactly what gets loaded; anything deeper is inert and not
31
+ * part of the trust decision. Returns null when there is no .kiso dir or
32
+ * no recognized artifacts (an empty .kiso has nothing to gate).
33
+ */
34
+ export async function projectArtifacts(cwd) {
35
+ const kisoDir = join(cwd, ".kiso");
36
+ try {
37
+ await stat(kisoDir);
38
+ }
39
+ catch (err) {
40
+ if (err.code === "ENOENT")
41
+ return null;
42
+ throw err;
43
+ }
44
+ const root = await realpath(kisoDir);
45
+ const entries = [];
46
+ for (const f of await readdirOrEmpty(join(root, "extensions"))) {
47
+ if (!f.endsWith(".mjs"))
48
+ continue;
49
+ entries.push({ path: `extensions/${f}`, buf: await readFile(join(root, "extensions", f)) });
50
+ }
51
+ try {
52
+ entries.push({ path: "mcp.json", buf: await readFile(join(root, "mcp.json")) });
53
+ }
54
+ catch (err) {
55
+ if (!isMissing(err))
56
+ throw err;
57
+ }
58
+ for (const dir of await readdirOrEmpty(join(root, "skills"))) {
59
+ try {
60
+ entries.push({ path: `skills/${dir}/SKILL.md`, buf: await readFile(join(root, "skills", dir, "SKILL.md")) });
61
+ }
62
+ catch (err) {
63
+ if (!isMissing(err))
64
+ throw err; // a file named like a dir → ENOTDIR: inert, skip
65
+ }
66
+ }
67
+ if (entries.length === 0)
68
+ return null;
69
+ entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
70
+ const files = entries.map((e) => ({ path: e.path, kind: kindOf(e.path), digest: sha256(e.buf) }));
71
+ return { root, files, digest: bundleDigest(entries) };
72
+ }
73
+ function bundleDigest(entries) {
74
+ const h = createHash("sha256");
75
+ for (const e of entries) {
76
+ h.update(e.path);
77
+ h.update("\n");
78
+ h.update(e.buf);
79
+ h.update("\0");
80
+ }
81
+ return h.digest("hex");
82
+ }
83
+ function sha256(buf) {
84
+ return createHash("sha256").update(buf).digest("hex");
85
+ }
86
+ function kindOf(path) {
87
+ if (path.startsWith("extensions/"))
88
+ return "extension";
89
+ if (path === "mcp.json")
90
+ return "mcp";
91
+ return "skill";
92
+ }
93
+ async function readdirOrEmpty(dir) {
94
+ try {
95
+ return await readdir(dir);
96
+ }
97
+ catch (err) {
98
+ if (isMissing(err))
99
+ return [];
100
+ throw err;
101
+ }
102
+ }
103
+ function isMissing(err) {
104
+ return err.code === "ENOENT" || err.code === "ENOTDIR";
105
+ }
106
+ function trustFile() {
107
+ return join(process.env.KISO_HOME ?? join(homedir(), ".kiso"), "trust.jsonl");
108
+ }
109
+ /**
110
+ * The last record matching (root, digest) — append-only, last wins. A
111
+ * corrupt line is skipped (trust is a memo, not an event stream — see the
112
+ * module comment). Callers pass the realpath'd root from projectArtifacts.
113
+ */
114
+ export function trustFor(root, digest) {
115
+ let text;
116
+ try {
117
+ text = readFileSync(trustFile(), "utf8");
118
+ }
119
+ catch {
120
+ return null; // no trust file = no records
121
+ }
122
+ let found = null;
123
+ for (const line of text.split("\n")) {
124
+ if (line.trim() === "")
125
+ continue;
126
+ let rec;
127
+ try {
128
+ rec = JSON.parse(line);
129
+ }
130
+ catch {
131
+ continue; // corrupt line = no record
132
+ }
133
+ if (!isTrustRecord(rec))
134
+ continue;
135
+ if (rec.root === root && rec.digest === digest)
136
+ found = rec;
137
+ }
138
+ return found;
139
+ }
140
+ /** Append one verdict. The same (root, digest) may be re-recorded — the
141
+ * newest record wins on read. */
142
+ export function recordTrust(record) {
143
+ const home = process.env.KISO_HOME ?? join(homedir(), ".kiso");
144
+ mkdirSync(home, { recursive: true });
145
+ appendFileSync(join(home, "trust.jsonl"), `${JSON.stringify({ root: record.root, digest: record.digest, decision: record.decision, ts: record.ts ?? new Date().toISOString() })}\n`, "utf8");
146
+ }
147
+ function isTrustRecord(v) {
148
+ if (typeof v !== "object" || v === null)
149
+ return false;
150
+ const r = v;
151
+ return typeof r.root === "string" && typeof r.digest === "string" && (r.decision === "granted" || r.decision === "refused") && typeof r.ts === "string";
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "kiso runtime — durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,11 +21,11 @@
21
21
  "test": "vitest run"
22
22
  },
23
23
  "dependencies": {
24
- "@vincemakes/kiso-core": "0.1.9"
24
+ "@vincemakes/kiso-core": "0.1.10"
25
25
  },
26
26
  "peerDependencies": {
27
- "@vincemakes/kiso-provider-anthropic": "0.1.9",
28
- "@vincemakes/kiso-provider-openai": "0.1.9"
27
+ "@vincemakes/kiso-provider-anthropic": "0.1.10",
28
+ "@vincemakes/kiso-provider-openai": "0.1.10"
29
29
  },
30
30
  "peerDependenciesMeta": {
31
31
  "@vincemakes/kiso-provider-anthropic": {
@@ -36,7 +36,7 @@
36
36
  }
37
37
  },
38
38
  "devDependencies": {
39
- "@vincemakes/kiso-evals": "0.1.9",
39
+ "@vincemakes/kiso-evals": "0.1.10",
40
40
  "@types/node": "^26.1.2",
41
41
  "typescript": "^5.7.2",
42
42
  "vitest": "^3.0.0"