@descryy/mcp 0.11.2 → 0.11.4
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/bin/descry-mcp.js +6 -0
- package/dist/bin/descry-mcp.js.map +1 -1
- package/dist/browser/auth.d.ts +8 -0
- package/dist/browser/auth.d.ts.map +1 -1
- package/dist/browser/auth.js +12 -2
- package/dist/browser/auth.js.map +1 -1
- package/dist/browser/driver.d.ts +21 -0
- package/dist/browser/driver.d.ts.map +1 -1
- package/dist/browser/playwright-driver.d.ts.map +1 -1
- package/dist/browser/playwright-driver.js +39 -0
- package/dist/browser/playwright-driver.js.map +1 -1
- package/dist/browser/reach-recording-session.d.ts.map +1 -1
- package/dist/browser/reach-recording-session.js +8 -0
- package/dist/browser/reach-recording-session.js.map +1 -1
- package/dist/capped.d.ts +22 -0
- package/dist/capped.d.ts.map +1 -1
- package/dist/capped.js +28 -0
- package/dist/capped.js.map +1 -1
- package/dist/credential-store.d.ts +22 -0
- package/dist/credential-store.d.ts.map +1 -0
- package/dist/credential-store.js +33 -0
- package/dist/credential-store.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/planner/predict-then-propose.d.ts +34 -1
- package/dist/planner/predict-then-propose.d.ts.map +1 -1
- package/dist/planner/predict-then-propose.js +51 -1
- package/dist/planner/predict-then-propose.js.map +1 -1
- package/dist/planner/propose-journey.d.ts +15 -0
- package/dist/planner/propose-journey.d.ts.map +1 -1
- package/dist/planner/propose-journey.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +8 -0
- package/dist/registry.js.map +1 -1
- package/dist/render.d.ts +44 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +104 -3
- package/dist/render.js.map +1 -1
- package/dist/scenarios/browser-run-plan-projection.d.ts +24 -6
- package/dist/scenarios/browser-run-plan-projection.d.ts.map +1 -1
- package/dist/scenarios/browser-run-plan-projection.js +45 -9
- package/dist/scenarios/browser-run-plan-projection.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +122 -12
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +114 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +178 -1
- package/dist/session.js.map +1 -1
- package/dist/tools/browser-run-scenario.d.ts.map +1 -1
- package/dist/tools/browser-run-scenario.js +19 -0
- package/dist/tools/browser-run-scenario.js.map +1 -1
- package/dist/tools/browser-start-session.d.ts.map +1 -1
- package/dist/tools/browser-start-session.js +75 -11
- package/dist/tools/browser-start-session.js.map +1 -1
- package/dist/tools/browser.d.ts.map +1 -1
- package/dist/tools/browser.js +9 -0
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/code-context.d.ts.map +1 -1
- package/dist/tools/code-context.js +61 -33
- package/dist/tools/code-context.js.map +1 -1
- package/dist/tools/cross-pr.d.ts.map +1 -1
- package/dist/tools/cross-pr.js +85 -36
- package/dist/tools/cross-pr.js.map +1 -1
- package/dist/tools/disclosure.d.ts.map +1 -1
- package/dist/tools/disclosure.js +25 -6
- package/dist/tools/disclosure.js.map +1 -1
- package/dist/tools/git-context.d.ts.map +1 -1
- package/dist/tools/git-context.js +22 -1
- package/dist/tools/git-context.js.map +1 -1
- package/dist/tools/git-history.d.ts.map +1 -1
- package/dist/tools/git-history.js +20 -0
- package/dist/tools/git-history.js.map +1 -1
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/observe-runtime.d.ts +40 -1
- package/dist/tools/observe-runtime.d.ts.map +1 -1
- package/dist/tools/observe-runtime.js +389 -15
- package/dist/tools/observe-runtime.js.map +1 -1
- package/dist/tools/observe.d.ts.map +1 -1
- package/dist/tools/observe.js +45 -0
- package/dist/tools/observe.js.map +1 -1
- package/dist/tools/pr-analysis.d.ts.map +1 -1
- package/dist/tools/pr-analysis.js +86 -11
- package/dist/tools/pr-analysis.js.map +1 -1
- package/dist/tools/prove-reach.d.ts +74 -0
- package/dist/tools/prove-reach.d.ts.map +1 -0
- package/dist/tools/prove-reach.js +286 -0
- package/dist/tools/prove-reach.js.map +1 -0
- package/dist/tools/refusal-fetch.d.ts.map +1 -1
- package/dist/tools/refusal-fetch.js +31 -11
- package/dist/tools/refusal-fetch.js.map +1 -1
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +42 -11
- package/dist/tools/scope.js.map +1 -1
- package/dist/tools/similar-incidents.d.ts.map +1 -1
- package/dist/tools/similar-incidents.js +42 -18
- package/dist/tools/similar-incidents.js.map +1 -1
- package/dist/tools/validate.d.ts.map +1 -1
- package/dist/tools/validate.js +145 -67
- package/dist/tools/validate.js.map +1 -1
- package/package.json +30 -26
|
@@ -11,6 +11,7 @@ import { checkNotSensitivePath, checkPathConfinement } from "../path-confinement
|
|
|
11
11
|
const ALLOW_EVIDENCE_PATH_OUTSIDE_REPO_ENV_VAR = "DESCRY_ALLOW_EVIDENCE_PATH_OUTSIDE_REPO";
|
|
12
12
|
const ALLOW_SENSITIVE_LOG_PATH_ENV_VAR = "DESCRY_ALLOW_SENSITIVE_LOG_PATH";
|
|
13
13
|
import { buildGraph, counts, createConfirmedIncidentSource, evictStaleConfirmedIncidentNodes, persistGraph, } from "@descryy/core";
|
|
14
|
+
import { createDatabaseQueryCollector, createPostgresLogSource, createPostgresQueryCollector, } from "@descryy/runtime-database-observation";
|
|
14
15
|
import { evaluateAction, PROFILE_MODES, SAFETY_LEVELS, validateProfile } from "@descryy/runtime-environment-profile";
|
|
15
16
|
import { correlateExecution } from "@descryy/runtime-evidence-correlation";
|
|
16
17
|
import { EvidenceStore } from "@descryy/runtime-evidence-store";
|
|
@@ -21,7 +22,7 @@ import { cancellationHeadline, cancellationNotes, whenAborted } from "../cancell
|
|
|
21
22
|
import { readScenario } from "../scenarios/index.js";
|
|
22
23
|
import { createRuntimeJourneyDrive, projectScenarioToRunPlan, } from "./runtime-journey-drive.js";
|
|
23
24
|
import { loadRuntimeAdapter, RuntimeAdapterLoadError } from "../runtime-registry.js";
|
|
24
|
-
import { writeConfirmedIncident } from "../session.js";
|
|
25
|
+
import { writeConfirmedIncident, writeSavedStart } from "../session.js";
|
|
25
26
|
import { runtimeObservedIncident } from "./runtime-incident.js";
|
|
26
27
|
/** Where evidence lands when the call does not say. Beside the graph, not inside it. */
|
|
27
28
|
export const DEFAULT_EVIDENCE_RELATIVE_PATH = join(".descry", "evidence.db");
|
|
@@ -62,8 +63,12 @@ const SCHEMA = {
|
|
|
62
63
|
},
|
|
63
64
|
services: {
|
|
64
65
|
type: "object",
|
|
65
|
-
description: "One entry per service, keyed by the name evidence will be attributed to.
|
|
66
|
-
"
|
|
66
|
+
description: "One entry per service, keyed by the name evidence will be attributed to. Exactly one of " +
|
|
67
|
+
"\"command\" or \"attach\" per service. Optional: when omitted, a repository that has a " +
|
|
68
|
+
"prior successful run's services saved reuses them (disclosed on the reply) — otherwise " +
|
|
69
|
+
"this call refuses and names that omission as the reason. Supplying this always overrides " +
|
|
70
|
+
"whatever is saved, and a run that reaches readiness replaces the saved services with " +
|
|
71
|
+
"this call's own, whichever source they came from.",
|
|
67
72
|
additionalProperties: {
|
|
68
73
|
type: "object",
|
|
69
74
|
properties: {
|
|
@@ -281,8 +286,35 @@ const SCHEMA = {
|
|
|
281
286
|
},
|
|
282
287
|
additionalProperties: false,
|
|
283
288
|
},
|
|
289
|
+
database: {
|
|
290
|
+
type: "object",
|
|
291
|
+
description: "RG-5(c) — the database channel. Node services get the node:sqlite observation preload " +
|
|
292
|
+
"automatically (no declaration needed); this argument is for Postgres, whose statement log " +
|
|
293
|
+
"lives on the server, not in a spawned service's own output. Declares where to tail it from, " +
|
|
294
|
+
"so real queries issued during this run's observation window become DATABASE_QUERY evidence. " +
|
|
295
|
+
"Omit entirely for no Postgres channel — the reply discloses that rather than staying silent.",
|
|
296
|
+
properties: {
|
|
297
|
+
statementLog: {
|
|
298
|
+
type: "object",
|
|
299
|
+
description: "Never turns Postgres logging on — reads whatever the deployment already produces.",
|
|
300
|
+
properties: {
|
|
301
|
+
kind: { type: "string", enum: ["file", "dockerContainer"] },
|
|
302
|
+
path: { type: "string", description: "kind \"file\": a log file the deployment already writes to." },
|
|
303
|
+
container: { type: "string", description: "kind \"dockerContainer\": tails `docker logs -f` for this container." },
|
|
304
|
+
},
|
|
305
|
+
required: ["kind"],
|
|
306
|
+
additionalProperties: false,
|
|
307
|
+
},
|
|
308
|
+
service: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "Which declared service this DATABASE_QUERY evidence is attributed to. Optional.",
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
required: ["statementLog"],
|
|
314
|
+
additionalProperties: false,
|
|
315
|
+
},
|
|
284
316
|
},
|
|
285
|
-
required: ["profile", "
|
|
317
|
+
required: ["profile", "adapter"],
|
|
286
318
|
additionalProperties: false,
|
|
287
319
|
};
|
|
288
320
|
function asRecord(value, what) {
|
|
@@ -373,12 +405,91 @@ function readAdapterSpec(args) {
|
|
|
373
405
|
...(options === undefined ? {} : { options: options }),
|
|
374
406
|
};
|
|
375
407
|
}
|
|
376
|
-
|
|
377
|
-
|
|
408
|
+
/** `readiness`'s raw JSON, captured as `SavedServiceStart.readiness` — before `readReadiness`
|
|
409
|
+
* turns it into a closure that cannot be serialised. Reads the same fields `readReadiness`
|
|
410
|
+
* itself validates, so this can only be called after that validation has already run. */
|
|
411
|
+
function toSavedReadiness(raw) {
|
|
412
|
+
const entry = asRecord(raw, "readiness");
|
|
413
|
+
const kind = entry["kind"];
|
|
414
|
+
const path = typeof entry["path"] === "string" ? entry["path"] : undefined;
|
|
415
|
+
const expectedStatus = typeof entry["expectedStatus"] === "number" ? entry["expectedStatus"] : undefined;
|
|
416
|
+
const host = typeof entry["host"] === "string" ? entry["host"] : undefined;
|
|
417
|
+
const command = typeof entry["command"] === "string" ? entry["command"] : undefined;
|
|
418
|
+
const rawArgs = entry["args"];
|
|
419
|
+
const args = Array.isArray(rawArgs) ? rawArgs : undefined;
|
|
420
|
+
const timeoutMs = typeof entry["timeoutMs"] === "number" ? entry["timeoutMs"] : undefined;
|
|
421
|
+
return {
|
|
422
|
+
kind,
|
|
423
|
+
...(path === undefined ? {} : { path }),
|
|
424
|
+
...(expectedStatus === undefined ? {} : { expectedStatus }),
|
|
425
|
+
...(host === undefined ? {} : { host }),
|
|
426
|
+
...(command === undefined ? {} : { command }),
|
|
427
|
+
...(args === undefined ? {} : { args }),
|
|
428
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/** Rebuilds `DeclaredService[]` from what a prior successful run saved — the reuse path
|
|
432
|
+
* (build item 3). `envNames` are re-read from THIS process's own environment, never from
|
|
433
|
+
* disk: nothing here ever held a value to begin with. A name with nothing currently set
|
|
434
|
+
* under it is reported back via `missingSavedEnv` rather than silently passing `undefined`
|
|
435
|
+
* through — the caller (`run`) discloses it, per rule 7. */
|
|
436
|
+
function declaredFromSaved(saved, repoPath) {
|
|
437
|
+
const missingSavedEnv = [];
|
|
438
|
+
const declared = Object.entries(saved.services).map(([name, svc]) => {
|
|
439
|
+
const cwd = isAbsolute(svc.cwd) ? svc.cwd : join(repoPath, svc.cwd);
|
|
440
|
+
const env = {};
|
|
441
|
+
for (const key of svc.envNames ?? []) {
|
|
442
|
+
const value = process.env[key];
|
|
443
|
+
if (value === undefined)
|
|
444
|
+
missingSavedEnv.push(`${name}.${key}`);
|
|
445
|
+
else
|
|
446
|
+
env[key] = value;
|
|
447
|
+
}
|
|
448
|
+
const configuration = {
|
|
449
|
+
...(svc.command === undefined ? {} : { command: svc.command }),
|
|
450
|
+
cwd,
|
|
451
|
+
...(svc.port === undefined ? {} : { port: svc.port }),
|
|
452
|
+
...(svc.dependsOn === undefined ? {} : { dependsOn: svc.dependsOn }),
|
|
453
|
+
...(Object.keys(env).length === 0 ? {} : { env }),
|
|
454
|
+
...(svc.attach === undefined ? {} : { attach: svc.attach }),
|
|
455
|
+
};
|
|
456
|
+
return {
|
|
457
|
+
name,
|
|
458
|
+
configuration,
|
|
459
|
+
readiness: readReadiness(svc.readiness, name, cwd),
|
|
460
|
+
attached: svc.attach !== undefined,
|
|
461
|
+
savable: svc,
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
return { declared, missingSavedEnv };
|
|
465
|
+
}
|
|
466
|
+
/** A saved command or attach target, named plainly for a refusal/failure note (build item
|
|
467
|
+
* 4) — never the whole saved shape, which would bury the one fact ("this is what didn't
|
|
468
|
+
* work") a caller needs to read at a glance. */
|
|
469
|
+
function describeSavedCommands(declared) {
|
|
470
|
+
return declared
|
|
471
|
+
.map((service) => service.attached
|
|
472
|
+
? `${service.name}: attach to pid ${String(service.configuration.attach?.pid ?? "?")}`
|
|
473
|
+
: `${service.name}: "${service.configuration.command ?? "?"}"`)
|
|
474
|
+
.join("; ");
|
|
475
|
+
}
|
|
476
|
+
function readServices(args, repoPath, savedFallback) {
|
|
477
|
+
const rawServices = args["services"];
|
|
478
|
+
if (rawServices === undefined) {
|
|
479
|
+
if (savedFallback === undefined || Object.keys(savedFallback.services).length === 0) {
|
|
480
|
+
throw new ToolInputError('"services" must declare at least one service, and no start settings have been saved yet ' +
|
|
481
|
+
"for this repository. Declare it once with a run that reaches readiness and this tool " +
|
|
482
|
+
"remembers it — after that, \"services\" can be omitted and the saved settings are " +
|
|
483
|
+
"reused, disclosed on every call that reuses them.");
|
|
484
|
+
}
|
|
485
|
+
const { declared, missingSavedEnv } = declaredFromSaved(savedFallback, repoPath);
|
|
486
|
+
return { declared, usedSaved: true, missingSavedEnv };
|
|
487
|
+
}
|
|
488
|
+
const raw = asRecord(rawServices, "services");
|
|
378
489
|
const names = Object.keys(raw);
|
|
379
490
|
if (names.length === 0)
|
|
380
491
|
throw new ToolInputError('"services" must declare at least one service');
|
|
381
|
-
|
|
492
|
+
const declared = names.map((name) => {
|
|
382
493
|
const entry = asRecord(raw[name], `services.${name}`);
|
|
383
494
|
const command = entry["command"];
|
|
384
495
|
const attachRaw = entry["attach"];
|
|
@@ -465,13 +576,25 @@ function readServices(args, repoPath) {
|
|
|
465
576
|
...(env === undefined ? {} : { env: env }),
|
|
466
577
|
...(attach === undefined ? {} : { attach }),
|
|
467
578
|
};
|
|
579
|
+
const savable = {
|
|
580
|
+
...(typeof command === "string" ? { command } : {}),
|
|
581
|
+
cwd: configuration.cwd,
|
|
582
|
+
...(port === undefined ? {} : { port: port }),
|
|
583
|
+
...(dependsOn === undefined ? {} : { dependsOn: dependsOn }),
|
|
584
|
+
// Names only — see this file's own header and SavedServiceStart's doc. Never `env` itself.
|
|
585
|
+
...(env === undefined ? {} : { envNames: Object.keys(env) }),
|
|
586
|
+
...(attach === undefined ? {} : { attach }),
|
|
587
|
+
readiness: toSavedReadiness(entry["readiness"]),
|
|
588
|
+
};
|
|
468
589
|
return {
|
|
469
590
|
name,
|
|
470
591
|
configuration,
|
|
471
592
|
readiness: readReadiness(entry["readiness"], name, configuration.cwd),
|
|
472
593
|
attached: attach !== undefined,
|
|
594
|
+
savable,
|
|
473
595
|
};
|
|
474
596
|
});
|
|
597
|
+
return { declared, usedSaved: false, missingSavedEnv: [] };
|
|
475
598
|
}
|
|
476
599
|
/** JSON→ReadinessCheck mapping. `log-pattern`/`custom-hook` can't survive a JSON boundary
|
|
477
600
|
* (one closes over a live process, the other is a function) — absent from the enum and
|
|
@@ -752,10 +875,40 @@ function readJourney(args) {
|
|
|
752
875
|
const settleForMs = optionalInteger(record, "settleForMs", 0);
|
|
753
876
|
return { service, scenarioName, startUrl, recordVideo, headless, settleForMs };
|
|
754
877
|
}
|
|
755
|
-
|
|
878
|
+
function readDatabaseChannel(args) {
|
|
879
|
+
const raw = args["database"];
|
|
880
|
+
if (raw === undefined || raw === null)
|
|
881
|
+
return undefined;
|
|
882
|
+
const record = asRecord(raw, "database");
|
|
883
|
+
const statementLogRaw = record["statementLog"];
|
|
884
|
+
const statementLogRecord = asRecord(statementLogRaw, "database.statementLog");
|
|
885
|
+
const kind = statementLogRecord["kind"];
|
|
886
|
+
const service = optionalString(record, "service");
|
|
887
|
+
if (kind === "file") {
|
|
888
|
+
const path = statementLogRecord["path"];
|
|
889
|
+
if (typeof path !== "string" || path === "") {
|
|
890
|
+
throw new ToolInputError('"database.statementLog.path" is required and must be a non-empty string for kind "file"');
|
|
891
|
+
}
|
|
892
|
+
// A-F6: same narrower, name-based check `attach.logFilePath` uses — a real statement log
|
|
893
|
+
// lives anywhere and can't be root-confined.
|
|
894
|
+
const sensitivity = checkNotSensitivePath(path, ALLOW_SENSITIVE_LOG_PATH_ENV_VAR);
|
|
895
|
+
if (sensitivity.sensitive)
|
|
896
|
+
throw new ToolInputError(`"database.statementLog.path": ${sensitivity.reason}`);
|
|
897
|
+
return { statementLog: { kind: "file", path }, service };
|
|
898
|
+
}
|
|
899
|
+
if (kind === "dockerContainer") {
|
|
900
|
+
const container = statementLogRecord["container"];
|
|
901
|
+
if (typeof container !== "string" || container === "") {
|
|
902
|
+
throw new ToolInputError('"database.statementLog.container" is required and must be a non-empty string for kind "dockerContainer"');
|
|
903
|
+
}
|
|
904
|
+
return { statementLog: { kind: "dockerContainer", container }, service };
|
|
905
|
+
}
|
|
906
|
+
throw new ToolInputError('"database.statementLog.kind" must be "file" or "dockerContainer"');
|
|
907
|
+
}
|
|
908
|
+
export function readArguments(args, repoPath, savedStart) {
|
|
756
909
|
const profile = readProfile(args);
|
|
757
910
|
const adapterSpec = readAdapterSpec(args);
|
|
758
|
-
const declared = readServices(args, repoPath);
|
|
911
|
+
const { declared, usedSaved: usedSavedServices, missingSavedEnv } = readServices(args, repoPath, savedStart);
|
|
759
912
|
const observeForMs = optionalInteger(args, "observeForMs", 1) ?? DEFAULT_OBSERVE_MS;
|
|
760
913
|
const timeoutMs = optionalInteger(args, "timeoutMs", 1) ?? DEFAULT_TIMEOUT_MS;
|
|
761
914
|
const fidelityRaw = optionalInteger(args, "fidelityLevel", 1) ?? 2;
|
|
@@ -783,10 +936,13 @@ export function readArguments(args, repoPath) {
|
|
|
783
936
|
const networkPolicy = readNetworkPolicy(args);
|
|
784
937
|
const sandboxBackend = readSandboxBackend(args);
|
|
785
938
|
const journey = readJourney(args);
|
|
939
|
+
const database = readDatabaseChannel(args);
|
|
786
940
|
return {
|
|
787
941
|
profile,
|
|
788
942
|
adapterSpec,
|
|
789
943
|
declared,
|
|
944
|
+
usedSavedServices,
|
|
945
|
+
missingSavedEnv,
|
|
790
946
|
observeForMs,
|
|
791
947
|
timeoutMs,
|
|
792
948
|
fidelityRaw,
|
|
@@ -797,16 +953,147 @@ export function readArguments(args, repoPath) {
|
|
|
797
953
|
networkPolicy,
|
|
798
954
|
sandboxBackend,
|
|
799
955
|
journey,
|
|
956
|
+
database,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* RG-5(a) — every service this tool spawns gets whatever database-observation
|
|
961
|
+
* launch its own adapter declares, injected automatically with no caller
|
|
962
|
+
* declaration needed. Reads `RuntimeAdapter.databaseObservationLaunch()`
|
|
963
|
+
* (`@descryy/runtime-backend-observation`) — the same language-neutral seam
|
|
964
|
+
* `outboundHttpLaunch()` already established for outbound-fetch
|
|
965
|
+
* instrumentation — rather than comparing `adapter.language` against a name:
|
|
966
|
+
* this file sits above the Canonical IR, where deciding behaviour from which
|
|
967
|
+
* language an adapter is for is the exact leak architecture principle 7
|
|
968
|
+
* forbids (enforced here by `eslint-plugin-descry-boundary`'s
|
|
969
|
+
* `no-language-vocabulary`, which is what caught the first draft of this
|
|
970
|
+
* function). The per-language answer — Node gets `node:sqlite`'s preload,
|
|
971
|
+
* most runtimes get nothing yet — lives in each adapter package instead.
|
|
972
|
+
*
|
|
973
|
+
* Attach-mode services (no `command`) are untouched — Descry did not spawn
|
|
974
|
+
* them, so there is no command line to inject into.
|
|
975
|
+
*/
|
|
976
|
+
function withDatabaseObservationPreload(service, adapter) {
|
|
977
|
+
if (service.command === undefined)
|
|
978
|
+
return service;
|
|
979
|
+
const launch = adapter.databaseObservationLaunch?.();
|
|
980
|
+
if (launch === undefined || launch === null)
|
|
981
|
+
return service;
|
|
982
|
+
return {
|
|
983
|
+
...service,
|
|
984
|
+
...(launch.interpreterArgs === undefined ? {} : { interpreterArgs: [...(service.interpreterArgs ?? []), ...launch.interpreterArgs] }),
|
|
985
|
+
...(launch.env === undefined ? {} : { env: { ...service.env, ...launch.env } }),
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* RG-5(a) read-back half — desktop's `observeDatabaseQueries`, same shape:
|
|
990
|
+
* replays each service's own already-captured output (the preload's
|
|
991
|
+
* `DESCRY_DB_QUERY` marker lines) through `createDatabaseQueryCollector` and
|
|
992
|
+
* writes real `DATABASE_QUERY` evidence into this run's store. Returns how
|
|
993
|
+
* many records it wrote, so the caller can fold it into one disclosed count
|
|
994
|
+
* alongside the Postgres channel's.
|
|
995
|
+
*/
|
|
996
|
+
async function observeSqliteQueries(store, executionId, configuration, execution) {
|
|
997
|
+
const serviceOutput = capturedServiceOutput(execution);
|
|
998
|
+
if (serviceOutput === null)
|
|
999
|
+
return 0;
|
|
1000
|
+
let written = 0;
|
|
1001
|
+
await Promise.all(Object.entries(serviceOutput).map(async ([serviceName, output]) => {
|
|
1002
|
+
const source = {
|
|
1003
|
+
processId: `${executionId}:${serviceName}`,
|
|
1004
|
+
serviceName,
|
|
1005
|
+
lines: (async function* () {
|
|
1006
|
+
for (const text of output.split("\n")) {
|
|
1007
|
+
if (text.length === 0)
|
|
1008
|
+
continue;
|
|
1009
|
+
yield { text, stream: "combined", observedAt: new Date().toISOString() };
|
|
1010
|
+
}
|
|
1011
|
+
})(),
|
|
1012
|
+
};
|
|
1013
|
+
const context = {
|
|
1014
|
+
executionId,
|
|
1015
|
+
configuration,
|
|
1016
|
+
emit: (evidenceInput) => {
|
|
1017
|
+
store.write({ ...evidenceInput, executionId });
|
|
1018
|
+
written += 1;
|
|
1019
|
+
},
|
|
1020
|
+
};
|
|
1021
|
+
const collector = createDatabaseQueryCollector({ source, service: serviceName });
|
|
1022
|
+
await collector.start(context);
|
|
1023
|
+
await collector.stop();
|
|
1024
|
+
}));
|
|
1025
|
+
return written;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* RG-5(c) — the Postgres statement-log channel's own start/stop pair, same
|
|
1029
|
+
* design as the desktop's `beginPostgresChannel`: started before
|
|
1030
|
+
* `runInstrumentedExecution` (the server keeps running independently of
|
|
1031
|
+
* whatever this tool spawns or tears down, so the only way to capture
|
|
1032
|
+
* queries issued *during* the observation window is to already be tailing
|
|
1033
|
+
* when they happen), buffered until a real `executionId` exists to attribute
|
|
1034
|
+
* it to, and idempotent to `stop()` because the caller's own `finally` may
|
|
1035
|
+
* call it a second time after an already-successful stop.
|
|
1036
|
+
*/
|
|
1037
|
+
function beginPostgresChannel(database, processId) {
|
|
1038
|
+
if (database === undefined)
|
|
1039
|
+
return { kind: "not-declared" };
|
|
1040
|
+
const captured = [];
|
|
1041
|
+
const controller = new AbortController();
|
|
1042
|
+
const serviceName = database.service ?? "postgres";
|
|
1043
|
+
const source = createPostgresLogSource(database.statementLog, processId, serviceName, { signal: controller.signal });
|
|
1044
|
+
const collector = createPostgresQueryCollector({ source, service: serviceName });
|
|
1045
|
+
const context = {
|
|
1046
|
+
executionId: processId,
|
|
1047
|
+
configuration: { environmentTier: "tier-2-container", fidelityLevel: 2, timeoutMs: 0, services: {} },
|
|
1048
|
+
emit: (evidence) => {
|
|
1049
|
+
captured.push(evidence);
|
|
1050
|
+
},
|
|
1051
|
+
};
|
|
1052
|
+
const started = collector.start(context);
|
|
1053
|
+
let stopped;
|
|
1054
|
+
return {
|
|
1055
|
+
kind: "observing",
|
|
1056
|
+
stop: () => {
|
|
1057
|
+
if (stopped === undefined) {
|
|
1058
|
+
stopped = (async () => {
|
|
1059
|
+
await started;
|
|
1060
|
+
controller.abort();
|
|
1061
|
+
await collector.stop();
|
|
1062
|
+
return captured;
|
|
1063
|
+
})();
|
|
1064
|
+
}
|
|
1065
|
+
return stopped;
|
|
1066
|
+
},
|
|
800
1067
|
};
|
|
801
1068
|
}
|
|
802
1069
|
/** Exported so `observe.ts` can dispatch the "runtime" verb to this exact body — the merge
|
|
803
1070
|
* reuses this function rather than reimplementing it (mcp-surface-consolidation.md). */
|
|
804
1071
|
export async function run(args, ctx) {
|
|
805
1072
|
const session = ctx.session;
|
|
806
|
-
const { profile, adapterSpec, declared, observeForMs, timeoutMs, fidelityRaw, environmentTier, evidencePath, resourceLimits, filesystemPolicy, networkPolicy, sandboxBackend, journey, } = readArguments(args, session.repoPath);
|
|
1073
|
+
const { profile, adapterSpec, declared, usedSavedServices, missingSavedEnv, observeForMs, timeoutMs, fidelityRaw, environmentTier, evidencePath, resourceLimits, filesystemPolicy, networkPolicy, sandboxBackend, journey, database, } = readArguments(args, session.repoPath, session.config.savedStart);
|
|
807
1074
|
const notes = [...STANDING_NOTES, sandboxDisclosure({ resourceLimits, filesystemPolicy, networkPolicy, sandboxBackend })];
|
|
1075
|
+
// RG-5(c). Stated on every call, success or refusal, same reasoning as the
|
|
1076
|
+
// sandbox disclosure above: a caller unaware the Postgres channel needs an
|
|
1077
|
+
// explicit statement-log source would misread a "channel absent" run as
|
|
1078
|
+
// "this application makes no database calls."
|
|
1079
|
+
notes.push(database === undefined
|
|
1080
|
+
? "database channel: not observed — no statement log configured. Declare \"database.statementLog\" " +
|
|
1081
|
+
"(a file path, or a Docker container's logs) to capture real Postgres queries issued during this " +
|
|
1082
|
+
"run. Node services using node:sqlite are observed automatically, with no declaration needed."
|
|
1083
|
+
: `database channel: tailing the Postgres statement log via ${database.statementLog.kind === "file" ? `file "${database.statementLog.path}"` : `docker container "${database.statementLog.container}"`}.`);
|
|
808
1084
|
// Added on refusal paths too — telling the caller only on success means telling them
|
|
809
|
-
// after the run whose result it would have explained.
|
|
1085
|
+
// after the run whose result it would have explained (same reasoning as the line above).
|
|
1086
|
+
if (usedSavedServices) {
|
|
1087
|
+
const savedAt = session.config.savedStart?.savedAt ?? "an earlier run";
|
|
1088
|
+
notes.push(`No "services" was supplied on this call, so it reused the start settings a prior successful run ` +
|
|
1089
|
+
`saved (${savedAt}): ${describeSavedCommands(declared)}. Declare "services" explicitly to override ` +
|
|
1090
|
+
"them — only another run that reaches readiness replaces what is saved.");
|
|
1091
|
+
if (missingSavedEnv.length > 0) {
|
|
1092
|
+
notes.push(`These environment variables were declared on the run that saved these settings, but nothing is ` +
|
|
1093
|
+
`set under their names in this process's environment now, so they were not passed this time: ` +
|
|
1094
|
+
`${missingSavedEnv.join(", ")}. Only variable names are ever saved, never their values.`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
810
1097
|
if (declared.some((service) => service.attached)) {
|
|
811
1098
|
notes.push(ATTACH_BUFFERING_NOTE, ATTACH_IDENTITY_NOTE);
|
|
812
1099
|
}
|
|
@@ -833,6 +1120,7 @@ export async function run(args, ctx) {
|
|
|
833
1120
|
correlation: null,
|
|
834
1121
|
wrote: EMPTY_WRITE,
|
|
835
1122
|
journey: null,
|
|
1123
|
+
database: null,
|
|
836
1124
|
...data,
|
|
837
1125
|
},
|
|
838
1126
|
});
|
|
@@ -884,7 +1172,7 @@ export async function run(args, ctx) {
|
|
|
884
1172
|
const services = {};
|
|
885
1173
|
const readiness = {};
|
|
886
1174
|
for (const service of declared) {
|
|
887
|
-
services[service.name] = service.configuration;
|
|
1175
|
+
services[service.name] = withDatabaseObservationPreload(service.configuration, adapter);
|
|
888
1176
|
readiness[service.name] = service.readiness;
|
|
889
1177
|
}
|
|
890
1178
|
const configuration = {
|
|
@@ -971,6 +1259,13 @@ export async function run(args, ctx) {
|
|
|
971
1259
|
// orchestrator — it would reject the whole execution and discard every piece of backend
|
|
972
1260
|
// evidence already collected — so the drive itself never throws and this is a second belt.
|
|
973
1261
|
let journeyOutcome;
|
|
1262
|
+
// RG-5(c). Started before `runInstrumentedExecution`, not after: a Postgres
|
|
1263
|
+
// statement log is the server's own, and the server keeps running
|
|
1264
|
+
// independently of whatever this run spawns or tears down — the only way
|
|
1265
|
+
// to capture queries issued *during* this run's observation window is to
|
|
1266
|
+
// already be tailing when they happen (same reasoning as the journey drive
|
|
1267
|
+
// being wired through `onReady` rather than run after the fact).
|
|
1268
|
+
const postgresChannel = beginPostgresChannel(database, `postgres:${root.repo}`);
|
|
974
1269
|
try {
|
|
975
1270
|
ctx.progress(`Running ${declared.length} service(s), observing for ${String(observeForMs)}ms`);
|
|
976
1271
|
const execution = await runInstrumentedExecution({
|
|
@@ -1038,7 +1333,11 @@ export async function run(args, ctx) {
|
|
|
1038
1333
|
// The kill above doesn't shorten the window — this is where a cancelled run actually
|
|
1039
1334
|
// stops. Nothing is correlated or written: an R4 edge can't be re-derived and checked
|
|
1040
1335
|
// later, so minting one from a run nobody watched to the end would be unquestionable.
|
|
1336
|
+
// RG-5(c): the postgres tail is stopped here too (releasing its child process) but its
|
|
1337
|
+
// capture is discarded, same "nothing correlated or written" rule as everything else.
|
|
1041
1338
|
if (ctx.signal.aborted) {
|
|
1339
|
+
if (postgresChannel.kind === "observing")
|
|
1340
|
+
await postgresChannel.stop().catch(() => undefined);
|
|
1042
1341
|
return refuse(cancellationHeadline("the observed application was stopped"), {
|
|
1043
1342
|
executionId: execution.execution.executionId,
|
|
1044
1343
|
executionState: execution.execution.state,
|
|
@@ -1050,6 +1349,28 @@ export async function run(args, ctx) {
|
|
|
1050
1349
|
"left alone, because this call did not start them.", "No evidence was correlated and no edge was written. Evidence already collected is still on disk " +
|
|
1051
1350
|
`at ${evidencePath}, so nothing witnessed was thrown away.`));
|
|
1052
1351
|
}
|
|
1352
|
+
// RG-5(a)/(c) — stop the postgres tail now that the observation window has closed and
|
|
1353
|
+
// replay the sqlite preload's captured output, attributing both to this run's real
|
|
1354
|
+
// executionId (unknown until now, which is why the postgres collector above buffered
|
|
1355
|
+
// rather than wrote directly). Must happen before correlation, same ordering the desktop
|
|
1356
|
+
// uses, so a database-node resolver can attribute what is already in the store.
|
|
1357
|
+
const executionId = execution.execution.executionId;
|
|
1358
|
+
let databaseRecordCount = 0;
|
|
1359
|
+
if (postgresChannel.kind === "observing") {
|
|
1360
|
+
const capturedPostgres = await postgresChannel.stop();
|
|
1361
|
+
for (const evidence of capturedPostgres) {
|
|
1362
|
+
evidenceStore.write({ ...evidence, executionId });
|
|
1363
|
+
databaseRecordCount += 1;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
databaseRecordCount += await observeSqliteQueries(evidenceStore, executionId, configuration, execution);
|
|
1367
|
+
const databaseObservation = database === undefined
|
|
1368
|
+
? {
|
|
1369
|
+
observed: false,
|
|
1370
|
+
disclosure: "database channel: not observed — no statement log configured",
|
|
1371
|
+
recordCount: databaseRecordCount,
|
|
1372
|
+
}
|
|
1373
|
+
: { observed: true, disclosure: null, recordCount: databaseRecordCount };
|
|
1053
1374
|
const observed = describeServices(execution.execution.processes, declared);
|
|
1054
1375
|
const evidenceByType = tally(execution.evidence);
|
|
1055
1376
|
// RG-4 — `execution.evidence` is `runInstrumentedExecution`'s own returned snapshot and
|
|
@@ -1058,7 +1379,12 @@ export async function run(args, ctx) {
|
|
|
1058
1379
|
// the store, scoped to this executionId, only when a journey actually ran — so the
|
|
1059
1380
|
// headline/notes below (which read `execution.evidence` directly, unchanged) still pin
|
|
1060
1381
|
// exactly what they always have on every backend-only run.
|
|
1061
|
-
|
|
1382
|
+
// RG-5(c): also re-read when the database channel wrote anything — same reasoning as the
|
|
1383
|
+
// journey case, a different write path `execution.evidence` was already materialised
|
|
1384
|
+
// before this run's own DATABASE_QUERY records ever reached the store.
|
|
1385
|
+
const evidenceIncludingJourney = journeyOutcome === undefined && databaseRecordCount === 0
|
|
1386
|
+
? execution.evidence
|
|
1387
|
+
: [...evidenceStore.getByExecution(execution.execution.executionId)];
|
|
1062
1388
|
if (execution.validationError !== null) {
|
|
1063
1389
|
// Refused before spawning — a fact about the declaration, not the application;
|
|
1064
1390
|
// emphatically not "the service is clean".
|
|
@@ -1066,9 +1392,17 @@ export async function run(args, ctx) {
|
|
|
1066
1392
|
"evidence was collected, and no graph edge was written.", {
|
|
1067
1393
|
executionState: execution.execution.state,
|
|
1068
1394
|
adapterLanguage: adapter.language,
|
|
1395
|
+
database: databaseObservation,
|
|
1069
1396
|
services: observed,
|
|
1070
1397
|
evidenceByType,
|
|
1071
|
-
}
|
|
1398
|
+
}, usedSavedServices
|
|
1399
|
+
? [
|
|
1400
|
+
`These start settings came from a prior successful run — "services" was omitted on this ` +
|
|
1401
|
+
`call — and they no longer work: ${describeSavedCommands(declared)}. Call observe_runtime ` +
|
|
1402
|
+
'again with "services" declared explicitly to replace what is saved; only another run ' +
|
|
1403
|
+
"that reaches readiness updates it.",
|
|
1404
|
+
]
|
|
1405
|
+
: []);
|
|
1072
1406
|
}
|
|
1073
1407
|
// See deriveRunOutcome for the full rule. Budget-expired case measured pre-clamp: a 1.5s
|
|
1074
1408
|
// budget over a 12s window returned at 12.3s with zero evidence (DEC-NEXT-observe-runtime-
|
|
@@ -1095,6 +1429,11 @@ export async function run(args, ctx) {
|
|
|
1095
1429
|
notes.push(`This run's execution ended in state "${execution.execution.state}", not "COMPLETED" — the ` +
|
|
1096
1430
|
"application did not finish booting successfully. Any evidence below may be partial, or " +
|
|
1097
1431
|
"left over from earlier in this same run, and does not mean the service ever became ready.");
|
|
1432
|
+
if (usedSavedServices) {
|
|
1433
|
+
notes.push(`These start settings came from a prior successful run — "services" was omitted on this call ` +
|
|
1434
|
+
`— and they did not work this time: ${describeSavedCommands(declared)}. Call observe_runtime ` +
|
|
1435
|
+
'again with "services" declared explicitly to replace what is saved.');
|
|
1436
|
+
}
|
|
1098
1437
|
}
|
|
1099
1438
|
ctx.progress(`Correlating ${String(execution.evidence.length)} evidence item(s) against the graph`);
|
|
1100
1439
|
const pass = correlateExecution({
|
|
@@ -1199,6 +1538,34 @@ export async function run(args, ctx) {
|
|
|
1199
1538
|
if (wrote.unjoinedSplitEvidence !== null)
|
|
1200
1539
|
notes.push(wrote.unjoinedSplitEvidence);
|
|
1201
1540
|
notes.push(...wrote.confirmedFactConflict);
|
|
1541
|
+
// Build item 2: save only on a real success, whichever source these services came
|
|
1542
|
+
// from (fresh args or the saved settings this same call may have reused) — an
|
|
1543
|
+
// idempotent overwrite either way. Never on `timedOut` or `failed`: neither
|
|
1544
|
+
// establishes that every declared service actually reached readiness.
|
|
1545
|
+
if (outcome.state === "ok") {
|
|
1546
|
+
const toSave = {};
|
|
1547
|
+
for (const service of declared)
|
|
1548
|
+
toSave[service.name] = service.savable;
|
|
1549
|
+
try {
|
|
1550
|
+
const saved = await writeSavedStart(session.repoPath, toSave);
|
|
1551
|
+
// Written to disk above; `reloadConfig()` (session.ts's own sole mutator of
|
|
1552
|
+
// `session.config`) mirrors it into this live session immediately, so a second
|
|
1553
|
+
// call on this same connection sees it with no explicit reload in between.
|
|
1554
|
+
// Without this, `session.config.savedStart` stays whatever it was when this
|
|
1555
|
+
// session was constructed — stale for the rest of the process's life — and this
|
|
1556
|
+
// same write would also show up as spurious drift on this very reply, since
|
|
1557
|
+
// `checkConfigDrift` (server.ts) diffs on-disk config against `session.config`
|
|
1558
|
+
// right after this tool returns.
|
|
1559
|
+
await session.reloadConfig();
|
|
1560
|
+
notes.push(`This run's services were saved to ${saved.path} (auto-action, disclosed per rule 7 — never a ` +
|
|
1561
|
+
"secret value, since environment variables are saved by name only). A later observe_runtime " +
|
|
1562
|
+
'call on this repository may omit "services" and these settings will be reused.');
|
|
1563
|
+
}
|
|
1564
|
+
catch (error) {
|
|
1565
|
+
notes.push(`This run's start settings could not be saved (${error instanceof Error ? error.message : String(error)}); ` +
|
|
1566
|
+
'a later call must still declare "services" explicitly.');
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1202
1569
|
const headline = failed
|
|
1203
1570
|
? `The run failed to complete: execution ended in state "${execution.execution.state}", not ` +
|
|
1204
1571
|
`"COMPLETED". ${String(execution.evidence.length)} evidence item(s) were collected across ` +
|
|
@@ -1237,14 +1604,21 @@ export async function run(args, ctx) {
|
|
|
1237
1604
|
adapterLanguage: adapter.language,
|
|
1238
1605
|
evidencePath,
|
|
1239
1606
|
services,
|
|
1240
|
-
evidenceByType: journeyOutcome === undefined ? evidenceByType : tally(evidenceIncludingJourney),
|
|
1607
|
+
evidenceByType: journeyOutcome === undefined && databaseRecordCount === 0 ? evidenceByType : tally(evidenceIncludingJourney),
|
|
1241
1608
|
correlation,
|
|
1242
1609
|
wrote,
|
|
1243
1610
|
journey: journeyOutcome ?? null,
|
|
1611
|
+
database: databaseObservation,
|
|
1244
1612
|
},
|
|
1245
1613
|
});
|
|
1246
1614
|
}
|
|
1247
1615
|
finally {
|
|
1616
|
+
// Best-effort: normally already stopped above. Only still "observing" here when something
|
|
1617
|
+
// threw before that point (e.g. runInstrumentedExecution itself), in which case the
|
|
1618
|
+
// captured evidence has no real executionId to attribute to and is discarded — the same
|
|
1619
|
+
// "no fabrication" posture as the cancellation path.
|
|
1620
|
+
if (postgresChannel.kind === "observing")
|
|
1621
|
+
await postgresChannel.stop().catch(() => undefined);
|
|
1248
1622
|
abort.dispose();
|
|
1249
1623
|
evidenceStore.close();
|
|
1250
1624
|
}
|