@lore-co/cli 0.1.16 → 0.1.18
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/README.md +76 -14
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +3 -26
- package/dist/ask.js.map +1 -1
- package/dist/cli.d.ts +8 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +566 -39
- package/dist/cli.js.map +1 -1
- package/dist/context-fallback.d.ts +37 -0
- package/dist/context-fallback.d.ts.map +1 -0
- package/dist/context-fallback.js +259 -0
- package/dist/context-fallback.js.map +1 -0
- package/dist/generated-assets.d.ts +4 -4
- package/dist/generated-assets.d.ts.map +1 -1
- package/dist/generated-assets.js +4 -4
- package/dist/generated-assets.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/invocation-health-writer.d.ts +19 -0
- package/dist/invocation-health-writer.d.ts.map +1 -0
- package/dist/invocation-health-writer.js +131 -0
- package/dist/invocation-health-writer.js.map +1 -0
- package/dist/reliability-store.d.ts +283 -0
- package/dist/reliability-store.d.ts.map +1 -0
- package/dist/reliability-store.js +1913 -0
- package/dist/reliability-store.js.map +1 -0
- package/dist/runtime-version.d.ts +2 -0
- package/dist/runtime-version.d.ts.map +1 -0
- package/dist/runtime-version.js +5 -0
- package/dist/runtime-version.js.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1041 -152
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +1 -1
- package/dist/self-host.js +2 -2
- package/dist/signed-snapshot.d.ts +59 -0
- package/dist/signed-snapshot.d.ts.map +1 -0
- package/dist/signed-snapshot.js +303 -0
- package/dist/signed-snapshot.js.map +1 -0
- package/dist/update.js +3 -3
- package/package.json +19 -3
package/dist/runtime.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { access, chmod, mkdir, readFile,
|
|
3
|
-
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { access, chmod, mkdir, readFile, stat, } from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants, realpathSync } from "node:fs";
|
|
4
4
|
import { execFile as execFileCallback } from "node:child_process";
|
|
5
5
|
import { dirname, parse, relative, resolve } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import { boundedUtf8Text, repositoryScopeFromGitRoot, } from "./repository.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
import { ReliabilityStore, ReliabilityStoreError, atomicWriteJson as durableAtomicWriteJson, classifyRetryFailure, createIntegrationInvocationAttempt, durableUnlink, recordLocalGuardMetric, recordLocalRetrievalMetric, } from "./reliability-store.js";
|
|
11
|
+
import { writeInvocationAttemptBounded, writeInvocationCompletionBounded, } from "./invocation-health-writer.js";
|
|
12
|
+
import { SignedSnapshotError, verifySignedSnapshot, } from "./signed-snapshot.js";
|
|
13
|
+
import { CONTEXT_CACHE_MAX_STALE_MS, isContextTransportFailure, selectCachedContext, } from "./context-fallback.js";
|
|
14
|
+
import { RUNTIME_VERSION } from "./runtime-version.js";
|
|
13
15
|
const IS_STANDALONE_RUNTIME = typeof __LORE_STANDALONE__ === "boolean" && __LORE_STANDALONE__;
|
|
14
16
|
export const COMMAND_HOOK_AGENT_NAMES = [
|
|
15
17
|
"codex",
|
|
@@ -22,11 +24,33 @@ function isCommandHookAgent(value) {
|
|
|
22
24
|
return typeof value === "string" && COMMAND_HOOK_AGENTS.has(value);
|
|
23
25
|
}
|
|
24
26
|
const MAX_STDIN_BYTES = 1024 * 1024;
|
|
27
|
+
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
25
28
|
const MAX_CONTEXT_CHARS = 12_000;
|
|
26
29
|
const PENDING_ASSISTANT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
27
|
-
const MAX_QUEUE_ITEMS = 100;
|
|
28
30
|
const MAX_GIT_DIFF_BYTES = 256 * 1024;
|
|
29
31
|
const MAX_GIT_FILES = 100;
|
|
32
|
+
const RELIABILITY_V1_MEDIA_TYPE = "application/vnd.lore.reliability-v1+json";
|
|
33
|
+
const TRUST_KEY_REFRESH_TTL_MS = 60 * 60_000;
|
|
34
|
+
const TRUST_KEY_REFRESH_STATE_KEY = "snapshot-trust-keys";
|
|
35
|
+
const CONTEXT_NOTICE_COOLDOWN_MS = 15 * 60_000;
|
|
36
|
+
const RELIABILITY_REASON_CODES = new Set([
|
|
37
|
+
"live_success",
|
|
38
|
+
"idempotent_replay",
|
|
39
|
+
"semantic_unavailable",
|
|
40
|
+
"lexical_fallback",
|
|
41
|
+
"live_unavailable",
|
|
42
|
+
"cached_context",
|
|
43
|
+
"cached_policy",
|
|
44
|
+
"no_usable_cache",
|
|
45
|
+
"capture_queued",
|
|
46
|
+
"local_persistence_failed",
|
|
47
|
+
"governed_rule",
|
|
48
|
+
"policy_unavailable",
|
|
49
|
+
"policy_expired",
|
|
50
|
+
"user_override",
|
|
51
|
+
"invalid_response",
|
|
52
|
+
"integration_unsupported",
|
|
53
|
+
]);
|
|
30
54
|
const NON_SECRET_TOKEN_VALUES = new Set([
|
|
31
55
|
"available",
|
|
32
56
|
"configured",
|
|
@@ -81,6 +105,18 @@ export async function readLineageMetadata(home, environment = process.env) {
|
|
|
81
105
|
function isObject(value) {
|
|
82
106
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
83
107
|
}
|
|
108
|
+
function canonicalJson(value) {
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
111
|
+
}
|
|
112
|
+
if (isObject(value)) {
|
|
113
|
+
return `{${Object.keys(value)
|
|
114
|
+
.sort()
|
|
115
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
116
|
+
.join(",")}}`;
|
|
117
|
+
}
|
|
118
|
+
return JSON.stringify(value) ?? "null";
|
|
119
|
+
}
|
|
84
120
|
function stringField(value) {
|
|
85
121
|
return typeof value === "string" && value.trim() !== ""
|
|
86
122
|
? value
|
|
@@ -181,6 +217,48 @@ function normalizeHookInput(input, agent, environment) {
|
|
|
181
217
|
sessionId,
|
|
182
218
|
};
|
|
183
219
|
}
|
|
220
|
+
class InvalidHookInputError extends Error {
|
|
221
|
+
constructor() {
|
|
222
|
+
super("Hook input does not match the configured agent event");
|
|
223
|
+
this.name = "InvalidHookInputError";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function validHookInput(input, agent, eventName) {
|
|
227
|
+
const recognized = agent === "polytoken"
|
|
228
|
+
? eventName === "UserPromptSubmit" || eventName === "AssistantResponse"
|
|
229
|
+
: agent === "cursor"
|
|
230
|
+
? eventName === "UserPromptSubmit" ||
|
|
231
|
+
eventName === "AssistantResponse" ||
|
|
232
|
+
eventName === "PreToolUse" ||
|
|
233
|
+
eventName === "SessionEnd"
|
|
234
|
+
: eventName === "UserPromptSubmit" ||
|
|
235
|
+
eventName === "PreToolUse" ||
|
|
236
|
+
eventName === "Stop" ||
|
|
237
|
+
eventName === "SessionEnd";
|
|
238
|
+
if (!recognized) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (eventName === "UserPromptSubmit") {
|
|
242
|
+
return stringField(input.prompt) !== undefined;
|
|
243
|
+
}
|
|
244
|
+
if (eventName === "Stop" || eventName === "AssistantResponse") {
|
|
245
|
+
return stringField(input.last_assistant_message) !== undefined;
|
|
246
|
+
}
|
|
247
|
+
if (eventName === "PreToolUse") {
|
|
248
|
+
const toolName = stringField(input.tool_name);
|
|
249
|
+
if (toolName === undefined || !isObject(input.tool_input)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
if (GUARD_EDIT_TOOLS.has(toolName)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
if (GUARD_SHELL_TOOLS.has(toolName)) {
|
|
256
|
+
return stringField(input.tool_input.command) !== undefined;
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
184
262
|
function sha256(value) {
|
|
185
263
|
return createHash("sha256").update(value).digest("hex");
|
|
186
264
|
}
|
|
@@ -227,22 +305,16 @@ export function redactSecrets(text) {
|
|
|
227
305
|
})
|
|
228
306
|
.replace(/\b((?:set-)?cookie\s*:\s*)[^\r\n]+/giu, (_match, prefix) => `${prefix}[REDACTED:AUTHORIZATION]`);
|
|
229
307
|
}
|
|
230
|
-
async function atomicWriteJson(path, value
|
|
308
|
+
async function atomicWriteJson(path, value) {
|
|
231
309
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
232
|
-
|
|
233
|
-
await
|
|
234
|
-
encoding: "utf8",
|
|
235
|
-
mode,
|
|
236
|
-
flag: "wx",
|
|
237
|
-
});
|
|
238
|
-
await rename(temporaryPath, path);
|
|
239
|
-
await chmod(path, mode);
|
|
310
|
+
await chmod(dirname(path), 0o700);
|
|
311
|
+
await durableAtomicWriteJson(path, value);
|
|
240
312
|
}
|
|
241
313
|
async function readRuntimeConfig(home) {
|
|
242
314
|
try {
|
|
243
315
|
const parsed = JSON.parse(await readFile(resolve(loreDirectory(home), "config.json"), "utf8"));
|
|
244
316
|
if (!isObject(parsed) ||
|
|
245
|
-
parsed.version !== 1 ||
|
|
317
|
+
(parsed.version !== 1 && parsed.version !== 2) ||
|
|
246
318
|
typeof parsed.apiUrl !== "string" ||
|
|
247
319
|
typeof parsed.token !== "string" ||
|
|
248
320
|
!Array.isArray(parsed.agents)) {
|
|
@@ -259,10 +331,15 @@ async function readRuntimeConfig(home) {
|
|
|
259
331
|
/^https?:\/\//u.test(parsed.dashboardUrl)
|
|
260
332
|
? parsed.dashboardUrl.replace(/\/+$/u, "")
|
|
261
333
|
: undefined;
|
|
334
|
+
const workspaceId = typeof parsed.workspaceId === "string" &&
|
|
335
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(parsed.workspaceId)
|
|
336
|
+
? parsed.workspaceId
|
|
337
|
+
: undefined;
|
|
262
338
|
return {
|
|
263
|
-
version:
|
|
339
|
+
version: parsed.version,
|
|
264
340
|
apiUrl: parsed.apiUrl,
|
|
265
341
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
342
|
+
...(workspaceId === undefined ? {} : { workspaceId }),
|
|
266
343
|
token: parsed.token,
|
|
267
344
|
agents,
|
|
268
345
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
@@ -291,11 +368,10 @@ async function savePending(input, agent, sessionId, now, home) {
|
|
|
291
368
|
capturedAt: now.toISOString(),
|
|
292
369
|
});
|
|
293
370
|
}
|
|
294
|
-
async function
|
|
371
|
+
async function readPending(agent, sessionId, home) {
|
|
295
372
|
const path = pendingPath(agent, sessionId, home);
|
|
296
373
|
try {
|
|
297
374
|
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
298
|
-
await rm(path, { force: true });
|
|
299
375
|
if (!isObject(parsed) ||
|
|
300
376
|
parsed.agent !== agent ||
|
|
301
377
|
parsed.sessionId !== sessionId ||
|
|
@@ -323,6 +399,9 @@ async function consumePending(agent, sessionId, home) {
|
|
|
323
399
|
return null;
|
|
324
400
|
}
|
|
325
401
|
}
|
|
402
|
+
async function clearPending(agent, sessionId, home) {
|
|
403
|
+
await durableUnlink(pendingPath(agent, sessionId, home), true);
|
|
404
|
+
}
|
|
326
405
|
function normalizedRepositoryPath(value) {
|
|
327
406
|
const parts = [];
|
|
328
407
|
for (const part of value.replaceAll("\\", "/").split("/")) {
|
|
@@ -539,6 +618,50 @@ function observationsUrl(apiUrl) {
|
|
|
539
618
|
function contextDeliveriesUrl(apiUrl) {
|
|
540
619
|
return `${apiUrl.replace(/\/+$/u, "")}/v1/context/deliveries`;
|
|
541
620
|
}
|
|
621
|
+
class RuntimeHttpError extends Error {
|
|
622
|
+
httpStatus;
|
|
623
|
+
constructor(message, httpStatus) {
|
|
624
|
+
super(message);
|
|
625
|
+
this.name = "RuntimeHttpError";
|
|
626
|
+
this.httpStatus = httpStatus;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async function boundedResponseJson(response) {
|
|
630
|
+
const declaredLength = response.headers.get("content-length");
|
|
631
|
+
if (declaredLength !== null &&
|
|
632
|
+
Number.isFinite(Number(declaredLength)) &&
|
|
633
|
+
Number(declaredLength) > MAX_RESPONSE_BYTES) {
|
|
634
|
+
await response.body?.cancel();
|
|
635
|
+
throw new Error("Lore response exceeds the reliability size limit");
|
|
636
|
+
}
|
|
637
|
+
if (response.body === null) {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
const reader = response.body.getReader();
|
|
641
|
+
const chunks = [];
|
|
642
|
+
let bytes = 0;
|
|
643
|
+
try {
|
|
644
|
+
for (;;) {
|
|
645
|
+
const chunk = await reader.read();
|
|
646
|
+
if (chunk.done) {
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
bytes += chunk.value.byteLength;
|
|
650
|
+
if (bytes > MAX_RESPONSE_BYTES) {
|
|
651
|
+
await reader.cancel();
|
|
652
|
+
throw new Error("Lore response exceeds the reliability size limit");
|
|
653
|
+
}
|
|
654
|
+
chunks.push(chunk.value);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
reader.releaseLock();
|
|
659
|
+
}
|
|
660
|
+
if (bytes === 0) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
return JSON.parse(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)), bytes).toString("utf8"));
|
|
664
|
+
}
|
|
542
665
|
function boundedContext(value) {
|
|
543
666
|
return Array.from(value.trim()).slice(0, MAX_CONTEXT_CHARS).join("");
|
|
544
667
|
}
|
|
@@ -638,11 +761,276 @@ function deliveryFromResponse(value) {
|
|
|
638
761
|
delivered,
|
|
639
762
|
};
|
|
640
763
|
}
|
|
641
|
-
|
|
764
|
+
class IncompatibleReliabilityError extends Error {
|
|
765
|
+
incompatible = true;
|
|
766
|
+
}
|
|
767
|
+
async function protocolResponseJson(response) {
|
|
768
|
+
try {
|
|
769
|
+
return await boundedResponseJson(response);
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
if (isContextTransportFailure(error)) {
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
775
|
+
throw new IncompatibleReliabilityError("Lore response is not valid bounded JSON");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function deliveryFromSignedContext(payload, expectedRequest) {
|
|
779
|
+
const request = isObject(payload.request) ? payload.request : null;
|
|
780
|
+
if (request === null ||
|
|
781
|
+
canonicalJson(request) !== canonicalJson(expectedRequest) ||
|
|
782
|
+
typeof payload.eventId !== "string" ||
|
|
783
|
+
typeof payload.receiptId !== "string" ||
|
|
784
|
+
typeof payload.context !== "string" ||
|
|
785
|
+
!Array.isArray(payload.memories) ||
|
|
786
|
+
!Array.isArray(payload.hits) ||
|
|
787
|
+
!isObject(payload.packing)) {
|
|
788
|
+
throw new IncompatibleReliabilityError("Lore context snapshot does not bind the request or signed delivery");
|
|
789
|
+
}
|
|
790
|
+
return deliveryFromResponse({
|
|
791
|
+
context: payload.context,
|
|
792
|
+
receipt: { id: payload.receiptId },
|
|
793
|
+
memories: payload.memories,
|
|
794
|
+
hits: payload.hits,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
function retrievalReliability(value, payload) {
|
|
798
|
+
const metadata = isObject(value) ? value : null;
|
|
799
|
+
const freshness = isObject(metadata?.freshness)
|
|
800
|
+
? metadata.freshness
|
|
801
|
+
: null;
|
|
802
|
+
const policy = isObject(metadata?.policy) ? metadata.policy : null;
|
|
803
|
+
const reasons = Array.isArray(metadata?.reasons)
|
|
804
|
+
? metadata.reasons.filter((reason) => typeof reason === "string")
|
|
805
|
+
: [];
|
|
806
|
+
const policySource = policy?.source;
|
|
807
|
+
const policyLoadedAt = policy?.loadedAt;
|
|
808
|
+
const policyValidUntil = policy?.validUntil;
|
|
809
|
+
if (metadata === null ||
|
|
810
|
+
metadata.contractVersion !== "reliability-v1" ||
|
|
811
|
+
Object.keys(metadata).length !== 9 ||
|
|
812
|
+
metadata.operation !== "retrieval" ||
|
|
813
|
+
!(metadata.requestId === null ||
|
|
814
|
+
typeof metadata.requestId === "string") ||
|
|
815
|
+
!["ok", "degraded", "failed"].includes(String(metadata.status)) ||
|
|
816
|
+
!["live", "cache", "none"].includes(String(metadata.source)) ||
|
|
817
|
+
!["none", "live_lexical", "cached_context"].includes(String(metadata.fallback)) ||
|
|
818
|
+
freshness === null ||
|
|
819
|
+
Object.keys(freshness).length !== 4 ||
|
|
820
|
+
!["fresh", "stale", "unknown"].includes(String(freshness.state)) ||
|
|
821
|
+
typeof freshness.asOf !== "string" ||
|
|
822
|
+
freshness.asOf !== payload.asOf ||
|
|
823
|
+
typeof freshness.ageMs !== "number" ||
|
|
824
|
+
!Number.isSafeInteger(freshness.ageMs) ||
|
|
825
|
+
freshness.ageMs < 0 ||
|
|
826
|
+
typeof freshness.validUntil !== "string" ||
|
|
827
|
+
freshness.validUntil !== payload.validUntil ||
|
|
828
|
+
policy === null ||
|
|
829
|
+
Object.keys(policy).length !== 4 ||
|
|
830
|
+
!["live", "cache", "none"].includes(String(policySource)) ||
|
|
831
|
+
(policy.version !== null && typeof policy.version !== "string") ||
|
|
832
|
+
(policy.version !== null && policy.version !== payload.policyVersion) ||
|
|
833
|
+
!((policySource === "none" &&
|
|
834
|
+
policy.version === null &&
|
|
835
|
+
policyLoadedAt === null &&
|
|
836
|
+
policyValidUntil === null) ||
|
|
837
|
+
(["live", "cache"].includes(String(policySource)) &&
|
|
838
|
+
typeof policy.version === "string" &&
|
|
839
|
+
typeof policyLoadedAt === "string" &&
|
|
840
|
+
Number.isFinite(Date.parse(policyLoadedAt)) &&
|
|
841
|
+
(policyValidUntil === null ||
|
|
842
|
+
(typeof policyValidUntil === "string" &&
|
|
843
|
+
Number.isFinite(Date.parse(policyValidUntil)))) &&
|
|
844
|
+
policyValidUntil === payload.validUntil &&
|
|
845
|
+
policyLoadedAt ===
|
|
846
|
+
(policySource === "cache" ? payload.asOf : payload.issuedAt))) ||
|
|
847
|
+
!Array.isArray(metadata.reasons) ||
|
|
848
|
+
reasons.length === 0 ||
|
|
849
|
+
reasons.length > 8 ||
|
|
850
|
+
reasons.length !== metadata.reasons.length ||
|
|
851
|
+
new Set(reasons).size !== reasons.length ||
|
|
852
|
+
reasons.some((reason) => !RELIABILITY_REASON_CODES.has(reason)) ||
|
|
853
|
+
(metadata.requestId !== null &&
|
|
854
|
+
metadata.requestId !== payload.requestId) ||
|
|
855
|
+
(metadata.source === "live" && freshness.state !== "fresh") ||
|
|
856
|
+
(metadata.source === "live" && policySource !== "live") ||
|
|
857
|
+
(metadata.source === "cache" &&
|
|
858
|
+
(metadata.fallback !== "cached_context" ||
|
|
859
|
+
policySource !== "cache" ||
|
|
860
|
+
typeof policyValidUntil !== "string" ||
|
|
861
|
+
!["fresh", "stale"].includes(String(freshness.state)))) ||
|
|
862
|
+
(metadata.fallback === "live_lexical" &&
|
|
863
|
+
(metadata.source !== "live" ||
|
|
864
|
+
metadata.status !== "degraded" ||
|
|
865
|
+
!reasons.includes("lexical_fallback"))) ||
|
|
866
|
+
(metadata.fallback === "cached_context" &&
|
|
867
|
+
!reasons.includes("cached_context")) ||
|
|
868
|
+
(metadata.status === "ok" && metadata.fallback !== "none")
|
|
869
|
+
|| (metadata.status === "degraded" && metadata.fallback === "none")
|
|
870
|
+
|| (metadata.status === "failed" &&
|
|
871
|
+
(metadata.source !== "none" || metadata.fallback !== "none"))) {
|
|
872
|
+
throw new IncompatibleReliabilityError("Lore retrieval reliability metadata is invalid");
|
|
873
|
+
}
|
|
874
|
+
const cached = metadata.source === "cache";
|
|
875
|
+
const nowMs = Date.now();
|
|
876
|
+
return {
|
|
877
|
+
status: metadata.status,
|
|
878
|
+
source: metadata.source,
|
|
879
|
+
fallback: metadata.fallback,
|
|
880
|
+
reasons,
|
|
881
|
+
freshness: {
|
|
882
|
+
state: cached
|
|
883
|
+
? nowMs < Date.parse(payload.validUntil)
|
|
884
|
+
? "fresh"
|
|
885
|
+
: "stale"
|
|
886
|
+
: freshness.state,
|
|
887
|
+
asOf: payload.asOf,
|
|
888
|
+
ageMs: cached
|
|
889
|
+
? Math.max(0, nowMs - Date.parse(payload.asOf))
|
|
890
|
+
: freshness.ageMs,
|
|
891
|
+
validUntil: payload.validUntil,
|
|
892
|
+
},
|
|
893
|
+
policyVersion: typeof policy.version === "string" ? policy.version : null,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function reliabilityWorkspaceKey(config) {
|
|
897
|
+
return (config.workspaceId ??
|
|
898
|
+
credentialReliabilityWorkspaceKey(config));
|
|
899
|
+
}
|
|
900
|
+
function credentialReliabilityWorkspaceKey(config) {
|
|
901
|
+
return `credential-${sha256(`${config.apiUrl}\0${config.token}`).slice(0, 32)}`;
|
|
902
|
+
}
|
|
903
|
+
function runtimeAccept(config) {
|
|
904
|
+
return config.version === 2
|
|
905
|
+
? RELIABILITY_V1_MEDIA_TYPE
|
|
906
|
+
: "application/json";
|
|
907
|
+
}
|
|
908
|
+
async function runtimeStore(config, home) {
|
|
909
|
+
const store = new ReliabilityStore(reliabilityWorkspaceKey(config), {
|
|
910
|
+
...(home === undefined ? {} : { home }),
|
|
911
|
+
});
|
|
912
|
+
await store.initialize();
|
|
913
|
+
await store.migrateLegacyQueue(queueDirectory(home));
|
|
914
|
+
if (config.workspaceId !== undefined) {
|
|
915
|
+
const credentialStore = new ReliabilityStore(credentialReliabilityWorkspaceKey(config), { ...(home === undefined ? {} : { home }) });
|
|
916
|
+
await credentialStore.transferPendingTo(store).catch(() => undefined);
|
|
917
|
+
}
|
|
918
|
+
return store;
|
|
919
|
+
}
|
|
920
|
+
function publicKeySet(value) {
|
|
921
|
+
if (!isObject(value) ||
|
|
922
|
+
typeof value.workspaceId !== "string" ||
|
|
923
|
+
!Array.isArray(value.keys)) {
|
|
924
|
+
throw new IncompatibleReliabilityError("Lore public key response is invalid");
|
|
925
|
+
}
|
|
926
|
+
const keys = value.keys.map((candidate) => {
|
|
927
|
+
if (!isObject(candidate) ||
|
|
928
|
+
typeof candidate.kid !== "string" ||
|
|
929
|
+
candidate.kty !== "OKP" ||
|
|
930
|
+
candidate.crv !== "Ed25519" ||
|
|
931
|
+
candidate.alg !== "EdDSA" ||
|
|
932
|
+
candidate.use !== "sig" ||
|
|
933
|
+
typeof candidate.x !== "string" ||
|
|
934
|
+
typeof candidate.notBefore !== "string" ||
|
|
935
|
+
!(candidate.retiredAt === null ||
|
|
936
|
+
typeof candidate.retiredAt === "string")) {
|
|
937
|
+
throw new IncompatibleReliabilityError("Lore public key response contains an invalid key");
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
workspaceId: value.workspaceId,
|
|
941
|
+
kid: candidate.kid,
|
|
942
|
+
kty: "OKP",
|
|
943
|
+
crv: "Ed25519",
|
|
944
|
+
alg: "EdDSA",
|
|
945
|
+
use: "sig",
|
|
946
|
+
x: candidate.x,
|
|
947
|
+
notBefore: candidate.notBefore,
|
|
948
|
+
retiredAt: candidate.retiredAt,
|
|
949
|
+
};
|
|
950
|
+
});
|
|
951
|
+
if (keys.length === 0) {
|
|
952
|
+
throw new IncompatibleReliabilityError("Lore public key response is empty");
|
|
953
|
+
}
|
|
954
|
+
return { workspaceId: value.workspaceId, keys };
|
|
955
|
+
}
|
|
956
|
+
async function refreshTrustKeys(config, store, fetchImplementation) {
|
|
957
|
+
const response = await fetchImplementation(`${config.apiUrl.replace(/\/+$/u, "")}/v1/workspace/identity/keys`, {
|
|
958
|
+
headers: {
|
|
959
|
+
accept: RELIABILITY_V1_MEDIA_TYPE,
|
|
960
|
+
authorization: `Bearer ${config.token}`,
|
|
961
|
+
"user-agent": `lore-cli/${RUNTIME_VERSION}`,
|
|
962
|
+
},
|
|
963
|
+
signal: AbortSignal.timeout(config.timeoutMs ?? 2_500),
|
|
964
|
+
});
|
|
965
|
+
if (!response.ok) {
|
|
966
|
+
throw new RuntimeHttpError(`Lore key discovery failed with HTTP ${response.status}`, response.status);
|
|
967
|
+
}
|
|
968
|
+
const keys = publicKeySet(await protocolResponseJson(response));
|
|
969
|
+
if (config.workspaceId !== undefined &&
|
|
970
|
+
keys.workspaceId !== config.workspaceId) {
|
|
971
|
+
throw new IncompatibleReliabilityError("Lore public keys belong to a different workspace");
|
|
972
|
+
}
|
|
973
|
+
if (store.workspaceId === keys.workspaceId) {
|
|
974
|
+
await store.writePublicTrustKeys(keys.keys.map((key) => ({
|
|
975
|
+
...key,
|
|
976
|
+
workspaceId: keys.workspaceId,
|
|
977
|
+
})));
|
|
978
|
+
await store.writeState(TRUST_KEY_REFRESH_STATE_KEY, {
|
|
979
|
+
refreshedAt: new Date().toISOString(),
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
return keys;
|
|
983
|
+
}
|
|
984
|
+
async function pinnedKeys(config, store, fetchImplementation) {
|
|
985
|
+
const keys = await store.readPublicTrustKeys();
|
|
986
|
+
const refreshState = await store.readState(TRUST_KEY_REFRESH_STATE_KEY);
|
|
987
|
+
const refreshedAt = isObject(refreshState)
|
|
988
|
+
? refreshState.refreshedAt
|
|
989
|
+
: undefined;
|
|
990
|
+
const refreshedAtMs = typeof refreshedAt === "string" ? Date.parse(refreshedAt) : Number.NaN;
|
|
991
|
+
if (keys.length > 0 &&
|
|
992
|
+
Number.isFinite(refreshedAtMs) &&
|
|
993
|
+
Date.now() - refreshedAtMs < TRUST_KEY_REFRESH_TTL_MS &&
|
|
994
|
+
refreshedAtMs <= Date.now() + 5 * 60_000) {
|
|
995
|
+
return { workspaceId: store.workspaceId, keys };
|
|
996
|
+
}
|
|
997
|
+
return refreshTrustKeys(config, store, fetchImplementation);
|
|
998
|
+
}
|
|
999
|
+
async function verifySnapshot(compactJws, kind, config, store, fetchImplementation, allowExpired = false) {
|
|
1000
|
+
const verifyWith = (keys) => verifySignedSnapshot(compactJws, {
|
|
1001
|
+
workspaceId: keys.workspaceId,
|
|
1002
|
+
kind,
|
|
1003
|
+
keys,
|
|
1004
|
+
allowExpired,
|
|
1005
|
+
}).payload;
|
|
1006
|
+
try {
|
|
1007
|
+
return verifyWith(await pinnedKeys(config, store, fetchImplementation));
|
|
1008
|
+
}
|
|
1009
|
+
catch (error) {
|
|
1010
|
+
if (!(error instanceof SignedSnapshotError) ||
|
|
1011
|
+
error.code !== "UNTRUSTED_KEY") {
|
|
1012
|
+
throw error;
|
|
1013
|
+
}
|
|
1014
|
+
return verifyWith(await refreshTrustKeys(config, store, fetchImplementation));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function committedAcknowledgement(value, externalEventId, serverEventId) {
|
|
1018
|
+
if (!isObject(value) || !isObject(value.acknowledgement)) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
const acknowledgement = value.acknowledgement;
|
|
1022
|
+
return (acknowledgement.state === "committed_server" &&
|
|
1023
|
+
acknowledgement.durability === "workspace_database" &&
|
|
1024
|
+
acknowledgement.replayPending === false &&
|
|
1025
|
+
acknowledgement.eventId === serverEventId &&
|
|
1026
|
+
acknowledgement.idempotencyKey === externalEventId);
|
|
1027
|
+
}
|
|
1028
|
+
async function postTurn(config, request, fetchImplementation, store) {
|
|
642
1029
|
const { idempotencyKey, ...body } = request;
|
|
643
1030
|
const response = await fetchImplementation(turnsUrl(config.apiUrl), {
|
|
644
1031
|
method: "POST",
|
|
645
1032
|
headers: {
|
|
1033
|
+
accept: runtimeAccept(config),
|
|
646
1034
|
authorization: `Bearer ${config.token}`,
|
|
647
1035
|
"content-type": "application/json",
|
|
648
1036
|
"idempotency-key": idempotencyKey,
|
|
@@ -652,18 +1040,70 @@ async function postTurn(config, request, fetchImplementation) {
|
|
|
652
1040
|
signal: AbortSignal.timeout(config.timeoutMs ?? 2_500),
|
|
653
1041
|
});
|
|
654
1042
|
if (!response.ok) {
|
|
655
|
-
throw new
|
|
656
|
-
}
|
|
657
|
-
const
|
|
658
|
-
if (
|
|
659
|
-
return
|
|
1043
|
+
throw new RuntimeHttpError(`Lore turn request failed with HTTP ${response.status}`, response.status);
|
|
1044
|
+
}
|
|
1045
|
+
const value = await protocolResponseJson(response);
|
|
1046
|
+
if (config.version === 1) {
|
|
1047
|
+
return deliveryFromResponse(value);
|
|
1048
|
+
}
|
|
1049
|
+
if (!isObject(value) ||
|
|
1050
|
+
!isObject(value.turn) ||
|
|
1051
|
+
!isObject(value.capture) ||
|
|
1052
|
+
typeof value.contextSnapshot !== "string" ||
|
|
1053
|
+
!isObject(value.turn.event) ||
|
|
1054
|
+
typeof value.turn.event.id !== "string" ||
|
|
1055
|
+
!isObject(value.retrieval) ||
|
|
1056
|
+
!committedAcknowledgement(value.capture, request.eventId, value.turn.event.id)) {
|
|
1057
|
+
throw new IncompatibleReliabilityError("Lore turn response has no matching durable acknowledgement");
|
|
1058
|
+
}
|
|
1059
|
+
const payload = await verifySnapshot(value.contextSnapshot, "context", config, store, fetchImplementation, true);
|
|
1060
|
+
if (payload.eventId !== value.turn.event.id ||
|
|
1061
|
+
!isObject(value.turn.receipt) ||
|
|
1062
|
+
payload.receiptId !== value.turn.receipt.id) {
|
|
1063
|
+
throw new IncompatibleReliabilityError("Lore turn snapshot does not bind the returned event and receipt");
|
|
1064
|
+
}
|
|
1065
|
+
const reliability = retrievalReliability(value.retrieval, payload);
|
|
1066
|
+
if (Date.now() - Date.parse(payload.validUntil) >
|
|
1067
|
+
CONTEXT_CACHE_MAX_STALE_MS) {
|
|
1068
|
+
return {
|
|
1069
|
+
...emptyDelivery(),
|
|
1070
|
+
reliability: {
|
|
1071
|
+
status: "failed",
|
|
1072
|
+
source: "none",
|
|
1073
|
+
fallback: "none",
|
|
1074
|
+
reasons: ["no_usable_cache"],
|
|
1075
|
+
freshness: {
|
|
1076
|
+
state: "unknown",
|
|
1077
|
+
asOf: null,
|
|
1078
|
+
ageMs: null,
|
|
1079
|
+
validUntil: null,
|
|
1080
|
+
},
|
|
1081
|
+
policyVersion: null,
|
|
1082
|
+
},
|
|
1083
|
+
};
|
|
660
1084
|
}
|
|
661
|
-
|
|
1085
|
+
await store
|
|
1086
|
+
.writeContextSnapshot(request.eventId, {
|
|
1087
|
+
compactJws: value.contextSnapshot,
|
|
1088
|
+
})
|
|
1089
|
+
.catch(() => undefined);
|
|
1090
|
+
const delivery = deliveryFromSignedContext(payload, {
|
|
1091
|
+
connector: request.connector,
|
|
1092
|
+
eventId: request.eventId,
|
|
1093
|
+
sessionId: request.sessionId,
|
|
1094
|
+
task: {
|
|
1095
|
+
agent: request.agent,
|
|
1096
|
+
...(request.scope === undefined ? {} : { scope: request.scope }),
|
|
1097
|
+
task: request.currentUser.content,
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
return { ...delivery, reliability };
|
|
662
1101
|
}
|
|
663
|
-
async function postPromptObservation(config, request, fetchImplementation) {
|
|
1102
|
+
async function postPromptObservation(config, request, fetchImplementation, _store) {
|
|
664
1103
|
const response = await fetchImplementation(observationsUrl(config.apiUrl), {
|
|
665
1104
|
method: "POST",
|
|
666
1105
|
headers: {
|
|
1106
|
+
accept: runtimeAccept(config),
|
|
667
1107
|
authorization: `Bearer ${config.token}`,
|
|
668
1108
|
"content-type": "application/json",
|
|
669
1109
|
"idempotency-key": request.idempotencyKey,
|
|
@@ -695,71 +1135,254 @@ async function postPromptObservation(config, request, fetchImplementation) {
|
|
|
695
1135
|
signal: AbortSignal.timeout(config.timeoutMs ?? 2_500),
|
|
696
1136
|
});
|
|
697
1137
|
if (!response.ok) {
|
|
698
|
-
throw new
|
|
1138
|
+
throw new RuntimeHttpError(`Lore prompt observation failed with HTTP ${response.status}`, response.status);
|
|
1139
|
+
}
|
|
1140
|
+
const value = await protocolResponseJson(response);
|
|
1141
|
+
if (config.version === 1) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (!isObject(value) ||
|
|
1145
|
+
!isObject(value.observation) ||
|
|
1146
|
+
!isObject(value.observation.event) ||
|
|
1147
|
+
typeof value.observation.event.id !== "string" ||
|
|
1148
|
+
!committedAcknowledgement(value.capture, request.eventId, value.observation.event.id)) {
|
|
1149
|
+
throw new IncompatibleReliabilityError("Lore observation response has no matching durable acknowledgement");
|
|
699
1150
|
}
|
|
700
|
-
await response.body?.cancel();
|
|
701
1151
|
}
|
|
702
|
-
async function getPromptContext(config, agent, sessionId, sourceEventId, prompt, gitContext, fetchImplementation) {
|
|
1152
|
+
async function getPromptContext(config, agent, sessionId, sourceEventId, prompt, gitContext, fetchImplementation, store) {
|
|
1153
|
+
const deliveryEventId = deterministicUuid(`lore-context\0${sourceEventId}`);
|
|
1154
|
+
const deliveryRequest = {
|
|
1155
|
+
connector: "lore-cli",
|
|
1156
|
+
eventId: deliveryEventId,
|
|
1157
|
+
sessionId,
|
|
1158
|
+
task: {
|
|
1159
|
+
agent,
|
|
1160
|
+
...(gitContext.scope === undefined
|
|
1161
|
+
? {}
|
|
1162
|
+
: { scope: gitContext.scope }),
|
|
1163
|
+
task: redactSecrets(prompt),
|
|
1164
|
+
...(gitContext.diff === undefined ? {} : { diff: gitContext.diff }),
|
|
1165
|
+
...(gitContext.files === undefined
|
|
1166
|
+
? {}
|
|
1167
|
+
: { files: gitContext.files }),
|
|
1168
|
+
},
|
|
1169
|
+
};
|
|
703
1170
|
const response = await fetchImplementation(contextDeliveriesUrl(config.apiUrl), {
|
|
704
1171
|
method: "POST",
|
|
705
1172
|
headers: {
|
|
1173
|
+
accept: runtimeAccept(config),
|
|
706
1174
|
authorization: `Bearer ${config.token}`,
|
|
707
1175
|
"content-type": "application/json",
|
|
708
1176
|
"user-agent": `lore-cli/${RUNTIME_VERSION}`,
|
|
709
1177
|
},
|
|
710
|
-
body: JSON.stringify(
|
|
711
|
-
connector: "lore-cli",
|
|
712
|
-
eventId: deterministicUuid(`lore-context\0${sourceEventId}`),
|
|
713
|
-
sessionId,
|
|
714
|
-
task: {
|
|
715
|
-
agent,
|
|
716
|
-
task: redactSecrets(prompt),
|
|
717
|
-
...(gitContext.scope === undefined
|
|
718
|
-
? {}
|
|
719
|
-
: { scope: gitContext.scope }),
|
|
720
|
-
...(gitContext.diff === undefined ? {} : { diff: gitContext.diff }),
|
|
721
|
-
...(gitContext.files === undefined
|
|
722
|
-
? {}
|
|
723
|
-
: { files: gitContext.files }),
|
|
724
|
-
},
|
|
725
|
-
}),
|
|
1178
|
+
body: JSON.stringify(deliveryRequest),
|
|
726
1179
|
signal: AbortSignal.timeout(config.timeoutMs ?? 2_500),
|
|
727
1180
|
});
|
|
728
1181
|
if (!response.ok) {
|
|
729
|
-
throw new
|
|
1182
|
+
throw new RuntimeHttpError(`Lore context request failed with HTTP ${response.status}`, response.status);
|
|
1183
|
+
}
|
|
1184
|
+
const value = await protocolResponseJson(response);
|
|
1185
|
+
if (config.version === 1) {
|
|
1186
|
+
return deliveryFromResponse(value);
|
|
1187
|
+
}
|
|
1188
|
+
if (!isObject(value) ||
|
|
1189
|
+
!isObject(value.delivery) ||
|
|
1190
|
+
!isObject(value.reliability) ||
|
|
1191
|
+
typeof value.snapshot !== "string" ||
|
|
1192
|
+
!isObject(value.delivery.event) ||
|
|
1193
|
+
typeof value.delivery.event.id !== "string" ||
|
|
1194
|
+
!isObject(value.delivery.receipt) ||
|
|
1195
|
+
typeof value.delivery.receipt.id !== "string") {
|
|
1196
|
+
throw new IncompatibleReliabilityError("Lore context response has no signed reliability snapshot");
|
|
1197
|
+
}
|
|
1198
|
+
const payload = await verifySnapshot(value.snapshot, "context", config, store, fetchImplementation, true);
|
|
1199
|
+
if (payload.eventId !== value.delivery.event.id ||
|
|
1200
|
+
payload.receiptId !== value.delivery.receipt.id) {
|
|
1201
|
+
throw new IncompatibleReliabilityError("Lore context snapshot does not bind the returned event and receipt");
|
|
1202
|
+
}
|
|
1203
|
+
const reliability = retrievalReliability(value.reliability, payload);
|
|
1204
|
+
if (Date.now() - Date.parse(payload.validUntil) >
|
|
1205
|
+
CONTEXT_CACHE_MAX_STALE_MS) {
|
|
1206
|
+
return {
|
|
1207
|
+
...emptyDelivery(),
|
|
1208
|
+
reliability: {
|
|
1209
|
+
status: "failed",
|
|
1210
|
+
source: "none",
|
|
1211
|
+
fallback: "none",
|
|
1212
|
+
reasons: ["no_usable_cache"],
|
|
1213
|
+
freshness: {
|
|
1214
|
+
state: "unknown",
|
|
1215
|
+
asOf: null,
|
|
1216
|
+
ageMs: null,
|
|
1217
|
+
validUntil: null,
|
|
1218
|
+
},
|
|
1219
|
+
policyVersion: null,
|
|
1220
|
+
},
|
|
1221
|
+
};
|
|
730
1222
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1223
|
+
await store
|
|
1224
|
+
.writeContextSnapshot(sourceEventId, { compactJws: value.snapshot })
|
|
1225
|
+
.catch(() => undefined);
|
|
1226
|
+
return {
|
|
1227
|
+
...deliveryFromSignedContext(payload, deliveryRequest),
|
|
1228
|
+
reliability,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
async function cachedSnapshotKeys(config, store, now) {
|
|
1232
|
+
if (config.workspaceId === undefined || store.workspaceId !== config.workspaceId) {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
const [keys, refreshState] = await Promise.all([
|
|
1236
|
+
store.readPublicTrustKeys(),
|
|
1237
|
+
store.readState(TRUST_KEY_REFRESH_STATE_KEY),
|
|
1238
|
+
]);
|
|
1239
|
+
const refreshedAt = isObject(refreshState)
|
|
1240
|
+
? refreshState.refreshedAt
|
|
1241
|
+
: undefined;
|
|
1242
|
+
const refreshedAtMs = typeof refreshedAt === "string" ? Date.parse(refreshedAt) : Number.NaN;
|
|
1243
|
+
if (keys.length === 0 ||
|
|
1244
|
+
!Number.isFinite(refreshedAtMs) ||
|
|
1245
|
+
now.getTime() - refreshedAtMs >= TRUST_KEY_REFRESH_TTL_MS ||
|
|
1246
|
+
refreshedAtMs > now.getTime() + 5 * 60_000) {
|
|
1247
|
+
return null;
|
|
734
1248
|
}
|
|
735
|
-
return
|
|
1249
|
+
return { workspaceId: config.workspaceId, keys };
|
|
736
1250
|
}
|
|
737
|
-
function
|
|
738
|
-
|
|
1251
|
+
async function getCachedPromptContext(config, prompt, gitContext, store, now) {
|
|
1252
|
+
const keys = await cachedSnapshotKeys(config, store, now);
|
|
1253
|
+
if (keys === null || config.workspaceId === undefined) {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
const scan = await store.listContextSnapshots();
|
|
1257
|
+
const candidates = [];
|
|
1258
|
+
for (const record of scan.records) {
|
|
1259
|
+
const value = isObject(record.value) ? record.value : null;
|
|
1260
|
+
if (value === null || typeof value.compactJws !== "string") {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
try {
|
|
1264
|
+
const payload = verifySignedSnapshot(value.compactJws, {
|
|
1265
|
+
workspaceId: config.workspaceId,
|
|
1266
|
+
kind: "context",
|
|
1267
|
+
keys,
|
|
1268
|
+
allowExpired: true,
|
|
1269
|
+
now,
|
|
1270
|
+
}).payload;
|
|
1271
|
+
candidates.push({
|
|
1272
|
+
cacheKey: record.cacheKey,
|
|
1273
|
+
compactJws: value.compactJws,
|
|
1274
|
+
payload,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
catch {
|
|
1278
|
+
// One invalid cache entry must not hide another verified candidate.
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const task = {
|
|
1282
|
+
workspaceId: config.workspaceId,
|
|
1283
|
+
task: redactSecrets(prompt),
|
|
1284
|
+
...(gitContext.scope === undefined ? {} : { scope: gitContext.scope }),
|
|
1285
|
+
...(gitContext.files === undefined ? {} : { files: gitContext.files }),
|
|
1286
|
+
};
|
|
1287
|
+
const selected = selectCachedContext(candidates, task, now);
|
|
1288
|
+
if (selected === null) {
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
context: selected.context,
|
|
1293
|
+
learned: [],
|
|
1294
|
+
delivered: [],
|
|
1295
|
+
reliability: {
|
|
1296
|
+
status: "degraded",
|
|
1297
|
+
source: "cache",
|
|
1298
|
+
fallback: "cached_context",
|
|
1299
|
+
reasons: ["live_unavailable", "cached_context"],
|
|
1300
|
+
freshness: selected.freshness,
|
|
1301
|
+
policyVersion: selected.payload.policyVersion,
|
|
1302
|
+
},
|
|
1303
|
+
};
|
|
739
1304
|
}
|
|
740
|
-
async function
|
|
741
|
-
const directory = queueDirectory(home);
|
|
742
|
-
let entries;
|
|
1305
|
+
async function getPromptContextWithFallback(config, agent, sessionId, sourceEventId, prompt, gitContext, fetchImplementation, store, now) {
|
|
743
1306
|
try {
|
|
744
|
-
|
|
745
|
-
.filter((entry) => entry.endsWith(".json"))
|
|
746
|
-
.sort();
|
|
1307
|
+
return await getPromptContext(config, agent, sessionId, sourceEventId, prompt, gitContext, fetchImplementation, store);
|
|
747
1308
|
}
|
|
748
|
-
catch {
|
|
749
|
-
|
|
1309
|
+
catch (error) {
|
|
1310
|
+
if (isContextTransportFailure(error)) {
|
|
1311
|
+
const cached = await getCachedPromptContext(config, prompt, gitContext, store, now).catch(() => null);
|
|
1312
|
+
if (cached !== null) {
|
|
1313
|
+
return cached;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
...emptyDelivery(),
|
|
1318
|
+
reliability: {
|
|
1319
|
+
status: "failed",
|
|
1320
|
+
source: "none",
|
|
1321
|
+
fallback: "none",
|
|
1322
|
+
reasons: [
|
|
1323
|
+
...(isContextTransportFailure(error) ? ["live_unavailable"] : []),
|
|
1324
|
+
isContextTransportFailure(error)
|
|
1325
|
+
? "no_usable_cache"
|
|
1326
|
+
: "invalid_response",
|
|
1327
|
+
],
|
|
1328
|
+
freshness: {
|
|
1329
|
+
state: "unknown",
|
|
1330
|
+
asOf: null,
|
|
1331
|
+
ageMs: null,
|
|
1332
|
+
validUntil: null,
|
|
1333
|
+
},
|
|
1334
|
+
policyVersion: null,
|
|
1335
|
+
},
|
|
1336
|
+
};
|
|
750
1337
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1338
|
+
}
|
|
1339
|
+
function formatAge(ageMs) {
|
|
1340
|
+
if (ageMs === null || ageMs < 60_000) {
|
|
1341
|
+
return "less than a minute";
|
|
1342
|
+
}
|
|
1343
|
+
if (ageMs < 60 * 60_000) {
|
|
1344
|
+
return `${Math.floor(ageMs / 60_000)}m`;
|
|
754
1345
|
}
|
|
755
|
-
|
|
756
|
-
.
|
|
757
|
-
|
|
1346
|
+
if (ageMs < 24 * 60 * 60_000) {
|
|
1347
|
+
return `${Math.floor(ageMs / (60 * 60_000))}h`;
|
|
1348
|
+
}
|
|
1349
|
+
return `${Math.floor(ageMs / (24 * 60 * 60_000))}d`;
|
|
1350
|
+
}
|
|
1351
|
+
async function retrievalNotice(delivery, sessionId, store, now) {
|
|
1352
|
+
const reliability = delivery.reliability;
|
|
1353
|
+
if (reliability === undefined || reliability.status === "ok") {
|
|
1354
|
+
return undefined;
|
|
1355
|
+
}
|
|
1356
|
+
if (reliability.fallback === "cached_context") {
|
|
1357
|
+
return `Lore used verified ${reliability.freshness.state} cached context (${formatAge(reliability.freshness.ageMs)} old) because live retrieval was unavailable.`;
|
|
1358
|
+
}
|
|
1359
|
+
const notice = reliability.fallback === "live_lexical"
|
|
1360
|
+
? "Lore used live lexical retrieval because semantic retrieval was unavailable."
|
|
1361
|
+
: reliability.reasons.includes("invalid_response")
|
|
1362
|
+
? "Lore context could not be trusted; continuing without injected context."
|
|
1363
|
+
: "Lore context is temporarily unavailable; continuing without injected context.";
|
|
1364
|
+
const key = `context-notice:${sessionId}`;
|
|
1365
|
+
const previous = await store.readState(key).catch(() => null);
|
|
1366
|
+
if (isObject(previous) &&
|
|
1367
|
+
previous.notice === notice &&
|
|
1368
|
+
typeof previous.at === "string" &&
|
|
1369
|
+
now.getTime() - Date.parse(previous.at) < CONTEXT_NOTICE_COOLDOWN_MS) {
|
|
1370
|
+
return undefined;
|
|
1371
|
+
}
|
|
1372
|
+
await store
|
|
1373
|
+
.writeState(key, { notice, at: now.toISOString() }, now)
|
|
1374
|
+
.catch(() => undefined);
|
|
1375
|
+
return notice;
|
|
1376
|
+
}
|
|
1377
|
+
function queueDirectory(home) {
|
|
1378
|
+
return resolve(loreDirectory(home), "queue");
|
|
758
1379
|
}
|
|
759
|
-
async function
|
|
760
|
-
await
|
|
761
|
-
|
|
762
|
-
|
|
1380
|
+
async function enqueueCapture(store, queued) {
|
|
1381
|
+
return (await store.enqueue({
|
|
1382
|
+
kind: queued.kind,
|
|
1383
|
+
idempotencyKey: queued.request.eventId,
|
|
1384
|
+
payload: queued,
|
|
1385
|
+
})).entry;
|
|
763
1386
|
}
|
|
764
1387
|
function isTurnRequest(value) {
|
|
765
1388
|
return (isObject(value) &&
|
|
@@ -799,38 +1422,57 @@ function queuedRequest(value) {
|
|
|
799
1422
|
}
|
|
800
1423
|
return null;
|
|
801
1424
|
}
|
|
802
|
-
async function
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}
|
|
810
|
-
catch {
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
if (first === undefined) {
|
|
814
|
-
return;
|
|
1425
|
+
async function flushOne(config, fetchImplementation, store) {
|
|
1426
|
+
const claimed = await store.claimNext({
|
|
1427
|
+
workerId: `native-hook:${process.pid}`,
|
|
1428
|
+
kinds: ["turn", "prompt"],
|
|
1429
|
+
});
|
|
1430
|
+
if (claimed === null || claimed.claim === undefined) {
|
|
1431
|
+
return null;
|
|
815
1432
|
}
|
|
816
|
-
const path = resolve(directory, first);
|
|
817
1433
|
try {
|
|
818
|
-
const
|
|
819
|
-
const queued = queuedRequest(parsed);
|
|
1434
|
+
const queued = queuedRequest(claimed.payload);
|
|
820
1435
|
if (queued === null) {
|
|
821
|
-
await
|
|
822
|
-
|
|
1436
|
+
const failed = await store.failClaim({
|
|
1437
|
+
claimId: claimed.claim.id,
|
|
1438
|
+
classification: "incompatible",
|
|
1439
|
+
message: "Durable capture payload is incompatible",
|
|
1440
|
+
});
|
|
1441
|
+
return {
|
|
1442
|
+
idempotencyKey: claimed.idempotencyKey,
|
|
1443
|
+
state: failed.state === "auth-blocked" ? "auth-blocked" : "dead",
|
|
1444
|
+
};
|
|
823
1445
|
}
|
|
1446
|
+
let delivery;
|
|
824
1447
|
if (queued.kind === "turn") {
|
|
825
|
-
await postTurn(config, queued.request, fetchImplementation);
|
|
1448
|
+
delivery = await postTurn(config, queued.request, fetchImplementation, store);
|
|
826
1449
|
}
|
|
827
1450
|
else {
|
|
828
|
-
await postPromptObservation(config, queued.request, fetchImplementation);
|
|
1451
|
+
await postPromptObservation(config, queued.request, fetchImplementation, store);
|
|
829
1452
|
}
|
|
830
|
-
await
|
|
1453
|
+
await store.acknowledgeClaim(claimed.claim.id);
|
|
1454
|
+
return {
|
|
1455
|
+
idempotencyKey: claimed.idempotencyKey,
|
|
1456
|
+
state: "acknowledged",
|
|
1457
|
+
...(delivery === undefined ? {} : { delivery }),
|
|
1458
|
+
};
|
|
831
1459
|
}
|
|
832
|
-
catch {
|
|
833
|
-
|
|
1460
|
+
catch (error) {
|
|
1461
|
+
const failed = await store.failClaim({
|
|
1462
|
+
claimId: claimed.claim.id,
|
|
1463
|
+
classification: error instanceof SignedSnapshotError
|
|
1464
|
+
? "incompatible"
|
|
1465
|
+
: classifyRetryFailure(error),
|
|
1466
|
+
message: error instanceof Error ? error.message : "Lore upload failed",
|
|
1467
|
+
});
|
|
1468
|
+
return {
|
|
1469
|
+
idempotencyKey: claimed.idempotencyKey,
|
|
1470
|
+
state: failed.state === "auth-blocked"
|
|
1471
|
+
? "auth-blocked"
|
|
1472
|
+
: failed.state === "dead"
|
|
1473
|
+
? "dead"
|
|
1474
|
+
: "ready",
|
|
1475
|
+
};
|
|
834
1476
|
}
|
|
835
1477
|
}
|
|
836
1478
|
function receiptMessage(config, agent, delivery) {
|
|
@@ -979,6 +1621,7 @@ function guardResultFromResponse(value) {
|
|
|
979
1621
|
}
|
|
980
1622
|
const scope = isObject(item.scope) ? item.scope : {};
|
|
981
1623
|
const source = isObject(item.source) ? item.source : {};
|
|
1624
|
+
const explanation = guardExplanationFromValue(item.explanation);
|
|
982
1625
|
return [
|
|
983
1626
|
{
|
|
984
1627
|
content: redactSecrets(item.content),
|
|
@@ -992,6 +1635,7 @@ function guardResultFromResponse(value) {
|
|
|
992
1635
|
...(typeof source.agent === "string"
|
|
993
1636
|
? { sourceAgent: source.agent }
|
|
994
1637
|
: {}),
|
|
1638
|
+
...(explanation === undefined ? {} : { explanation }),
|
|
995
1639
|
},
|
|
996
1640
|
];
|
|
997
1641
|
})
|
|
@@ -1013,6 +1657,33 @@ function guardResultFromResponse(value) {
|
|
|
1013
1657
|
conflictSummaries,
|
|
1014
1658
|
};
|
|
1015
1659
|
}
|
|
1660
|
+
function guardExplanationFromValue(value) {
|
|
1661
|
+
if (!isObject(value) ||
|
|
1662
|
+
typeof value.summary !== "string" ||
|
|
1663
|
+
!isObject(value.provenance) ||
|
|
1664
|
+
typeof value.provenance.agent !== "string" ||
|
|
1665
|
+
typeof value.status !== "string" ||
|
|
1666
|
+
!isObject(value.freshness) ||
|
|
1667
|
+
typeof value.freshness.updatedAt !== "string") {
|
|
1668
|
+
return undefined;
|
|
1669
|
+
}
|
|
1670
|
+
const scope = isObject(value.scope) ? value.scope : {};
|
|
1671
|
+
const scopeParts = [
|
|
1672
|
+
typeof scope.organization === "string"
|
|
1673
|
+
? `org ${scope.organization}`
|
|
1674
|
+
: undefined,
|
|
1675
|
+
typeof scope.project === "string" ? `project ${scope.project}` : undefined,
|
|
1676
|
+
typeof scope.repo === "string" ? `repo ${scope.repo}` : undefined,
|
|
1677
|
+
typeof scope.path === "string" ? `path ${scope.path}` : undefined,
|
|
1678
|
+
typeof scope.component === "string"
|
|
1679
|
+
? `component ${scope.component}`
|
|
1680
|
+
: undefined,
|
|
1681
|
+
].filter((part) => part !== undefined);
|
|
1682
|
+
const session = typeof value.provenance.sessionId === "string"
|
|
1683
|
+
? ` session ${value.provenance.sessionId}`
|
|
1684
|
+
: "";
|
|
1685
|
+
return `Why: ${redactSecrets(value.summary)} Source: ${value.provenance.agent}${session}. Scope: ${scopeParts.join(" · ") || "workspace-wide"}. Status: ${value.status}. Updated: ${value.freshness.updatedAt.slice(0, 10)}.`;
|
|
1686
|
+
}
|
|
1016
1687
|
function guardItemSource(item) {
|
|
1017
1688
|
const scope = item.repo !== undefined
|
|
1018
1689
|
? `${item.repo} repo`
|
|
@@ -1027,10 +1698,7 @@ function guardAssistContext(result) {
|
|
|
1027
1698
|
const lines = ["Relevant Lore context:"];
|
|
1028
1699
|
for (const item of result.items) {
|
|
1029
1700
|
lines.push(`- ${item.content}`);
|
|
1030
|
-
|
|
1031
|
-
lines.push("", "Sources:");
|
|
1032
|
-
for (const item of result.items) {
|
|
1033
|
-
lines.push(`- ${guardItemSource(item)}`);
|
|
1701
|
+
lines.push(` ${item.explanation ?? `Source: ${guardItemSource(item)}.`}`);
|
|
1034
1702
|
}
|
|
1035
1703
|
if (result.conflictSummaries.length > 0) {
|
|
1036
1704
|
lines.push("", "Conflicting Lore context (do not silently pick a winner):");
|
|
@@ -1045,17 +1713,21 @@ function guardConfirmationReason(result) {
|
|
|
1045
1713
|
const shown = required.length > 0 ? required : result.items;
|
|
1046
1714
|
const lines = [
|
|
1047
1715
|
"Lore Guard: this action conflicts with a required rule.",
|
|
1048
|
-
...shown.
|
|
1716
|
+
...shown.flatMap((item) => [
|
|
1717
|
+
`- ${item.content}`,
|
|
1718
|
+
` ${item.explanation ?? `Source: ${guardItemSource(item)}.`}`,
|
|
1719
|
+
]),
|
|
1049
1720
|
];
|
|
1050
1721
|
lines.push(result.confirmCommand === undefined
|
|
1051
1722
|
? "Ask the user to confirm before proceeding."
|
|
1052
1723
|
: `Approve here to proceed, or the user can run: ${result.confirmCommand}`);
|
|
1053
1724
|
return boundedContext(lines.join("\n"));
|
|
1054
1725
|
}
|
|
1055
|
-
async function postGuardCheck(config, request, fetchImplementation) {
|
|
1726
|
+
async function postGuardCheck(config, request, fetchImplementation, store) {
|
|
1056
1727
|
const response = await fetchImplementation(guardCheckUrl(config.apiUrl), {
|
|
1057
1728
|
method: "POST",
|
|
1058
1729
|
headers: {
|
|
1730
|
+
accept: runtimeAccept(config),
|
|
1059
1731
|
authorization: `Bearer ${config.token}`,
|
|
1060
1732
|
"content-type": "application/json",
|
|
1061
1733
|
"user-agent": `lore-cli/${RUNTIME_VERSION}`,
|
|
@@ -1064,9 +1736,26 @@ async function postGuardCheck(config, request, fetchImplementation) {
|
|
|
1064
1736
|
signal: AbortSignal.timeout(Math.min(config.timeoutMs ?? GUARD_TIMEOUT_MS, GUARD_TIMEOUT_MS)),
|
|
1065
1737
|
});
|
|
1066
1738
|
if (!response.ok) {
|
|
1067
|
-
throw new
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1739
|
+
throw new RuntimeHttpError(`Lore guard check failed with HTTP ${response.status}`, response.status);
|
|
1740
|
+
}
|
|
1741
|
+
const value = await protocolResponseJson(response);
|
|
1742
|
+
if (config.version === 1) {
|
|
1743
|
+
return guardResultFromResponse(value);
|
|
1744
|
+
}
|
|
1745
|
+
if (!isObject(value) ||
|
|
1746
|
+
!isObject(value.check) ||
|
|
1747
|
+
typeof value.policyVersion !== "string" ||
|
|
1748
|
+
typeof value.policySnapshot !== "string") {
|
|
1749
|
+
throw new IncompatibleReliabilityError("Lore Guard response has no signed policy snapshot");
|
|
1750
|
+
}
|
|
1751
|
+
const payload = await verifySnapshot(value.policySnapshot, "guard_policy", config, store, fetchImplementation);
|
|
1752
|
+
if (payload.policyVersion !== value.policyVersion ||
|
|
1753
|
+
(config.workspaceId !== undefined &&
|
|
1754
|
+
payload.workspaceId !== config.workspaceId)) {
|
|
1755
|
+
throw new IncompatibleReliabilityError("Lore Guard snapshot does not bind the returned policy");
|
|
1756
|
+
}
|
|
1757
|
+
await store.writePolicySnapshot("current", value.policySnapshot);
|
|
1758
|
+
return guardResultFromResponse(value.check);
|
|
1070
1759
|
}
|
|
1071
1760
|
async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
1072
1761
|
if (agent === "polytoken") {
|
|
@@ -1081,10 +1770,12 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1081
1770
|
if (trigger === null) {
|
|
1082
1771
|
return undefined;
|
|
1083
1772
|
}
|
|
1773
|
+
const store = await runtimeStore(config, options.home);
|
|
1084
1774
|
const state = await readGuardState(agent, sessionId, options.home);
|
|
1085
1775
|
if (state.mode === "off" &&
|
|
1086
1776
|
state.modeCheckedAt !== undefined &&
|
|
1087
1777
|
now.getTime() - Date.parse(state.modeCheckedAt) < GUARD_MODE_TTL_MS) {
|
|
1778
|
+
await recordLocalGuardMetric(store, { outcome: "reused_decision" }, now).catch(() => undefined);
|
|
1088
1779
|
return undefined;
|
|
1089
1780
|
}
|
|
1090
1781
|
const cwd = stringField(input.cwd);
|
|
@@ -1112,24 +1803,38 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1112
1803
|
const cachedAt = state.keys[key];
|
|
1113
1804
|
if (cachedAt !== undefined &&
|
|
1114
1805
|
now.getTime() - Date.parse(cachedAt) < GUARD_KEY_COOLDOWN_MS) {
|
|
1806
|
+
await recordLocalGuardMetric(store, { outcome: "reused_decision" }, now).catch(() => undefined);
|
|
1115
1807
|
return undefined;
|
|
1116
1808
|
}
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
? {}
|
|
1128
|
-
|
|
1129
|
-
|
|
1809
|
+
const startedAt = Date.now();
|
|
1810
|
+
let result;
|
|
1811
|
+
try {
|
|
1812
|
+
result = await postGuardCheck(config, {
|
|
1813
|
+
connector: "lore-cli",
|
|
1814
|
+
agent,
|
|
1815
|
+
sessionId,
|
|
1816
|
+
action: trigger.action,
|
|
1817
|
+
tool: toolName,
|
|
1818
|
+
...(scope?.repo === undefined ? {} : { repo: scope.repo }),
|
|
1819
|
+
...(scope?.path === undefined ? {} : { path: scope.path }),
|
|
1820
|
+
...(files.length === 0 ? {} : { files }),
|
|
1821
|
+
...(trigger.command === undefined
|
|
1822
|
+
? {}
|
|
1823
|
+
: { command: redactSecrets(trigger.command).slice(0, 10_000) }),
|
|
1824
|
+
}, options.fetch ?? globalThis.fetch, store);
|
|
1825
|
+
}
|
|
1826
|
+
catch (error) {
|
|
1827
|
+
await recordLocalGuardMetric(store, { outcome: "failed" }, now).catch(() => undefined);
|
|
1828
|
+
throw error;
|
|
1829
|
+
}
|
|
1130
1830
|
if (result === null) {
|
|
1131
|
-
|
|
1831
|
+
await recordLocalGuardMetric(store, { outcome: "failed" }, now).catch(() => undefined);
|
|
1832
|
+
throw new IncompatibleReliabilityError("Lore Guard response is invalid");
|
|
1132
1833
|
}
|
|
1834
|
+
await recordLocalGuardMetric(store, {
|
|
1835
|
+
outcome: "live_check",
|
|
1836
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
1837
|
+
}, now).catch(() => undefined);
|
|
1133
1838
|
state.mode = result.mode;
|
|
1134
1839
|
state.modeCheckedAt = now.toISOString();
|
|
1135
1840
|
if (!result.requiresConfirmation) {
|
|
@@ -1174,14 +1879,16 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1174
1879
|
}
|
|
1175
1880
|
export async function handleHookEvent(value, agent, options = {}) {
|
|
1176
1881
|
if (!isObject(value)) {
|
|
1177
|
-
|
|
1882
|
+
throw new InvalidHookInputError();
|
|
1178
1883
|
}
|
|
1179
1884
|
const normalized = normalizeHookInput(value, agent, options.environment ?? process.env);
|
|
1180
1885
|
const input = normalized.input;
|
|
1181
1886
|
const eventName = normalized.eventName;
|
|
1182
1887
|
const sessionId = normalized.sessionId;
|
|
1183
|
-
if (eventName === undefined ||
|
|
1184
|
-
|
|
1888
|
+
if (eventName === undefined ||
|
|
1889
|
+
sessionId === undefined ||
|
|
1890
|
+
!validHookInput(input, agent, eventName)) {
|
|
1891
|
+
throw new InvalidHookInputError();
|
|
1185
1892
|
}
|
|
1186
1893
|
const config = await readRuntimeConfig(options.home);
|
|
1187
1894
|
if (config === null || !config.agents.includes(agent)) {
|
|
@@ -1203,20 +1910,65 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1203
1910
|
return await handleGuardEvent(input, agent, config, sessionId, options, now);
|
|
1204
1911
|
}
|
|
1205
1912
|
catch {
|
|
1206
|
-
|
|
1207
|
-
|
|
1913
|
+
const warning = "Lore Guard is unavailable; this action is proceeding without a live policy decision.";
|
|
1914
|
+
if (agent === "cursor") {
|
|
1915
|
+
return {
|
|
1916
|
+
userMessage: warning,
|
|
1917
|
+
agentMessage: warning,
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
return {
|
|
1921
|
+
hookSpecificOutput: {
|
|
1922
|
+
hookEventName: "PreToolUse",
|
|
1923
|
+
additionalContext: warning,
|
|
1924
|
+
},
|
|
1925
|
+
};
|
|
1208
1926
|
}
|
|
1209
1927
|
}
|
|
1210
|
-
if (eventName !== "UserPromptSubmit") {
|
|
1211
|
-
return undefined;
|
|
1212
|
-
}
|
|
1213
1928
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
1214
|
-
await retryOne(config, fetchImplementation, options.home);
|
|
1215
|
-
const pending = await consumePending(agent, sessionId, options.home);
|
|
1216
1929
|
const prompt = stringField(input.prompt);
|
|
1217
1930
|
if (prompt === undefined) {
|
|
1218
|
-
|
|
1931
|
+
throw new InvalidHookInputError();
|
|
1932
|
+
}
|
|
1933
|
+
const notices = [];
|
|
1934
|
+
let store;
|
|
1935
|
+
try {
|
|
1936
|
+
store = await runtimeStore(config, options.home);
|
|
1937
|
+
}
|
|
1938
|
+
catch (error) {
|
|
1939
|
+
const detail = error instanceof ReliabilityStoreError &&
|
|
1940
|
+
error.code === "CAPACITY_EXCEEDED"
|
|
1941
|
+
? "the local outbox is full"
|
|
1942
|
+
: "local durable storage is unavailable";
|
|
1943
|
+
notices.push(`Lore could not acknowledge this capture because ${detail}. Run lore doctor.`);
|
|
1944
|
+
const notice = notices.join(" ");
|
|
1945
|
+
return agent === "polytoken"
|
|
1946
|
+
? { outcome: "accept", additional_context: notice }
|
|
1947
|
+
: agent === "cursor"
|
|
1948
|
+
? {
|
|
1949
|
+
continue: true,
|
|
1950
|
+
hookSpecificOutput: {
|
|
1951
|
+
hookEventName: "UserPromptSubmit",
|
|
1952
|
+
additionalContext: notice,
|
|
1953
|
+
},
|
|
1954
|
+
}
|
|
1955
|
+
: { systemMessage: notice };
|
|
1219
1956
|
}
|
|
1957
|
+
const noticeForFlush = (result) => {
|
|
1958
|
+
if (result?.state === "auth-blocked") {
|
|
1959
|
+
notices.push("Lore retained a capture locally, but upload is blocked by authentication. Run lore connect again.");
|
|
1960
|
+
}
|
|
1961
|
+
else if (result?.state === "dead") {
|
|
1962
|
+
notices.push("Lore retained a capture as a dead letter because the server response was incompatible. Run lore doctor.");
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
try {
|
|
1966
|
+
noticeForFlush(await flushOne(config, fetchImplementation, store));
|
|
1967
|
+
}
|
|
1968
|
+
catch {
|
|
1969
|
+
notices.push("Lore retained queued captures locally, but replay could not run. Run lore doctor.");
|
|
1970
|
+
}
|
|
1971
|
+
const pending = await readPending(agent, sessionId, options.home);
|
|
1220
1972
|
const lineage = await readLineageMetadata(options.home, options.environment ?? process.env);
|
|
1221
1973
|
let delivery = emptyDelivery();
|
|
1222
1974
|
if (pending !== null) {
|
|
@@ -1224,26 +1976,61 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1224
1976
|
if (request === null) {
|
|
1225
1977
|
return undefined;
|
|
1226
1978
|
}
|
|
1979
|
+
let enqueued = false;
|
|
1227
1980
|
try {
|
|
1228
|
-
|
|
1981
|
+
await enqueueCapture(store, { kind: "turn", request });
|
|
1982
|
+
enqueued = true;
|
|
1983
|
+
await clearPending(agent, sessionId, options.home);
|
|
1229
1984
|
}
|
|
1230
|
-
catch {
|
|
1985
|
+
catch (error) {
|
|
1986
|
+
notices.push(error instanceof ReliabilityStoreError &&
|
|
1987
|
+
error.code === "CAPACITY_EXCEEDED"
|
|
1988
|
+
? "Lore could not acknowledge this capture because the local outbox is full. Run lore status."
|
|
1989
|
+
: "Lore could not acknowledge this capture because local persistence failed. Run lore doctor.");
|
|
1990
|
+
}
|
|
1991
|
+
if (enqueued) {
|
|
1231
1992
|
try {
|
|
1232
|
-
await
|
|
1993
|
+
const flushed = await flushOne(config, fetchImplementation, store);
|
|
1994
|
+
noticeForFlush(flushed);
|
|
1995
|
+
if (flushed?.idempotencyKey === request.eventId &&
|
|
1996
|
+
flushed.state === "acknowledged" &&
|
|
1997
|
+
flushed.delivery !== undefined) {
|
|
1998
|
+
delivery = flushed.delivery;
|
|
1999
|
+
}
|
|
2000
|
+
else {
|
|
2001
|
+
notices.push("Lore saved this capture locally; server upload is pending.");
|
|
2002
|
+
}
|
|
1233
2003
|
}
|
|
1234
2004
|
catch {
|
|
1235
|
-
|
|
2005
|
+
notices.push("Lore saved this capture locally; server upload is pending.");
|
|
1236
2006
|
}
|
|
2007
|
+
}
|
|
2008
|
+
if (delivery.context === "") {
|
|
1237
2009
|
try {
|
|
1238
|
-
delivery = await
|
|
2010
|
+
delivery = await getPromptContextWithFallback(config, agent, sessionId, request.eventId, prompt, {
|
|
1239
2011
|
...(request.scope === undefined ? {} : { scope: request.scope }),
|
|
1240
2012
|
learningScope: request.learningScope,
|
|
1241
2013
|
...(request.diff === undefined ? {} : { diff: request.diff }),
|
|
1242
2014
|
...(request.files === undefined ? {} : { files: request.files }),
|
|
1243
|
-
}, fetchImplementation);
|
|
2015
|
+
}, fetchImplementation, store, now);
|
|
1244
2016
|
}
|
|
1245
2017
|
catch {
|
|
1246
|
-
|
|
2018
|
+
delivery = {
|
|
2019
|
+
...emptyDelivery(),
|
|
2020
|
+
reliability: {
|
|
2021
|
+
status: "failed",
|
|
2022
|
+
source: "none",
|
|
2023
|
+
fallback: "none",
|
|
2024
|
+
reasons: ["invalid_response"],
|
|
2025
|
+
freshness: {
|
|
2026
|
+
state: "unknown",
|
|
2027
|
+
asOf: null,
|
|
2028
|
+
ageMs: null,
|
|
2029
|
+
validUntil: null,
|
|
2030
|
+
},
|
|
2031
|
+
policyVersion: null,
|
|
2032
|
+
},
|
|
2033
|
+
};
|
|
1247
2034
|
}
|
|
1248
2035
|
}
|
|
1249
2036
|
}
|
|
@@ -1253,45 +2040,99 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1253
2040
|
if (observation === null) {
|
|
1254
2041
|
return undefined;
|
|
1255
2042
|
}
|
|
2043
|
+
let enqueued = false;
|
|
1256
2044
|
try {
|
|
1257
|
-
await
|
|
2045
|
+
await enqueueCapture(store, { kind: "prompt", request: observation });
|
|
2046
|
+
enqueued = true;
|
|
1258
2047
|
}
|
|
1259
|
-
catch {
|
|
2048
|
+
catch (error) {
|
|
2049
|
+
notices.push(error instanceof ReliabilityStoreError &&
|
|
2050
|
+
error.code === "CAPACITY_EXCEEDED"
|
|
2051
|
+
? "Lore could not acknowledge this capture because the local outbox is full. Run lore status."
|
|
2052
|
+
: "Lore could not acknowledge this capture because local persistence failed. Run lore doctor.");
|
|
2053
|
+
}
|
|
2054
|
+
if (enqueued) {
|
|
1260
2055
|
try {
|
|
1261
|
-
await
|
|
2056
|
+
const flushed = await flushOne(config, fetchImplementation, store);
|
|
2057
|
+
noticeForFlush(flushed);
|
|
2058
|
+
if (flushed?.idempotencyKey !== observation.eventId ||
|
|
2059
|
+
flushed.state !== "acknowledged") {
|
|
2060
|
+
notices.push("Lore saved this capture locally; server upload is pending.");
|
|
2061
|
+
}
|
|
1262
2062
|
}
|
|
1263
2063
|
catch {
|
|
1264
|
-
|
|
2064
|
+
notices.push("Lore saved this capture locally; server upload is pending.");
|
|
1265
2065
|
}
|
|
1266
2066
|
}
|
|
1267
2067
|
try {
|
|
1268
|
-
delivery = await
|
|
2068
|
+
delivery = await getPromptContextWithFallback(config, agent, sessionId, observation.eventId, prompt, gitContext, fetchImplementation, store, now);
|
|
1269
2069
|
}
|
|
1270
2070
|
catch {
|
|
1271
|
-
|
|
2071
|
+
delivery = {
|
|
2072
|
+
...emptyDelivery(),
|
|
2073
|
+
reliability: {
|
|
2074
|
+
status: "failed",
|
|
2075
|
+
source: "none",
|
|
2076
|
+
fallback: "none",
|
|
2077
|
+
reasons: ["invalid_response"],
|
|
2078
|
+
freshness: {
|
|
2079
|
+
state: "unknown",
|
|
2080
|
+
asOf: null,
|
|
2081
|
+
ageMs: null,
|
|
2082
|
+
validUntil: null,
|
|
2083
|
+
},
|
|
2084
|
+
policyVersion: null,
|
|
2085
|
+
},
|
|
2086
|
+
};
|
|
1272
2087
|
}
|
|
1273
2088
|
}
|
|
2089
|
+
const reliability = delivery.reliability;
|
|
2090
|
+
await recordLocalRetrievalMetric(store, {
|
|
2091
|
+
outcome: reliability?.fallback === "cached_context"
|
|
2092
|
+
? "cached_fallback"
|
|
2093
|
+
: reliability?.fallback === "live_lexical"
|
|
2094
|
+
? "live_lexical_fallback"
|
|
2095
|
+
: reliability?.status === "failed" ||
|
|
2096
|
+
reliability?.source === "none"
|
|
2097
|
+
? "failed"
|
|
2098
|
+
: "live_primary",
|
|
2099
|
+
...(reliability?.freshness.ageMs === undefined
|
|
2100
|
+
? {}
|
|
2101
|
+
: { cacheAgeMs: reliability.freshness.ageMs }),
|
|
2102
|
+
}, now).catch(() => undefined);
|
|
2103
|
+
const contextReliabilityNotice = await retrievalNotice(delivery, sessionId, store, now);
|
|
2104
|
+
if (contextReliabilityNotice !== undefined) {
|
|
2105
|
+
notices.push(contextReliabilityNotice);
|
|
2106
|
+
}
|
|
2107
|
+
const uniqueNotices = [...new Set(notices)];
|
|
2108
|
+
const reliabilityNotice = uniqueNotices.length === 0 ? undefined : uniqueNotices.join(" ");
|
|
2109
|
+
const receipt = receiptMessage(config, agent, delivery);
|
|
1274
2110
|
const systemMessage = agent === "cursor" || agent === "polytoken"
|
|
1275
2111
|
? undefined
|
|
1276
|
-
:
|
|
1277
|
-
|
|
2112
|
+
: [receipt, reliabilityNotice].filter(Boolean).join(" · ") || undefined;
|
|
2113
|
+
const injectedContext = agent === "cursor" || agent === "polytoken"
|
|
2114
|
+
? [delivery.context, reliabilityNotice]
|
|
2115
|
+
.filter((entry) => entry !== undefined && entry !== "")
|
|
2116
|
+
.join("\n\n")
|
|
2117
|
+
: delivery.context;
|
|
2118
|
+
if (injectedContext === "" && systemMessage === undefined) {
|
|
1278
2119
|
return undefined;
|
|
1279
2120
|
}
|
|
1280
2121
|
if (agent === "polytoken") {
|
|
1281
2122
|
return {
|
|
1282
2123
|
outcome: "accept",
|
|
1283
|
-
additional_context:
|
|
2124
|
+
additional_context: injectedContext,
|
|
1284
2125
|
};
|
|
1285
2126
|
}
|
|
1286
2127
|
return {
|
|
1287
2128
|
...(agent === "cursor" ? { continue: true } : {}),
|
|
1288
2129
|
...(systemMessage === undefined ? {} : { systemMessage }),
|
|
1289
|
-
...(
|
|
2130
|
+
...(injectedContext === ""
|
|
1290
2131
|
? {}
|
|
1291
2132
|
: {
|
|
1292
2133
|
hookSpecificOutput: {
|
|
1293
2134
|
hookEventName: "UserPromptSubmit",
|
|
1294
|
-
additionalContext:
|
|
2135
|
+
additionalContext: injectedContext,
|
|
1295
2136
|
},
|
|
1296
2137
|
}),
|
|
1297
2138
|
};
|
|
@@ -1312,25 +2153,73 @@ function parseAgent(args) {
|
|
|
1312
2153
|
const value = index < 0 ? undefined : args[index + 1];
|
|
1313
2154
|
return isCommandHookAgent(value) ? value : null;
|
|
1314
2155
|
}
|
|
2156
|
+
function nativeIntegrationId(agent) {
|
|
2157
|
+
return `native/${agent}`;
|
|
2158
|
+
}
|
|
2159
|
+
async function beginHookInvocation(agent) {
|
|
2160
|
+
const config = await readRuntimeConfig();
|
|
2161
|
+
if (config === null || !config.agents.includes(agent)) {
|
|
2162
|
+
return null;
|
|
2163
|
+
}
|
|
2164
|
+
const workspaceKey = reliabilityWorkspaceKey(config);
|
|
2165
|
+
const home = homeDirectory();
|
|
2166
|
+
const attempt = createIntegrationInvocationAttempt(nativeIntegrationId(agent), { runtimeVersion: RUNTIME_VERSION });
|
|
2167
|
+
const recorded = await writeInvocationAttemptBounded(workspaceKey, attempt, { home, standalone: IS_STANDALONE_RUNTIME });
|
|
2168
|
+
return recorded ? { workspaceKey, home, attempt } : null;
|
|
2169
|
+
}
|
|
2170
|
+
async function finishHookInvocation(tracker, outcome) {
|
|
2171
|
+
if (tracker === null) {
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
await writeInvocationCompletionBounded(tracker.workspaceKey, tracker.attempt, outcome, { home: tracker.home, standalone: IS_STANDALONE_RUNTIME }).catch(() => false);
|
|
2175
|
+
}
|
|
1315
2176
|
export async function runHook(args = process.argv.slice(2)) {
|
|
1316
2177
|
const agent = parseAgent(args);
|
|
1317
2178
|
if (agent === null) {
|
|
1318
2179
|
return;
|
|
1319
2180
|
}
|
|
2181
|
+
const tracker = await beginHookInvocation(agent).catch(() => null);
|
|
2182
|
+
let input;
|
|
2183
|
+
try {
|
|
2184
|
+
input = await readStdin();
|
|
2185
|
+
}
|
|
2186
|
+
catch {
|
|
2187
|
+
await finishHookInvocation(tracker, {
|
|
2188
|
+
success: false,
|
|
2189
|
+
failureCode: "invalid_input",
|
|
2190
|
+
});
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
1320
2193
|
try {
|
|
1321
|
-
const result = await handleHookEvent(
|
|
2194
|
+
const result = await handleHookEvent(input, agent);
|
|
1322
2195
|
if (result !== undefined) {
|
|
1323
2196
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
1324
2197
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
2198
|
+
await finishHookInvocation(tracker, { success: true });
|
|
2199
|
+
}
|
|
2200
|
+
catch (error) {
|
|
2201
|
+
await finishHookInvocation(tracker, {
|
|
2202
|
+
success: false,
|
|
2203
|
+
failureCode: error instanceof InvalidHookInputError
|
|
2204
|
+
? "invalid_input"
|
|
2205
|
+
: "runtime_error",
|
|
2206
|
+
});
|
|
1327
2207
|
// Native hooks must never block or add error noise to an agent session.
|
|
1328
2208
|
}
|
|
1329
2209
|
}
|
|
1330
2210
|
const entryPath = process.argv[1];
|
|
2211
|
+
const isRuntimeEntrypoint = entryPath !== undefined &&
|
|
2212
|
+
(() => {
|
|
2213
|
+
try {
|
|
2214
|
+
return (realpathSync(resolve(entryPath)) ===
|
|
2215
|
+
realpathSync(fileURLToPath(import.meta.url)));
|
|
2216
|
+
}
|
|
2217
|
+
catch {
|
|
2218
|
+
return import.meta.url === pathToFileURL(resolve(entryPath)).href;
|
|
2219
|
+
}
|
|
2220
|
+
})();
|
|
1331
2221
|
if (!IS_STANDALONE_RUNTIME &&
|
|
1332
|
-
|
|
1333
|
-
import.meta.url === pathToFileURL(resolve(entryPath)).href) {
|
|
2222
|
+
isRuntimeEntrypoint) {
|
|
1334
2223
|
void runHook();
|
|
1335
2224
|
}
|
|
1336
2225
|
//# sourceMappingURL=runtime.js.map
|