graphlin 0.1.3 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +12 -3
- package/docs/decision-service.md +393 -0
- package/docs/extension-authoring.md +553 -0
- package/docs/model-api.md +293 -0
- package/docs/usage.md +465 -0
- package/docs/visualizer-views.md +199 -0
- package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
- package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
- package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
- package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
- package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
- package/package.json +74 -9
- package/plugin.json +4 -2
- package/runtime/core/evidence.mjs +43 -9
- package/runtime/core/graph.mjs +11 -6
- package/runtime/core/privacy.mjs +1 -0
- package/runtime/daemon/auth.mjs +7 -3
- package/runtime/daemon/diagnostics.mjs +1 -1
- package/runtime/daemon/extension-api.mjs +203 -0
- package/runtime/daemon/lineage.mjs +70 -0
- package/runtime/daemon/manager.mjs +9 -6
- package/runtime/daemon/model-api.mjs +728 -0
- package/runtime/daemon/model-persistence.mjs +220 -0
- package/runtime/daemon/server.mjs +70 -12
- package/runtime/daemon/settings.mjs +11 -3
- package/runtime/decisions/broker.mjs +349 -0
- package/runtime/decisions/contracts.mjs +179 -0
- package/runtime/decisions/evaluation.mjs +305 -0
- package/runtime/decisions/faults.mjs +32 -0
- package/runtime/decisions/index.mjs +818 -0
- package/runtime/decisions/profiles.mjs +93 -0
- package/runtime/decisions/questions.mjs +268 -0
- package/runtime/discovery/index.mjs +2 -0
- package/runtime/discovery/inventory.mjs +160 -0
- package/runtime/discovery/parser.mjs +40 -0
- package/runtime/discovery/structure.mjs +232 -0
- package/runtime/extensions/contracts.mjs +59 -0
- package/runtime/extensions/frame.mjs +64 -0
- package/runtime/extensions/index.mjs +9 -0
- package/runtime/extensions/manifest.mjs +95 -0
- package/runtime/extensions/packages.mjs +222 -0
- package/runtime/extensions/profiles.mjs +36 -0
- package/runtime/extensions/projection.mjs +130 -0
- package/runtime/extensions/registry.mjs +285 -0
- package/runtime/extensions/scene.mjs +105 -0
- package/runtime/extensions/sdk.d.ts +205 -0
- package/runtime/extensions/sdk.mjs +88 -0
- package/runtime/jev/index.mjs +13 -777
- package/runtime/jev/provider.mjs +101 -0
- package/runtime/jev/questions.mjs +16 -258
- package/runtime/jev/wire.mjs +17 -25
- package/runtime/model/changes.mjs +42 -0
- package/runtime/model/history.mjs +124 -0
- package/runtime/model/index.mjs +2 -0
- package/runtime/model/project-model.mjs +889 -0
- package/runtime/model/records.mjs +239 -0
- package/runtime/pipeline.mjs +127 -48
- package/runtime/platform.mjs +254 -0
- package/runtime/visualizers/blocks.mjs +5 -0
- package/runtime/visualizers/c4.mjs +52 -0
- package/runtime/visualizers/changes.mjs +24 -0
- package/runtime/visualizers/code.mjs +5 -0
- package/runtime/visualizers/index.mjs +23 -0
- package/runtime/visualizers/structure.mjs +120 -0
- package/runtime/visualizers/timeline.mjs +66 -0
- package/runtime/web/app.js +225 -63
- package/runtime/web/extension-frame.js +128 -0
- package/runtime/web/index.html +36 -1
- package/runtime/web/model-client.js +162 -0
- package/runtime/web/platform.js +337 -0
- package/runtime/web/scene.js +111 -0
- package/runtime/web/style.css +49 -0
- package/schemas/graph.schema.json +4 -1
- package/scripts/arguments.mjs +5 -1
- package/scripts/build-packages.mjs +6 -2
- package/scripts/control.mjs +1 -1
- package/scripts/daemon.mjs +2 -1
- package/scripts/extensions.mjs +44 -0
- package/scripts/graphlin.mjs +23 -3
- package/scripts/onboarding.mjs +10 -3
- package/scripts/validate-packages.mjs +54 -8
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open, realpath, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { readPrivateJSON } from './paths.mjs';
|
|
6
|
+
|
|
7
|
+
export const MAX_MODEL_STATE_BYTES = 48 * 1024 * 1024;
|
|
8
|
+
const MAX_NODES = 2_000_000, MAX_DEPTH = 64;
|
|
9
|
+
const COLLECTIONS = ['entities', 'relations', 'interpretations', 'activity', 'sessions', 'checkpoints'];
|
|
10
|
+
const ROOT_FIELDS = new Set(['schemaVersion', 'projectId', 'revision', 'sequence', 'coverage', 'storage', ...COLLECTIONS]);
|
|
11
|
+
const plain = value => value !== null && typeof value === 'object' &&
|
|
12
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
13
|
+
const natural = value => Number.isSafeInteger(value) && value >= 0;
|
|
14
|
+
const id = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/.test(value);
|
|
15
|
+
const invalid = () => { throw new Error('invalid_model_state'); };
|
|
16
|
+
|
|
17
|
+
function validateSnapshot(value, projectId, depth = 0) {
|
|
18
|
+
if (!plain(value) || depth > 8 || value.schemaVersion !== 2 || value.projectId !== projectId ||
|
|
19
|
+
!natural(value.revision) || !natural(value.sequence) || !plain(value.coverage) ||
|
|
20
|
+
Object.keys(value).some(key => !ROOT_FIELDS.has(key)) ||
|
|
21
|
+
COLLECTIONS.some(key => !Array.isArray(value[key])) ||
|
|
22
|
+
(value.storage !== undefined && !plain(value.storage))) invalid();
|
|
23
|
+
for (const checkpoint of value.checkpoints) {
|
|
24
|
+
if (!plain(checkpoint) || !id(checkpoint.id) ||
|
|
25
|
+
(checkpoint.projectId !== undefined && checkpoint.projectId !== projectId)) invalid();
|
|
26
|
+
if (checkpoint.state !== undefined) validateSnapshot(checkpoint.state, projectId, depth + 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Count exact UTF-8 JSON bytes before allocating the complete encoded snapshot.
|
|
31
|
+
// Reject cycles, getters and non-JSON values instead of executing toJSON hooks or
|
|
32
|
+
// silently losing Map/BigInt values. Undefined object fields are omitted, as in
|
|
33
|
+
// JSON.stringify: current policy uses them to withhold display/path metadata.
|
|
34
|
+
function boundedJSON(value, limit) {
|
|
35
|
+
let size = 0, nodes = 0;
|
|
36
|
+
const ancestors = new Set();
|
|
37
|
+
const add = count => { size += count; if (size > limit) invalid(); };
|
|
38
|
+
function string(value) {
|
|
39
|
+
add(2);
|
|
40
|
+
for (let i = 0; i < value.length; i++) {
|
|
41
|
+
const code = value.charCodeAt(i);
|
|
42
|
+
if (code === 34 || code === 92) add(2);
|
|
43
|
+
else if (code < 32) add([8, 9, 10, 12, 13].includes(code) ? 2 : 6);
|
|
44
|
+
else if (code < 128) add(1);
|
|
45
|
+
else if (code < 2048) add(2);
|
|
46
|
+
else if (code >= 0xd800 && code <= 0xdbff &&
|
|
47
|
+
value.charCodeAt(i + 1) >= 0xdc00 && value.charCodeAt(i + 1) <= 0xdfff) { add(4); i++; }
|
|
48
|
+
else if (code >= 0xd800 && code <= 0xdfff) add(6);
|
|
49
|
+
else add(3);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function visit(value, depth) {
|
|
53
|
+
if (++nodes > MAX_NODES || depth > MAX_DEPTH) invalid();
|
|
54
|
+
if (value === null) { add(4); return; }
|
|
55
|
+
if (typeof value === 'string') { string(value); return; }
|
|
56
|
+
if (typeof value === 'boolean') { add(value ? 4 : 5); return; }
|
|
57
|
+
if (typeof value === 'number' && Number.isFinite(value)) { add(JSON.stringify(value).length); return; }
|
|
58
|
+
if ((!plain(value) && !Array.isArray(value)) || ancestors.has(value)) invalid();
|
|
59
|
+
ancestors.add(value);
|
|
60
|
+
const keys = Reflect.ownKeys(value);
|
|
61
|
+
if (keys.some(key => typeof key !== 'string')) invalid();
|
|
62
|
+
add(2);
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
if (value.length > MAX_NODES || keys.length !== value.length + 1) invalid();
|
|
65
|
+
for (let i = 0; i < value.length; i++) {
|
|
66
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
|
|
67
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value') || !descriptor.enumerable) invalid();
|
|
68
|
+
if (i) add(1);
|
|
69
|
+
visit(descriptor.value, depth + 1);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
let emitted = 0;
|
|
73
|
+
for (let i = 0; i < keys.length; i++) {
|
|
74
|
+
const key = keys[i], descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
75
|
+
if (!Object.hasOwn(descriptor, 'value') || !descriptor.enumerable) invalid();
|
|
76
|
+
if (descriptor.value === undefined) continue;
|
|
77
|
+
if (emitted++) add(1);
|
|
78
|
+
string(key); add(1); visit(descriptor.value, depth + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
ancestors.delete(value);
|
|
82
|
+
}
|
|
83
|
+
visit(value, 0);
|
|
84
|
+
const result = JSON.stringify(value);
|
|
85
|
+
if (Buffer.byteLength(result) !== size || size > limit) invalid();
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function owned(info) {
|
|
90
|
+
return process.getuid === undefined || info.uid === process.getuid();
|
|
91
|
+
}
|
|
92
|
+
async function safeDirectory(directory) {
|
|
93
|
+
const info = await lstat(directory);
|
|
94
|
+
if (!info.isDirectory() || info.isSymbolicLink() || !owned(info) ||
|
|
95
|
+
(info.mode & 0o077) !== 0 || await realpath(directory) !== directory) invalid();
|
|
96
|
+
return info;
|
|
97
|
+
}
|
|
98
|
+
async function safeDestination(filename) {
|
|
99
|
+
try {
|
|
100
|
+
const info = await lstat(filename);
|
|
101
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || !owned(info) ||
|
|
102
|
+
(info.mode & 0o077) !== 0) invalid();
|
|
103
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The parent MUST pass model.snapshot({ persistent: true }) under current policy.
|
|
108
|
+
* This storage layer preserves that JSON (including storage/checkpoint state);
|
|
109
|
+
* it is not an export projector, source reader, migration, or policy authority.
|
|
110
|
+
*
|
|
111
|
+
* Use a separate, canonical private path such as <project-data>/model-state.json.
|
|
112
|
+
* The parent retains its existing state.json writer and owns daemon locking.
|
|
113
|
+
* An omitted projectId binds to the first successfully loaded/scheduled model.
|
|
114
|
+
*/
|
|
115
|
+
export function createModelPersistence(filename, {
|
|
116
|
+
projectId, maxBytes = MAX_MODEL_STATE_BYTES, debounceMs = 100, now = Date.now,
|
|
117
|
+
} = {}) {
|
|
118
|
+
if (typeof filename !== 'string' || !filename || filename.includes('\0') || !path.isAbsolute(filename) ||
|
|
119
|
+
path.resolve(filename) !== filename || path.basename(filename).toLowerCase() === 'state.json' ||
|
|
120
|
+
(projectId !== undefined && !id(projectId)) || !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_MODEL_STATE_BYTES ||
|
|
121
|
+
!Number.isInteger(debounceMs) || debounceMs < 0 || debounceMs > 1000 || typeof now !== 'function') {
|
|
122
|
+
throw new TypeError('invalid_model_persistence_options');
|
|
123
|
+
}
|
|
124
|
+
const directory = path.dirname(filename);
|
|
125
|
+
let pending = null, timer = null, running = null, closing = null, closed = false;
|
|
126
|
+
let failures = 0, writes = 0;
|
|
127
|
+
|
|
128
|
+
async function write(body) {
|
|
129
|
+
const parent = await safeDirectory(directory);
|
|
130
|
+
await safeDestination(filename);
|
|
131
|
+
const temporary = path.join(directory, `.${path.basename(filename)}.${randomUUID()}.tmp`);
|
|
132
|
+
let file, directoryHandle;
|
|
133
|
+
try {
|
|
134
|
+
directoryHandle = await open(directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
|
|
135
|
+
const opened = await directoryHandle.stat();
|
|
136
|
+
if (opened.dev !== parent.dev || opened.ino !== parent.ino) invalid();
|
|
137
|
+
file = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL |
|
|
138
|
+
(constants.O_NOFOLLOW ?? 0), 0o600);
|
|
139
|
+
await file.writeFile(body, 'utf8');
|
|
140
|
+
await file.sync();
|
|
141
|
+
await file.close(); file = null;
|
|
142
|
+
const current = await safeDirectory(directory);
|
|
143
|
+
if (current.dev !== parent.dev || current.ino !== parent.ino) invalid();
|
|
144
|
+
await safeDestination(filename);
|
|
145
|
+
await rename(temporary, filename);
|
|
146
|
+
try { await directoryHandle.sync(); }
|
|
147
|
+
catch (error) { if (!['EINVAL', 'ENOTSUP'].includes(error.code)) throw error; }
|
|
148
|
+
writes++;
|
|
149
|
+
} finally {
|
|
150
|
+
await file?.close().catch(() => {});
|
|
151
|
+
await directoryHandle?.close().catch(() => {});
|
|
152
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function drain() {
|
|
156
|
+
if (running) return running;
|
|
157
|
+
running = (async () => {
|
|
158
|
+
while (pending !== null) {
|
|
159
|
+
const body = pending; pending = null;
|
|
160
|
+
try { await write(body); } catch { failures++; }
|
|
161
|
+
}
|
|
162
|
+
})().finally(() => { running = null; });
|
|
163
|
+
return running;
|
|
164
|
+
}
|
|
165
|
+
async function flush() {
|
|
166
|
+
do {
|
|
167
|
+
clearTimeout(timer); timer = null;
|
|
168
|
+
await drain();
|
|
169
|
+
} while (pending !== null || running !== null);
|
|
170
|
+
}
|
|
171
|
+
async function load() {
|
|
172
|
+
try {
|
|
173
|
+
await safeDirectory(directory);
|
|
174
|
+
const envelope = await readPrivateJSON(filename, maxBytes);
|
|
175
|
+
if (!plain(envelope) || envelope.schemaVersion !== 2 || !id(envelope.projectId) ||
|
|
176
|
+
(projectId !== undefined && envelope.projectId !== projectId) ||
|
|
177
|
+
!natural(envelope.savedAt) || envelope.savedAt > now() + 60_000 ||
|
|
178
|
+
Object.keys(envelope).some(key => !['schemaVersion', 'savedAt', 'projectId', 'snapshot'].includes(key))) invalid();
|
|
179
|
+
// Revalidate the same depth, JSON and schema limits when reading a file
|
|
180
|
+
// made by an older daemon. Never delete an incompatible or invalid file.
|
|
181
|
+
boundedJSON(envelope, maxBytes);
|
|
182
|
+
validateSnapshot(envelope.snapshot, envelope.projectId);
|
|
183
|
+
projectId ??= envelope.projectId;
|
|
184
|
+
return envelope.snapshot;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error?.code !== 'ENOENT') failures++;
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function schedule(snapshot) {
|
|
191
|
+
if (closed) return false;
|
|
192
|
+
try {
|
|
193
|
+
const savedAt = now();
|
|
194
|
+
if (!natural(savedAt)) invalid();
|
|
195
|
+
const candidateProjectId = projectId ?? (plain(snapshot)
|
|
196
|
+
? Object.getOwnPropertyDescriptor(snapshot, 'projectId')?.value : undefined);
|
|
197
|
+
if (!id(candidateProjectId)) invalid();
|
|
198
|
+
// Bounded serialization runs first, before reading any schema properties
|
|
199
|
+
// that could otherwise invoke a caller-provided getter.
|
|
200
|
+
const body = boundedJSON({ schemaVersion: 2, savedAt, projectId: candidateProjectId, snapshot }, maxBytes);
|
|
201
|
+
validateSnapshot(snapshot, candidateProjectId);
|
|
202
|
+
projectId ??= candidateProjectId;
|
|
203
|
+
pending = body; // An immutable serialization, never a caller-owned object.
|
|
204
|
+
if (!timer && !running) {
|
|
205
|
+
timer = setTimeout(() => { timer = null; void drain(); }, debounceMs);
|
|
206
|
+
timer.unref?.();
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
} catch {
|
|
210
|
+
failures++;
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function close() {
|
|
215
|
+
if (!closing) { closed = true; closing = flush(); }
|
|
216
|
+
return closing;
|
|
217
|
+
}
|
|
218
|
+
return { load, schedule, flush, close,
|
|
219
|
+
stats: () => ({ persistenceFailures: failures, writes, pending: pending !== null || running !== null, maxBytes }) };
|
|
220
|
+
}
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import net from 'node:net';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import { readFile, chmod, rm } from 'node:fs/promises';
|
|
4
5
|
import { createPipeline } from '../pipeline.mjs';
|
|
5
6
|
import { createPolicy, materializeBundle, buildRelationProposals } from '../core/index.mjs';
|
|
6
|
-
import { createDecisionService } from '../
|
|
7
|
+
import { createDecisionService } from '../decisions/index.mjs';
|
|
8
|
+
import { createJevProvider } from '../jev/provider.mjs';
|
|
9
|
+
import { createAnalysisBroker } from '../decisions/broker.mjs';
|
|
7
10
|
import { projectPaths, canonicalProjectRoot, MAX_IPC_BYTES, MAX_STATE_BYTES, PROTOCOL, runtimeError } from './paths.mjs';
|
|
8
11
|
import { acquireLock } from './lock.mjs';
|
|
9
12
|
import { createAuth } from './auth.mjs';
|
|
10
13
|
import { createPersistence } from './persistence.mjs';
|
|
14
|
+
import { createModelPersistence } from './model-persistence.mjs';
|
|
15
|
+
import { createModelAPI } from './model-api.mjs';
|
|
16
|
+
import { createExtensionAPI } from './extension-api.mjs';
|
|
17
|
+
import { createExtensionRegistry } from '../extensions/index.mjs';
|
|
11
18
|
import { exportSnapshot } from './export.mjs';
|
|
12
19
|
import { createDiagnostics } from './diagnostics.mjs';
|
|
13
20
|
import { createDashboardInfoProvider } from './dashboard-info.mjs';
|
|
21
|
+
import { createLineageReader } from './lineage.mjs';
|
|
14
22
|
|
|
15
23
|
const WEB = new URL('../web/', import.meta.url);
|
|
16
24
|
const assets = new Map([
|
|
@@ -21,8 +29,17 @@ const assets = new Map([
|
|
|
21
29
|
['/sketch.js', ['sketch.js', 'text/javascript; charset=utf-8']],
|
|
22
30
|
['/style.css', ['style.css', 'text/css; charset=utf-8']],
|
|
23
31
|
]);
|
|
32
|
+
for (const filename of ['platform.js', 'model-client.js', 'scene.js', 'extension-frame.js']) {
|
|
33
|
+
assets.set(`/${filename}`, [filename, 'text/javascript; charset=utf-8']);
|
|
34
|
+
}
|
|
35
|
+
for (const filename of [
|
|
36
|
+
'visualizers/index.mjs', 'visualizers/structure.mjs', 'visualizers/code.mjs',
|
|
37
|
+
'visualizers/blocks.mjs', 'visualizers/c4.mjs', 'visualizers/changes.mjs', 'visualizers/timeline.mjs',
|
|
38
|
+
'model/changes.mjs', 'extensions/scene.mjs', 'extensions/contracts.mjs',
|
|
39
|
+
'extensions/sdk.mjs', 'extensions/profiles.mjs',
|
|
40
|
+
]) assets.set(`/${filename}`, [`../${filename}`, 'text/javascript; charset=utf-8']);
|
|
24
41
|
const HOSTS = new Set(['claude', 'codex', 'kiro']);
|
|
25
|
-
const CSP = "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'";
|
|
42
|
+
const CSP = "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'";
|
|
26
43
|
|
|
27
44
|
function headers(res) {
|
|
28
45
|
res.setHeader('Cache-Control', 'no-store');
|
|
@@ -53,25 +70,27 @@ async function bodyJSON(req) {
|
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
56
|
-
decisionService, apiKey: configuredKey, mode = 'live', port = 0, dashboardInfoDependencies } = {}) {
|
|
73
|
+
decisionService, decisionProvider, apiKey: configuredKey, mode = 'live', port = 0, dashboardInfoDependencies } = {}) {
|
|
57
74
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw runtimeError('invalid_port');
|
|
58
75
|
if (!['live', 'demo'].includes(mode)) throw runtimeError('invalid_mode');
|
|
59
76
|
const paths = await projectPaths(projectRoot, dataDir, { create: true });
|
|
77
|
+
const readLineage = createLineageReader({ projectRoot: paths.projectRoot, projectId: paths.projectId });
|
|
60
78
|
const dashboardInfo = createDashboardInfoProvider({
|
|
61
79
|
projectRoot: paths.projectRoot, dataDir: paths.dataDir, mode,
|
|
62
80
|
}, dashboardInfoDependencies);
|
|
63
81
|
const lock = await acquireLock(paths);
|
|
64
82
|
let finished;
|
|
65
83
|
const whenClosed = new Promise(resolve => { finished = resolve; });
|
|
66
|
-
let web, ipc, pipeline, auth, diagnostics, interval, ping, closing, reconciling = false;
|
|
84
|
+
let web, ipc, pipeline, auth, diagnostics, modelAPI, extensionAPI, modelFlush, interval, ping, closing, reconciling = false;
|
|
67
85
|
let drops = 0, intake = 0;
|
|
68
86
|
const receivedHooks = { claude: 0, codex: 0, kiro: 0 };
|
|
69
87
|
const clients = new Set(), connections = new Set(), ipcConnections = new Set();
|
|
70
88
|
const persistence = createPersistence(paths.state);
|
|
89
|
+
const modelPersistence = createModelPersistence(path.join(paths.directory, 'model-state.json'), { projectId: paths.projectId });
|
|
71
90
|
const policy = createPolicy(policyOptions ?? {});
|
|
72
|
-
const apiKey = !decisionService && policy.transmitSource && mode === 'live'
|
|
91
|
+
const apiKey = !decisionService && !decisionProvider && policy.transmitSource && mode === 'live'
|
|
73
92
|
? configuredKey ?? process.env.TYPESAFE_API_KEY : undefined;
|
|
74
|
-
const missingKey = !decisionService && policy.transmitSource && mode === 'live' && !apiKey;
|
|
93
|
+
const missingKey = !decisionService && !decisionProvider && policy.transmitSource && mode === 'live' && !apiKey;
|
|
75
94
|
function snapshot(persistent = false) {
|
|
76
95
|
const state = pipeline.getState({ persistent });
|
|
77
96
|
return { ...state, status: { ...state.status,
|
|
@@ -88,13 +107,25 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
88
107
|
function notify() {
|
|
89
108
|
if (!pipeline || closing) return;
|
|
90
109
|
persistence.schedule(snapshot(true));
|
|
110
|
+
if (!modelFlush) {
|
|
111
|
+
// Coalesce model serialization outside the hook intake path.
|
|
112
|
+
modelFlush = setTimeout(() => {
|
|
113
|
+
modelFlush = null;
|
|
114
|
+
if (!closing) modelPersistence.schedule(pipeline.getModelState({ persistent: true }));
|
|
115
|
+
}, 100);
|
|
116
|
+
modelFlush.unref?.();
|
|
117
|
+
}
|
|
118
|
+
modelAPI?.notify();
|
|
91
119
|
const state = snapshot();
|
|
92
120
|
for (const res of clients) stream(res, state);
|
|
93
121
|
}
|
|
94
122
|
async function reconcile() {
|
|
95
123
|
if (reconciling || closing || !pipeline) return;
|
|
96
124
|
reconciling = true;
|
|
97
|
-
try {
|
|
125
|
+
try {
|
|
126
|
+
await pipeline.observeLineage(await readLineage());
|
|
127
|
+
await pipeline.reconcile();
|
|
128
|
+
}
|
|
98
129
|
catch { drops++; }
|
|
99
130
|
finally { reconciling = false; }
|
|
100
131
|
}
|
|
@@ -102,6 +133,8 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
102
133
|
if (closing) return closing;
|
|
103
134
|
closing = (async () => {
|
|
104
135
|
clearInterval(interval); clearInterval(ping);
|
|
136
|
+
clearTimeout(modelFlush);
|
|
137
|
+
modelAPI?.close();
|
|
105
138
|
auth?.clear();
|
|
106
139
|
for (const res of clients) res.end();
|
|
107
140
|
for (const socket of [...connections, ...ipcConnections]) socket.destroy();
|
|
@@ -109,8 +142,11 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
109
142
|
new Promise(resolve => server.close(() => resolve()))));
|
|
110
143
|
try {
|
|
111
144
|
await pipeline?.close();
|
|
112
|
-
if (pipeline)
|
|
113
|
-
|
|
145
|
+
if (pipeline) {
|
|
146
|
+
persistence.schedule(snapshot(true));
|
|
147
|
+
modelPersistence.schedule(pipeline.getModelState({ persistent: true }));
|
|
148
|
+
}
|
|
149
|
+
await Promise.all([persistence.close(), modelPersistence.close()]);
|
|
114
150
|
} finally {
|
|
115
151
|
try { await diagnostics?.close(); }
|
|
116
152
|
finally {
|
|
@@ -127,19 +163,40 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
127
163
|
// Reading the key is conditional on explicit source-transmission permission.
|
|
128
164
|
// Test/demo services are injected; this module never logs request bodies.
|
|
129
165
|
const service = decisionService ?? createDecisionService({
|
|
130
|
-
apiKey,
|
|
166
|
+
provider: decisionProvider ?? createJevProvider({ apiKey }),
|
|
131
167
|
materializeBundle, buildRelationProposals,
|
|
132
168
|
limits: { eventDeadlineMs: 5000 },
|
|
133
169
|
});
|
|
134
170
|
pipeline = createPipeline({ projectRoot: paths.projectRoot, policy, decisionService: service,
|
|
135
171
|
classificationDeadlineMs: 5000,
|
|
136
|
-
mode, restoredState: await persistence.load(),
|
|
172
|
+
mode, restoredState: await persistence.load(), restoredModel: await modelPersistence.load(),
|
|
173
|
+
onChange: notify, onDiagnostic: diagnostics.record });
|
|
174
|
+
modelAPI = createModelAPI({ projectId: paths.projectId, getSnapshot: pipeline.getModelState,
|
|
175
|
+
createCheckpoint: options => {
|
|
176
|
+
const marker = pipeline.createCheckpoint(options);
|
|
177
|
+
notify();
|
|
178
|
+
return marker;
|
|
179
|
+
} });
|
|
180
|
+
const registry = await createExtensionRegistry({ dataDir: paths.dataDir, projectId: paths.projectId });
|
|
181
|
+
const analyze = typeof service.evaluate === 'function'
|
|
182
|
+
? createAnalysisBroker({ service, model: pipeline.model, policy, projectId: paths.projectId, registry })
|
|
183
|
+
: async () => ({ status: 'unavailable' });
|
|
184
|
+
extensionAPI = createExtensionAPI({ registry, projectId: paths.projectId, getSnapshot: pipeline.getModelState,
|
|
185
|
+
runAnalysis: async input => { const result = await analyze(input); notify(); return result; } });
|
|
137
186
|
web = http.createServer({ maxHeaderSize: 8192, requestTimeout: 2000, headersTimeout: 2000 }, (req, res) => {
|
|
138
187
|
headers(res);
|
|
139
188
|
void (async () => {
|
|
189
|
+
if (req.url?.startsWith('/api/model/v1/')) {
|
|
190
|
+
if (!auth?.validTransport(req)) return json(res, 403, { error: 'forbidden_origin' });
|
|
191
|
+
await modelAPI.handle(req, res, {
|
|
192
|
+
viewerAuthorized: auth.validRequest(req, { mutation: req.method !== 'GET' }) && auth.authorized(req),
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
140
196
|
if (!auth || !auth.validRequest(req, { mutation: req.method !== 'GET' })) {
|
|
141
197
|
return json(res, 403, { error: 'forbidden_origin' });
|
|
142
198
|
}
|
|
199
|
+
if (await extensionAPI.handle(req, res, { viewerAuthorized: auth.authorized(req) })) return;
|
|
143
200
|
if (typeof req.url !== 'string' || req.url.length > 1024 || req.url.includes('?')) {
|
|
144
201
|
return json(res, 400, { error: 'invalid_route' });
|
|
145
202
|
}
|
|
@@ -218,10 +275,11 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
218
275
|
function describe() {
|
|
219
276
|
return { ok: true, protocol: PROTOCOL, instanceId: lock.owner.instanceId,
|
|
220
277
|
projectId: paths.projectId, pid: process.pid, port: actualPort, mode,
|
|
221
|
-
policy: { transmitSource: policy.transmitSource, displayEvidence: policy.displayEvidence,
|
|
278
|
+
policy: { readSource: policy.readSource, transmitSource: policy.transmitSource, displayEvidence: policy.displayEvidence,
|
|
222
279
|
persistEvidence: policy.persistEvidence, version: policy.version },
|
|
223
280
|
status: snapshot().status, ...persistence.stats(), captureDropped: drops,
|
|
224
281
|
observations: { hooks: { ...receivedHooks }, shapes: pipeline.getState().graph.nodes.length },
|
|
282
|
+
model: pipeline.model.stats(), modelPersistence: modelPersistence.stats(),
|
|
225
283
|
logPath: diagnostics.stats().logPath, diagnostics: diagnostics.stats() };
|
|
226
284
|
}
|
|
227
285
|
// Only the exclusive owner may remove a stale socket from a prior process.
|
|
@@ -7,8 +7,11 @@ const LIMIT = 16 * 1024;
|
|
|
7
7
|
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
8
|
const validKey = value => typeof value === 'string' && value.length > 0 &&
|
|
9
9
|
value.length <= 4096 && !/[\s\u0000-\u001f\u007f]/u.test(value);
|
|
10
|
-
const validPolicy = value => record(value) &&
|
|
11
|
-
['allowSource', 'persistEvidence', 'displayEvidence'].
|
|
10
|
+
const validPolicy = value => record(value) &&
|
|
11
|
+
Object.keys(value).every(key => ['allowSource', 'localSource', 'persistEvidence', 'displayEvidence'].includes(key)) &&
|
|
12
|
+
['allowSource', 'persistEvidence', 'displayEvidence'].every(key => typeof value[key] === 'boolean') &&
|
|
13
|
+
(value.localSource === undefined || typeof value.localSource === 'boolean') &&
|
|
14
|
+
!(value.localSource && value.allowSource);
|
|
12
15
|
const validHosts = value => Array.isArray(value) && value.length <= 2 &&
|
|
13
16
|
new Set(value).size === value.length && value.every(host => ['claude', 'codex'].includes(host));
|
|
14
17
|
const validInstallation = value => record(value) &&
|
|
@@ -89,14 +92,19 @@ export function savedPolicy(policy) {
|
|
|
89
92
|
allowSource: policy.transmitSource,
|
|
90
93
|
persistEvidence: policy.persistEvidence,
|
|
91
94
|
displayEvidence: policy.displayEvidence,
|
|
95
|
+
...(policy.readSource && !policy.transmitSource ? { localSource: true } : {}),
|
|
92
96
|
} : {};
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
// An omitted field means reuse consent, while false is an explicit opt-out.
|
|
96
100
|
export function resolvePolicy(options, { current, saved } = {}) {
|
|
97
101
|
const fallback = current ? savedPolicy(current) : saved ?? {};
|
|
102
|
+
const allowSource = options.allowSource ?? fallback.allowSource ?? false;
|
|
103
|
+
const localSource = !allowSource && (options.localSource ??
|
|
104
|
+
(options.allowSource === false ? false : fallback.localSource) ?? false);
|
|
98
105
|
return {
|
|
99
|
-
allowSource
|
|
106
|
+
allowSource,
|
|
107
|
+
...(localSource ? { localSource: true } : {}),
|
|
100
108
|
persistEvidence: options.persistEvidence ?? fallback.persistEvidence ?? false,
|
|
101
109
|
displayEvidence: options.displayEvidence ?? fallback.displayEvidence ?? true,
|
|
102
110
|
};
|