@lore-co/cli 0.1.17 → 0.1.19
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 +99 -17
- 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 +37 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1071 -80
- 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 +9 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1113 -153
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +15 -2
- package/dist/self-host.d.ts.map +1 -1
- package/dist/self-host.js +55 -8
- package/dist/self-host.js.map +1 -1
- 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,19 +1,23 @@
|
|
|
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",
|
|
16
18
|
"claude",
|
|
19
|
+
"copilot-cli",
|
|
20
|
+
"copilot-vscode",
|
|
17
21
|
"cursor",
|
|
18
22
|
"polytoken",
|
|
19
23
|
];
|
|
@@ -22,11 +26,33 @@ function isCommandHookAgent(value) {
|
|
|
22
26
|
return typeof value === "string" && COMMAND_HOOK_AGENTS.has(value);
|
|
23
27
|
}
|
|
24
28
|
const MAX_STDIN_BYTES = 1024 * 1024;
|
|
29
|
+
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
25
30
|
const MAX_CONTEXT_CHARS = 12_000;
|
|
26
31
|
const PENDING_ASSISTANT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
27
|
-
const MAX_QUEUE_ITEMS = 100;
|
|
28
32
|
const MAX_GIT_DIFF_BYTES = 256 * 1024;
|
|
29
33
|
const MAX_GIT_FILES = 100;
|
|
34
|
+
const RELIABILITY_V1_MEDIA_TYPE = "application/vnd.lore.reliability-v1+json";
|
|
35
|
+
const TRUST_KEY_REFRESH_TTL_MS = 60 * 60_000;
|
|
36
|
+
const TRUST_KEY_REFRESH_STATE_KEY = "snapshot-trust-keys";
|
|
37
|
+
const CONTEXT_NOTICE_COOLDOWN_MS = 15 * 60_000;
|
|
38
|
+
const RELIABILITY_REASON_CODES = new Set([
|
|
39
|
+
"live_success",
|
|
40
|
+
"idempotent_replay",
|
|
41
|
+
"semantic_unavailable",
|
|
42
|
+
"lexical_fallback",
|
|
43
|
+
"live_unavailable",
|
|
44
|
+
"cached_context",
|
|
45
|
+
"cached_policy",
|
|
46
|
+
"no_usable_cache",
|
|
47
|
+
"capture_queued",
|
|
48
|
+
"local_persistence_failed",
|
|
49
|
+
"governed_rule",
|
|
50
|
+
"policy_unavailable",
|
|
51
|
+
"policy_expired",
|
|
52
|
+
"user_override",
|
|
53
|
+
"invalid_response",
|
|
54
|
+
"integration_unsupported",
|
|
55
|
+
]);
|
|
30
56
|
const NON_SECRET_TOKEN_VALUES = new Set([
|
|
31
57
|
"available",
|
|
32
58
|
"configured",
|
|
@@ -81,6 +107,18 @@ export async function readLineageMetadata(home, environment = process.env) {
|
|
|
81
107
|
function isObject(value) {
|
|
82
108
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
83
109
|
}
|
|
110
|
+
function canonicalJson(value) {
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
113
|
+
}
|
|
114
|
+
if (isObject(value)) {
|
|
115
|
+
return `{${Object.keys(value)
|
|
116
|
+
.sort()
|
|
117
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
118
|
+
.join(",")}}`;
|
|
119
|
+
}
|
|
120
|
+
return JSON.stringify(value) ?? "null";
|
|
121
|
+
}
|
|
84
122
|
function stringField(value) {
|
|
85
123
|
return typeof value === "string" && value.trim() !== ""
|
|
86
124
|
? value
|
|
@@ -89,16 +127,54 @@ function stringField(value) {
|
|
|
89
127
|
function normalizeHookInput(input, agent, environment) {
|
|
90
128
|
const rawEventName = stringField(input.hook_event_name) ??
|
|
91
129
|
stringField(input.event) ??
|
|
130
|
+
(agent === "copilot-cli"
|
|
131
|
+
? stringField(environment.LORE_HOOK_EVENT)
|
|
132
|
+
: undefined) ??
|
|
92
133
|
(agent === "polytoken"
|
|
93
134
|
? stringField(environment.POLYTOKEN_HOOK_EVENT)
|
|
94
135
|
: undefined);
|
|
95
|
-
if (agent !== "cursor" &&
|
|
136
|
+
if (agent !== "cursor" &&
|
|
137
|
+
agent !== "polytoken" &&
|
|
138
|
+
agent !== "copilot-cli") {
|
|
96
139
|
return {
|
|
97
140
|
input,
|
|
98
141
|
eventName: rawEventName,
|
|
99
142
|
sessionId: stringField(input.session_id),
|
|
100
143
|
};
|
|
101
144
|
}
|
|
145
|
+
if (agent === "copilot-cli") {
|
|
146
|
+
const eventName = rawEventName === "userPromptTransformed"
|
|
147
|
+
? "UserPromptSubmit"
|
|
148
|
+
: rawEventName === "preToolUse"
|
|
149
|
+
? "PreToolUse"
|
|
150
|
+
: rawEventName === "agentStop"
|
|
151
|
+
? "Stop"
|
|
152
|
+
: rawEventName === "sessionEnd"
|
|
153
|
+
? "SessionEnd"
|
|
154
|
+
: rawEventName;
|
|
155
|
+
const sessionId = stringField(input.sessionId) ?? stringField(input.session_id);
|
|
156
|
+
return {
|
|
157
|
+
input: {
|
|
158
|
+
...input,
|
|
159
|
+
...(rawEventName === undefined ? {} : { event: rawEventName }),
|
|
160
|
+
...(sessionId === undefined ? {} : { session_id: sessionId }),
|
|
161
|
+
...(stringField(input.transformedPrompt) === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: { transformed_prompt: input.transformedPrompt }),
|
|
164
|
+
...(eventName === "PreToolUse"
|
|
165
|
+
? {
|
|
166
|
+
tool_name: input.toolName,
|
|
167
|
+
tool_input: input.toolArgs,
|
|
168
|
+
}
|
|
169
|
+
: {}),
|
|
170
|
+
...(eventName === "Stop"
|
|
171
|
+
? { last_assistant_message: input.response }
|
|
172
|
+
: {}),
|
|
173
|
+
},
|
|
174
|
+
eventName,
|
|
175
|
+
sessionId,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
102
178
|
if (agent === "polytoken") {
|
|
103
179
|
const eventName = rawEventName === "pre_user_prompt"
|
|
104
180
|
? "UserPromptSubmit"
|
|
@@ -181,6 +257,48 @@ function normalizeHookInput(input, agent, environment) {
|
|
|
181
257
|
sessionId,
|
|
182
258
|
};
|
|
183
259
|
}
|
|
260
|
+
class InvalidHookInputError extends Error {
|
|
261
|
+
constructor() {
|
|
262
|
+
super("Hook input does not match the configured agent event");
|
|
263
|
+
this.name = "InvalidHookInputError";
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function validHookInput(input, agent, eventName) {
|
|
267
|
+
const recognized = agent === "polytoken"
|
|
268
|
+
? eventName === "UserPromptSubmit" || eventName === "AssistantResponse"
|
|
269
|
+
: agent === "cursor"
|
|
270
|
+
? eventName === "UserPromptSubmit" ||
|
|
271
|
+
eventName === "AssistantResponse" ||
|
|
272
|
+
eventName === "PreToolUse" ||
|
|
273
|
+
eventName === "SessionEnd"
|
|
274
|
+
: eventName === "UserPromptSubmit" ||
|
|
275
|
+
eventName === "PreToolUse" ||
|
|
276
|
+
eventName === "Stop" ||
|
|
277
|
+
eventName === "SessionEnd";
|
|
278
|
+
if (!recognized) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
if (eventName === "UserPromptSubmit") {
|
|
282
|
+
return stringField(input.prompt) !== undefined;
|
|
283
|
+
}
|
|
284
|
+
if (eventName === "Stop" || eventName === "AssistantResponse") {
|
|
285
|
+
return stringField(input.last_assistant_message) !== undefined;
|
|
286
|
+
}
|
|
287
|
+
if (eventName === "PreToolUse") {
|
|
288
|
+
const toolName = stringField(input.tool_name);
|
|
289
|
+
if (toolName === undefined || !isObject(input.tool_input)) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
if (GUARD_EDIT_TOOLS.has(toolName)) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
if (GUARD_SHELL_TOOLS.has(toolName)) {
|
|
296
|
+
return stringField(input.tool_input.command) !== undefined;
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
184
302
|
function sha256(value) {
|
|
185
303
|
return createHash("sha256").update(value).digest("hex");
|
|
186
304
|
}
|
|
@@ -227,22 +345,16 @@ export function redactSecrets(text) {
|
|
|
227
345
|
})
|
|
228
346
|
.replace(/\b((?:set-)?cookie\s*:\s*)[^\r\n]+/giu, (_match, prefix) => `${prefix}[REDACTED:AUTHORIZATION]`);
|
|
229
347
|
}
|
|
230
|
-
async function atomicWriteJson(path, value
|
|
348
|
+
async function atomicWriteJson(path, value) {
|
|
231
349
|
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);
|
|
350
|
+
await chmod(dirname(path), 0o700);
|
|
351
|
+
await durableAtomicWriteJson(path, value);
|
|
240
352
|
}
|
|
241
353
|
async function readRuntimeConfig(home) {
|
|
242
354
|
try {
|
|
243
355
|
const parsed = JSON.parse(await readFile(resolve(loreDirectory(home), "config.json"), "utf8"));
|
|
244
356
|
if (!isObject(parsed) ||
|
|
245
|
-
parsed.version !== 1 ||
|
|
357
|
+
(parsed.version !== 1 && parsed.version !== 2) ||
|
|
246
358
|
typeof parsed.apiUrl !== "string" ||
|
|
247
359
|
typeof parsed.token !== "string" ||
|
|
248
360
|
!Array.isArray(parsed.agents)) {
|
|
@@ -259,10 +371,15 @@ async function readRuntimeConfig(home) {
|
|
|
259
371
|
/^https?:\/\//u.test(parsed.dashboardUrl)
|
|
260
372
|
? parsed.dashboardUrl.replace(/\/+$/u, "")
|
|
261
373
|
: undefined;
|
|
374
|
+
const workspaceId = typeof parsed.workspaceId === "string" &&
|
|
375
|
+
/^[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)
|
|
376
|
+
? parsed.workspaceId
|
|
377
|
+
: undefined;
|
|
262
378
|
return {
|
|
263
|
-
version:
|
|
379
|
+
version: parsed.version,
|
|
264
380
|
apiUrl: parsed.apiUrl,
|
|
265
381
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
382
|
+
...(workspaceId === undefined ? {} : { workspaceId }),
|
|
266
383
|
token: parsed.token,
|
|
267
384
|
agents,
|
|
268
385
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
@@ -291,11 +408,10 @@ async function savePending(input, agent, sessionId, now, home) {
|
|
|
291
408
|
capturedAt: now.toISOString(),
|
|
292
409
|
});
|
|
293
410
|
}
|
|
294
|
-
async function
|
|
411
|
+
async function readPending(agent, sessionId, home) {
|
|
295
412
|
const path = pendingPath(agent, sessionId, home);
|
|
296
413
|
try {
|
|
297
414
|
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
298
|
-
await rm(path, { force: true });
|
|
299
415
|
if (!isObject(parsed) ||
|
|
300
416
|
parsed.agent !== agent ||
|
|
301
417
|
parsed.sessionId !== sessionId ||
|
|
@@ -323,6 +439,9 @@ async function consumePending(agent, sessionId, home) {
|
|
|
323
439
|
return null;
|
|
324
440
|
}
|
|
325
441
|
}
|
|
442
|
+
async function clearPending(agent, sessionId, home) {
|
|
443
|
+
await durableUnlink(pendingPath(agent, sessionId, home), true);
|
|
444
|
+
}
|
|
326
445
|
function normalizedRepositoryPath(value) {
|
|
327
446
|
const parts = [];
|
|
328
447
|
for (const part of value.replaceAll("\\", "/").split("/")) {
|
|
@@ -539,6 +658,50 @@ function observationsUrl(apiUrl) {
|
|
|
539
658
|
function contextDeliveriesUrl(apiUrl) {
|
|
540
659
|
return `${apiUrl.replace(/\/+$/u, "")}/v1/context/deliveries`;
|
|
541
660
|
}
|
|
661
|
+
class RuntimeHttpError extends Error {
|
|
662
|
+
httpStatus;
|
|
663
|
+
constructor(message, httpStatus) {
|
|
664
|
+
super(message);
|
|
665
|
+
this.name = "RuntimeHttpError";
|
|
666
|
+
this.httpStatus = httpStatus;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async function boundedResponseJson(response) {
|
|
670
|
+
const declaredLength = response.headers.get("content-length");
|
|
671
|
+
if (declaredLength !== null &&
|
|
672
|
+
Number.isFinite(Number(declaredLength)) &&
|
|
673
|
+
Number(declaredLength) > MAX_RESPONSE_BYTES) {
|
|
674
|
+
await response.body?.cancel();
|
|
675
|
+
throw new Error("Lore response exceeds the reliability size limit");
|
|
676
|
+
}
|
|
677
|
+
if (response.body === null) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
const reader = response.body.getReader();
|
|
681
|
+
const chunks = [];
|
|
682
|
+
let bytes = 0;
|
|
683
|
+
try {
|
|
684
|
+
for (;;) {
|
|
685
|
+
const chunk = await reader.read();
|
|
686
|
+
if (chunk.done) {
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
bytes += chunk.value.byteLength;
|
|
690
|
+
if (bytes > MAX_RESPONSE_BYTES) {
|
|
691
|
+
await reader.cancel();
|
|
692
|
+
throw new Error("Lore response exceeds the reliability size limit");
|
|
693
|
+
}
|
|
694
|
+
chunks.push(chunk.value);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
finally {
|
|
698
|
+
reader.releaseLock();
|
|
699
|
+
}
|
|
700
|
+
if (bytes === 0) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
return JSON.parse(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)), bytes).toString("utf8"));
|
|
704
|
+
}
|
|
542
705
|
function boundedContext(value) {
|
|
543
706
|
return Array.from(value.trim()).slice(0, MAX_CONTEXT_CHARS).join("");
|
|
544
707
|
}
|
|
@@ -638,11 +801,276 @@ function deliveryFromResponse(value) {
|
|
|
638
801
|
delivered,
|
|
639
802
|
};
|
|
640
803
|
}
|
|
641
|
-
|
|
804
|
+
class IncompatibleReliabilityError extends Error {
|
|
805
|
+
incompatible = true;
|
|
806
|
+
}
|
|
807
|
+
async function protocolResponseJson(response) {
|
|
808
|
+
try {
|
|
809
|
+
return await boundedResponseJson(response);
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
if (isContextTransportFailure(error)) {
|
|
813
|
+
throw error;
|
|
814
|
+
}
|
|
815
|
+
throw new IncompatibleReliabilityError("Lore response is not valid bounded JSON");
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function deliveryFromSignedContext(payload, expectedRequest) {
|
|
819
|
+
const request = isObject(payload.request) ? payload.request : null;
|
|
820
|
+
if (request === null ||
|
|
821
|
+
canonicalJson(request) !== canonicalJson(expectedRequest) ||
|
|
822
|
+
typeof payload.eventId !== "string" ||
|
|
823
|
+
typeof payload.receiptId !== "string" ||
|
|
824
|
+
typeof payload.context !== "string" ||
|
|
825
|
+
!Array.isArray(payload.memories) ||
|
|
826
|
+
!Array.isArray(payload.hits) ||
|
|
827
|
+
!isObject(payload.packing)) {
|
|
828
|
+
throw new IncompatibleReliabilityError("Lore context snapshot does not bind the request or signed delivery");
|
|
829
|
+
}
|
|
830
|
+
return deliveryFromResponse({
|
|
831
|
+
context: payload.context,
|
|
832
|
+
receipt: { id: payload.receiptId },
|
|
833
|
+
memories: payload.memories,
|
|
834
|
+
hits: payload.hits,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
function retrievalReliability(value, payload) {
|
|
838
|
+
const metadata = isObject(value) ? value : null;
|
|
839
|
+
const freshness = isObject(metadata?.freshness)
|
|
840
|
+
? metadata.freshness
|
|
841
|
+
: null;
|
|
842
|
+
const policy = isObject(metadata?.policy) ? metadata.policy : null;
|
|
843
|
+
const reasons = Array.isArray(metadata?.reasons)
|
|
844
|
+
? metadata.reasons.filter((reason) => typeof reason === "string")
|
|
845
|
+
: [];
|
|
846
|
+
const policySource = policy?.source;
|
|
847
|
+
const policyLoadedAt = policy?.loadedAt;
|
|
848
|
+
const policyValidUntil = policy?.validUntil;
|
|
849
|
+
if (metadata === null ||
|
|
850
|
+
metadata.contractVersion !== "reliability-v1" ||
|
|
851
|
+
Object.keys(metadata).length !== 9 ||
|
|
852
|
+
metadata.operation !== "retrieval" ||
|
|
853
|
+
!(metadata.requestId === null ||
|
|
854
|
+
typeof metadata.requestId === "string") ||
|
|
855
|
+
!["ok", "degraded", "failed"].includes(String(metadata.status)) ||
|
|
856
|
+
!["live", "cache", "none"].includes(String(metadata.source)) ||
|
|
857
|
+
!["none", "live_lexical", "cached_context"].includes(String(metadata.fallback)) ||
|
|
858
|
+
freshness === null ||
|
|
859
|
+
Object.keys(freshness).length !== 4 ||
|
|
860
|
+
!["fresh", "stale", "unknown"].includes(String(freshness.state)) ||
|
|
861
|
+
typeof freshness.asOf !== "string" ||
|
|
862
|
+
freshness.asOf !== payload.asOf ||
|
|
863
|
+
typeof freshness.ageMs !== "number" ||
|
|
864
|
+
!Number.isSafeInteger(freshness.ageMs) ||
|
|
865
|
+
freshness.ageMs < 0 ||
|
|
866
|
+
typeof freshness.validUntil !== "string" ||
|
|
867
|
+
freshness.validUntil !== payload.validUntil ||
|
|
868
|
+
policy === null ||
|
|
869
|
+
Object.keys(policy).length !== 4 ||
|
|
870
|
+
!["live", "cache", "none"].includes(String(policySource)) ||
|
|
871
|
+
(policy.version !== null && typeof policy.version !== "string") ||
|
|
872
|
+
(policy.version !== null && policy.version !== payload.policyVersion) ||
|
|
873
|
+
!((policySource === "none" &&
|
|
874
|
+
policy.version === null &&
|
|
875
|
+
policyLoadedAt === null &&
|
|
876
|
+
policyValidUntil === null) ||
|
|
877
|
+
(["live", "cache"].includes(String(policySource)) &&
|
|
878
|
+
typeof policy.version === "string" &&
|
|
879
|
+
typeof policyLoadedAt === "string" &&
|
|
880
|
+
Number.isFinite(Date.parse(policyLoadedAt)) &&
|
|
881
|
+
(policyValidUntil === null ||
|
|
882
|
+
(typeof policyValidUntil === "string" &&
|
|
883
|
+
Number.isFinite(Date.parse(policyValidUntil)))) &&
|
|
884
|
+
policyValidUntil === payload.validUntil &&
|
|
885
|
+
policyLoadedAt ===
|
|
886
|
+
(policySource === "cache" ? payload.asOf : payload.issuedAt))) ||
|
|
887
|
+
!Array.isArray(metadata.reasons) ||
|
|
888
|
+
reasons.length === 0 ||
|
|
889
|
+
reasons.length > 8 ||
|
|
890
|
+
reasons.length !== metadata.reasons.length ||
|
|
891
|
+
new Set(reasons).size !== reasons.length ||
|
|
892
|
+
reasons.some((reason) => !RELIABILITY_REASON_CODES.has(reason)) ||
|
|
893
|
+
(metadata.requestId !== null &&
|
|
894
|
+
metadata.requestId !== payload.requestId) ||
|
|
895
|
+
(metadata.source === "live" && freshness.state !== "fresh") ||
|
|
896
|
+
(metadata.source === "live" && policySource !== "live") ||
|
|
897
|
+
(metadata.source === "cache" &&
|
|
898
|
+
(metadata.fallback !== "cached_context" ||
|
|
899
|
+
policySource !== "cache" ||
|
|
900
|
+
typeof policyValidUntil !== "string" ||
|
|
901
|
+
!["fresh", "stale"].includes(String(freshness.state)))) ||
|
|
902
|
+
(metadata.fallback === "live_lexical" &&
|
|
903
|
+
(metadata.source !== "live" ||
|
|
904
|
+
metadata.status !== "degraded" ||
|
|
905
|
+
!reasons.includes("lexical_fallback"))) ||
|
|
906
|
+
(metadata.fallback === "cached_context" &&
|
|
907
|
+
!reasons.includes("cached_context")) ||
|
|
908
|
+
(metadata.status === "ok" && metadata.fallback !== "none")
|
|
909
|
+
|| (metadata.status === "degraded" && metadata.fallback === "none")
|
|
910
|
+
|| (metadata.status === "failed" &&
|
|
911
|
+
(metadata.source !== "none" || metadata.fallback !== "none"))) {
|
|
912
|
+
throw new IncompatibleReliabilityError("Lore retrieval reliability metadata is invalid");
|
|
913
|
+
}
|
|
914
|
+
const cached = metadata.source === "cache";
|
|
915
|
+
const nowMs = Date.now();
|
|
916
|
+
return {
|
|
917
|
+
status: metadata.status,
|
|
918
|
+
source: metadata.source,
|
|
919
|
+
fallback: metadata.fallback,
|
|
920
|
+
reasons,
|
|
921
|
+
freshness: {
|
|
922
|
+
state: cached
|
|
923
|
+
? nowMs < Date.parse(payload.validUntil)
|
|
924
|
+
? "fresh"
|
|
925
|
+
: "stale"
|
|
926
|
+
: freshness.state,
|
|
927
|
+
asOf: payload.asOf,
|
|
928
|
+
ageMs: cached
|
|
929
|
+
? Math.max(0, nowMs - Date.parse(payload.asOf))
|
|
930
|
+
: freshness.ageMs,
|
|
931
|
+
validUntil: payload.validUntil,
|
|
932
|
+
},
|
|
933
|
+
policyVersion: typeof policy.version === "string" ? policy.version : null,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function reliabilityWorkspaceKey(config) {
|
|
937
|
+
return (config.workspaceId ??
|
|
938
|
+
credentialReliabilityWorkspaceKey(config));
|
|
939
|
+
}
|
|
940
|
+
function credentialReliabilityWorkspaceKey(config) {
|
|
941
|
+
return `credential-${sha256(`${config.apiUrl}\0${config.token}`).slice(0, 32)}`;
|
|
942
|
+
}
|
|
943
|
+
function runtimeAccept(config) {
|
|
944
|
+
return config.version === 2
|
|
945
|
+
? RELIABILITY_V1_MEDIA_TYPE
|
|
946
|
+
: "application/json";
|
|
947
|
+
}
|
|
948
|
+
async function runtimeStore(config, home) {
|
|
949
|
+
const store = new ReliabilityStore(reliabilityWorkspaceKey(config), {
|
|
950
|
+
...(home === undefined ? {} : { home }),
|
|
951
|
+
});
|
|
952
|
+
await store.initialize();
|
|
953
|
+
await store.migrateLegacyQueue(queueDirectory(home));
|
|
954
|
+
if (config.workspaceId !== undefined) {
|
|
955
|
+
const credentialStore = new ReliabilityStore(credentialReliabilityWorkspaceKey(config), { ...(home === undefined ? {} : { home }) });
|
|
956
|
+
await credentialStore.transferPendingTo(store).catch(() => undefined);
|
|
957
|
+
}
|
|
958
|
+
return store;
|
|
959
|
+
}
|
|
960
|
+
function publicKeySet(value) {
|
|
961
|
+
if (!isObject(value) ||
|
|
962
|
+
typeof value.workspaceId !== "string" ||
|
|
963
|
+
!Array.isArray(value.keys)) {
|
|
964
|
+
throw new IncompatibleReliabilityError("Lore public key response is invalid");
|
|
965
|
+
}
|
|
966
|
+
const keys = value.keys.map((candidate) => {
|
|
967
|
+
if (!isObject(candidate) ||
|
|
968
|
+
typeof candidate.kid !== "string" ||
|
|
969
|
+
candidate.kty !== "OKP" ||
|
|
970
|
+
candidate.crv !== "Ed25519" ||
|
|
971
|
+
candidate.alg !== "EdDSA" ||
|
|
972
|
+
candidate.use !== "sig" ||
|
|
973
|
+
typeof candidate.x !== "string" ||
|
|
974
|
+
typeof candidate.notBefore !== "string" ||
|
|
975
|
+
!(candidate.retiredAt === null ||
|
|
976
|
+
typeof candidate.retiredAt === "string")) {
|
|
977
|
+
throw new IncompatibleReliabilityError("Lore public key response contains an invalid key");
|
|
978
|
+
}
|
|
979
|
+
return {
|
|
980
|
+
workspaceId: value.workspaceId,
|
|
981
|
+
kid: candidate.kid,
|
|
982
|
+
kty: "OKP",
|
|
983
|
+
crv: "Ed25519",
|
|
984
|
+
alg: "EdDSA",
|
|
985
|
+
use: "sig",
|
|
986
|
+
x: candidate.x,
|
|
987
|
+
notBefore: candidate.notBefore,
|
|
988
|
+
retiredAt: candidate.retiredAt,
|
|
989
|
+
};
|
|
990
|
+
});
|
|
991
|
+
if (keys.length === 0) {
|
|
992
|
+
throw new IncompatibleReliabilityError("Lore public key response is empty");
|
|
993
|
+
}
|
|
994
|
+
return { workspaceId: value.workspaceId, keys };
|
|
995
|
+
}
|
|
996
|
+
async function refreshTrustKeys(config, store, fetchImplementation) {
|
|
997
|
+
const response = await fetchImplementation(`${config.apiUrl.replace(/\/+$/u, "")}/v1/workspace/identity/keys`, {
|
|
998
|
+
headers: {
|
|
999
|
+
accept: RELIABILITY_V1_MEDIA_TYPE,
|
|
1000
|
+
authorization: `Bearer ${config.token}`,
|
|
1001
|
+
"user-agent": `lore-cli/${RUNTIME_VERSION}`,
|
|
1002
|
+
},
|
|
1003
|
+
signal: AbortSignal.timeout(config.timeoutMs ?? 2_500),
|
|
1004
|
+
});
|
|
1005
|
+
if (!response.ok) {
|
|
1006
|
+
throw new RuntimeHttpError(`Lore key discovery failed with HTTP ${response.status}`, response.status);
|
|
1007
|
+
}
|
|
1008
|
+
const keys = publicKeySet(await protocolResponseJson(response));
|
|
1009
|
+
if (config.workspaceId !== undefined &&
|
|
1010
|
+
keys.workspaceId !== config.workspaceId) {
|
|
1011
|
+
throw new IncompatibleReliabilityError("Lore public keys belong to a different workspace");
|
|
1012
|
+
}
|
|
1013
|
+
if (store.workspaceId === keys.workspaceId) {
|
|
1014
|
+
await store.writePublicTrustKeys(keys.keys.map((key) => ({
|
|
1015
|
+
...key,
|
|
1016
|
+
workspaceId: keys.workspaceId,
|
|
1017
|
+
})));
|
|
1018
|
+
await store.writeState(TRUST_KEY_REFRESH_STATE_KEY, {
|
|
1019
|
+
refreshedAt: new Date().toISOString(),
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
return keys;
|
|
1023
|
+
}
|
|
1024
|
+
async function pinnedKeys(config, store, fetchImplementation) {
|
|
1025
|
+
const keys = await store.readPublicTrustKeys();
|
|
1026
|
+
const refreshState = await store.readState(TRUST_KEY_REFRESH_STATE_KEY);
|
|
1027
|
+
const refreshedAt = isObject(refreshState)
|
|
1028
|
+
? refreshState.refreshedAt
|
|
1029
|
+
: undefined;
|
|
1030
|
+
const refreshedAtMs = typeof refreshedAt === "string" ? Date.parse(refreshedAt) : Number.NaN;
|
|
1031
|
+
if (keys.length > 0 &&
|
|
1032
|
+
Number.isFinite(refreshedAtMs) &&
|
|
1033
|
+
Date.now() - refreshedAtMs < TRUST_KEY_REFRESH_TTL_MS &&
|
|
1034
|
+
refreshedAtMs <= Date.now() + 5 * 60_000) {
|
|
1035
|
+
return { workspaceId: store.workspaceId, keys };
|
|
1036
|
+
}
|
|
1037
|
+
return refreshTrustKeys(config, store, fetchImplementation);
|
|
1038
|
+
}
|
|
1039
|
+
async function verifySnapshot(compactJws, kind, config, store, fetchImplementation, allowExpired = false) {
|
|
1040
|
+
const verifyWith = (keys) => verifySignedSnapshot(compactJws, {
|
|
1041
|
+
workspaceId: keys.workspaceId,
|
|
1042
|
+
kind,
|
|
1043
|
+
keys,
|
|
1044
|
+
allowExpired,
|
|
1045
|
+
}).payload;
|
|
1046
|
+
try {
|
|
1047
|
+
return verifyWith(await pinnedKeys(config, store, fetchImplementation));
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
if (!(error instanceof SignedSnapshotError) ||
|
|
1051
|
+
error.code !== "UNTRUSTED_KEY") {
|
|
1052
|
+
throw error;
|
|
1053
|
+
}
|
|
1054
|
+
return verifyWith(await refreshTrustKeys(config, store, fetchImplementation));
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function committedAcknowledgement(value, externalEventId, serverEventId) {
|
|
1058
|
+
if (!isObject(value) || !isObject(value.acknowledgement)) {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
const acknowledgement = value.acknowledgement;
|
|
1062
|
+
return (acknowledgement.state === "committed_server" &&
|
|
1063
|
+
acknowledgement.durability === "workspace_database" &&
|
|
1064
|
+
acknowledgement.replayPending === false &&
|
|
1065
|
+
acknowledgement.eventId === serverEventId &&
|
|
1066
|
+
acknowledgement.idempotencyKey === externalEventId);
|
|
1067
|
+
}
|
|
1068
|
+
async function postTurn(config, request, fetchImplementation, store) {
|
|
642
1069
|
const { idempotencyKey, ...body } = request;
|
|
643
1070
|
const response = await fetchImplementation(turnsUrl(config.apiUrl), {
|
|
644
1071
|
method: "POST",
|
|
645
1072
|
headers: {
|
|
1073
|
+
accept: runtimeAccept(config),
|
|
646
1074
|
authorization: `Bearer ${config.token}`,
|
|
647
1075
|
"content-type": "application/json",
|
|
648
1076
|
"idempotency-key": idempotencyKey,
|
|
@@ -652,18 +1080,70 @@ async function postTurn(config, request, fetchImplementation) {
|
|
|
652
1080
|
signal: AbortSignal.timeout(config.timeoutMs ?? 2_500),
|
|
653
1081
|
});
|
|
654
1082
|
if (!response.ok) {
|
|
655
|
-
throw new
|
|
656
|
-
}
|
|
657
|
-
const
|
|
658
|
-
if (
|
|
659
|
-
return
|
|
1083
|
+
throw new RuntimeHttpError(`Lore turn request failed with HTTP ${response.status}`, response.status);
|
|
1084
|
+
}
|
|
1085
|
+
const value = await protocolResponseJson(response);
|
|
1086
|
+
if (config.version === 1) {
|
|
1087
|
+
return deliveryFromResponse(value);
|
|
1088
|
+
}
|
|
1089
|
+
if (!isObject(value) ||
|
|
1090
|
+
!isObject(value.turn) ||
|
|
1091
|
+
!isObject(value.capture) ||
|
|
1092
|
+
typeof value.contextSnapshot !== "string" ||
|
|
1093
|
+
!isObject(value.turn.event) ||
|
|
1094
|
+
typeof value.turn.event.id !== "string" ||
|
|
1095
|
+
!isObject(value.retrieval) ||
|
|
1096
|
+
!committedAcknowledgement(value.capture, request.eventId, value.turn.event.id)) {
|
|
1097
|
+
throw new IncompatibleReliabilityError("Lore turn response has no matching durable acknowledgement");
|
|
1098
|
+
}
|
|
1099
|
+
const payload = await verifySnapshot(value.contextSnapshot, "context", config, store, fetchImplementation, true);
|
|
1100
|
+
if (payload.eventId !== value.turn.event.id ||
|
|
1101
|
+
!isObject(value.turn.receipt) ||
|
|
1102
|
+
payload.receiptId !== value.turn.receipt.id) {
|
|
1103
|
+
throw new IncompatibleReliabilityError("Lore turn snapshot does not bind the returned event and receipt");
|
|
1104
|
+
}
|
|
1105
|
+
const reliability = retrievalReliability(value.retrieval, payload);
|
|
1106
|
+
if (Date.now() - Date.parse(payload.validUntil) >
|
|
1107
|
+
CONTEXT_CACHE_MAX_STALE_MS) {
|
|
1108
|
+
return {
|
|
1109
|
+
...emptyDelivery(),
|
|
1110
|
+
reliability: {
|
|
1111
|
+
status: "failed",
|
|
1112
|
+
source: "none",
|
|
1113
|
+
fallback: "none",
|
|
1114
|
+
reasons: ["no_usable_cache"],
|
|
1115
|
+
freshness: {
|
|
1116
|
+
state: "unknown",
|
|
1117
|
+
asOf: null,
|
|
1118
|
+
ageMs: null,
|
|
1119
|
+
validUntil: null,
|
|
1120
|
+
},
|
|
1121
|
+
policyVersion: null,
|
|
1122
|
+
},
|
|
1123
|
+
};
|
|
660
1124
|
}
|
|
661
|
-
|
|
1125
|
+
await store
|
|
1126
|
+
.writeContextSnapshot(request.eventId, {
|
|
1127
|
+
compactJws: value.contextSnapshot,
|
|
1128
|
+
})
|
|
1129
|
+
.catch(() => undefined);
|
|
1130
|
+
const delivery = deliveryFromSignedContext(payload, {
|
|
1131
|
+
connector: request.connector,
|
|
1132
|
+
eventId: request.eventId,
|
|
1133
|
+
sessionId: request.sessionId,
|
|
1134
|
+
task: {
|
|
1135
|
+
agent: request.agent,
|
|
1136
|
+
...(request.scope === undefined ? {} : { scope: request.scope }),
|
|
1137
|
+
task: request.currentUser.content,
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1140
|
+
return { ...delivery, reliability };
|
|
662
1141
|
}
|
|
663
|
-
async function postPromptObservation(config, request, fetchImplementation) {
|
|
1142
|
+
async function postPromptObservation(config, request, fetchImplementation, _store) {
|
|
664
1143
|
const response = await fetchImplementation(observationsUrl(config.apiUrl), {
|
|
665
1144
|
method: "POST",
|
|
666
1145
|
headers: {
|
|
1146
|
+
accept: runtimeAccept(config),
|
|
667
1147
|
authorization: `Bearer ${config.token}`,
|
|
668
1148
|
"content-type": "application/json",
|
|
669
1149
|
"idempotency-key": request.idempotencyKey,
|
|
@@ -695,71 +1175,254 @@ async function postPromptObservation(config, request, fetchImplementation) {
|
|
|
695
1175
|
signal: AbortSignal.timeout(config.timeoutMs ?? 2_500),
|
|
696
1176
|
});
|
|
697
1177
|
if (!response.ok) {
|
|
698
|
-
throw new
|
|
1178
|
+
throw new RuntimeHttpError(`Lore prompt observation failed with HTTP ${response.status}`, response.status);
|
|
1179
|
+
}
|
|
1180
|
+
const value = await protocolResponseJson(response);
|
|
1181
|
+
if (config.version === 1) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (!isObject(value) ||
|
|
1185
|
+
!isObject(value.observation) ||
|
|
1186
|
+
!isObject(value.observation.event) ||
|
|
1187
|
+
typeof value.observation.event.id !== "string" ||
|
|
1188
|
+
!committedAcknowledgement(value.capture, request.eventId, value.observation.event.id)) {
|
|
1189
|
+
throw new IncompatibleReliabilityError("Lore observation response has no matching durable acknowledgement");
|
|
699
1190
|
}
|
|
700
|
-
await response.body?.cancel();
|
|
701
1191
|
}
|
|
702
|
-
async function getPromptContext(config, agent, sessionId, sourceEventId, prompt, gitContext, fetchImplementation) {
|
|
1192
|
+
async function getPromptContext(config, agent, sessionId, sourceEventId, prompt, gitContext, fetchImplementation, store) {
|
|
1193
|
+
const deliveryEventId = deterministicUuid(`lore-context\0${sourceEventId}`);
|
|
1194
|
+
const deliveryRequest = {
|
|
1195
|
+
connector: "lore-cli",
|
|
1196
|
+
eventId: deliveryEventId,
|
|
1197
|
+
sessionId,
|
|
1198
|
+
task: {
|
|
1199
|
+
agent,
|
|
1200
|
+
...(gitContext.scope === undefined
|
|
1201
|
+
? {}
|
|
1202
|
+
: { scope: gitContext.scope }),
|
|
1203
|
+
task: redactSecrets(prompt),
|
|
1204
|
+
...(gitContext.diff === undefined ? {} : { diff: gitContext.diff }),
|
|
1205
|
+
...(gitContext.files === undefined
|
|
1206
|
+
? {}
|
|
1207
|
+
: { files: gitContext.files }),
|
|
1208
|
+
},
|
|
1209
|
+
};
|
|
703
1210
|
const response = await fetchImplementation(contextDeliveriesUrl(config.apiUrl), {
|
|
704
1211
|
method: "POST",
|
|
705
1212
|
headers: {
|
|
1213
|
+
accept: runtimeAccept(config),
|
|
706
1214
|
authorization: `Bearer ${config.token}`,
|
|
707
1215
|
"content-type": "application/json",
|
|
708
1216
|
"user-agent": `lore-cli/${RUNTIME_VERSION}`,
|
|
709
1217
|
},
|
|
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
|
-
}),
|
|
1218
|
+
body: JSON.stringify(deliveryRequest),
|
|
726
1219
|
signal: AbortSignal.timeout(config.timeoutMs ?? 2_500),
|
|
727
1220
|
});
|
|
728
1221
|
if (!response.ok) {
|
|
729
|
-
throw new
|
|
1222
|
+
throw new RuntimeHttpError(`Lore context request failed with HTTP ${response.status}`, response.status);
|
|
1223
|
+
}
|
|
1224
|
+
const value = await protocolResponseJson(response);
|
|
1225
|
+
if (config.version === 1) {
|
|
1226
|
+
return deliveryFromResponse(value);
|
|
1227
|
+
}
|
|
1228
|
+
if (!isObject(value) ||
|
|
1229
|
+
!isObject(value.delivery) ||
|
|
1230
|
+
!isObject(value.reliability) ||
|
|
1231
|
+
typeof value.snapshot !== "string" ||
|
|
1232
|
+
!isObject(value.delivery.event) ||
|
|
1233
|
+
typeof value.delivery.event.id !== "string" ||
|
|
1234
|
+
!isObject(value.delivery.receipt) ||
|
|
1235
|
+
typeof value.delivery.receipt.id !== "string") {
|
|
1236
|
+
throw new IncompatibleReliabilityError("Lore context response has no signed reliability snapshot");
|
|
1237
|
+
}
|
|
1238
|
+
const payload = await verifySnapshot(value.snapshot, "context", config, store, fetchImplementation, true);
|
|
1239
|
+
if (payload.eventId !== value.delivery.event.id ||
|
|
1240
|
+
payload.receiptId !== value.delivery.receipt.id) {
|
|
1241
|
+
throw new IncompatibleReliabilityError("Lore context snapshot does not bind the returned event and receipt");
|
|
1242
|
+
}
|
|
1243
|
+
const reliability = retrievalReliability(value.reliability, payload);
|
|
1244
|
+
if (Date.now() - Date.parse(payload.validUntil) >
|
|
1245
|
+
CONTEXT_CACHE_MAX_STALE_MS) {
|
|
1246
|
+
return {
|
|
1247
|
+
...emptyDelivery(),
|
|
1248
|
+
reliability: {
|
|
1249
|
+
status: "failed",
|
|
1250
|
+
source: "none",
|
|
1251
|
+
fallback: "none",
|
|
1252
|
+
reasons: ["no_usable_cache"],
|
|
1253
|
+
freshness: {
|
|
1254
|
+
state: "unknown",
|
|
1255
|
+
asOf: null,
|
|
1256
|
+
ageMs: null,
|
|
1257
|
+
validUntil: null,
|
|
1258
|
+
},
|
|
1259
|
+
policyVersion: null,
|
|
1260
|
+
},
|
|
1261
|
+
};
|
|
730
1262
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1263
|
+
await store
|
|
1264
|
+
.writeContextSnapshot(sourceEventId, { compactJws: value.snapshot })
|
|
1265
|
+
.catch(() => undefined);
|
|
1266
|
+
return {
|
|
1267
|
+
...deliveryFromSignedContext(payload, deliveryRequest),
|
|
1268
|
+
reliability,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
async function cachedSnapshotKeys(config, store, now) {
|
|
1272
|
+
if (config.workspaceId === undefined || store.workspaceId !== config.workspaceId) {
|
|
1273
|
+
return null;
|
|
734
1274
|
}
|
|
735
|
-
|
|
1275
|
+
const [keys, refreshState] = await Promise.all([
|
|
1276
|
+
store.readPublicTrustKeys(),
|
|
1277
|
+
store.readState(TRUST_KEY_REFRESH_STATE_KEY),
|
|
1278
|
+
]);
|
|
1279
|
+
const refreshedAt = isObject(refreshState)
|
|
1280
|
+
? refreshState.refreshedAt
|
|
1281
|
+
: undefined;
|
|
1282
|
+
const refreshedAtMs = typeof refreshedAt === "string" ? Date.parse(refreshedAt) : Number.NaN;
|
|
1283
|
+
if (keys.length === 0 ||
|
|
1284
|
+
!Number.isFinite(refreshedAtMs) ||
|
|
1285
|
+
now.getTime() - refreshedAtMs >= TRUST_KEY_REFRESH_TTL_MS ||
|
|
1286
|
+
refreshedAtMs > now.getTime() + 5 * 60_000) {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
return { workspaceId: config.workspaceId, keys };
|
|
736
1290
|
}
|
|
737
|
-
function
|
|
738
|
-
|
|
1291
|
+
async function getCachedPromptContext(config, prompt, gitContext, store, now) {
|
|
1292
|
+
const keys = await cachedSnapshotKeys(config, store, now);
|
|
1293
|
+
if (keys === null || config.workspaceId === undefined) {
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
const scan = await store.listContextSnapshots();
|
|
1297
|
+
const candidates = [];
|
|
1298
|
+
for (const record of scan.records) {
|
|
1299
|
+
const value = isObject(record.value) ? record.value : null;
|
|
1300
|
+
if (value === null || typeof value.compactJws !== "string") {
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
try {
|
|
1304
|
+
const payload = verifySignedSnapshot(value.compactJws, {
|
|
1305
|
+
workspaceId: config.workspaceId,
|
|
1306
|
+
kind: "context",
|
|
1307
|
+
keys,
|
|
1308
|
+
allowExpired: true,
|
|
1309
|
+
now,
|
|
1310
|
+
}).payload;
|
|
1311
|
+
candidates.push({
|
|
1312
|
+
cacheKey: record.cacheKey,
|
|
1313
|
+
compactJws: value.compactJws,
|
|
1314
|
+
payload,
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
catch {
|
|
1318
|
+
// One invalid cache entry must not hide another verified candidate.
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const task = {
|
|
1322
|
+
workspaceId: config.workspaceId,
|
|
1323
|
+
task: redactSecrets(prompt),
|
|
1324
|
+
...(gitContext.scope === undefined ? {} : { scope: gitContext.scope }),
|
|
1325
|
+
...(gitContext.files === undefined ? {} : { files: gitContext.files }),
|
|
1326
|
+
};
|
|
1327
|
+
const selected = selectCachedContext(candidates, task, now);
|
|
1328
|
+
if (selected === null) {
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
return {
|
|
1332
|
+
context: selected.context,
|
|
1333
|
+
learned: [],
|
|
1334
|
+
delivered: [],
|
|
1335
|
+
reliability: {
|
|
1336
|
+
status: "degraded",
|
|
1337
|
+
source: "cache",
|
|
1338
|
+
fallback: "cached_context",
|
|
1339
|
+
reasons: ["live_unavailable", "cached_context"],
|
|
1340
|
+
freshness: selected.freshness,
|
|
1341
|
+
policyVersion: selected.payload.policyVersion,
|
|
1342
|
+
},
|
|
1343
|
+
};
|
|
739
1344
|
}
|
|
740
|
-
async function
|
|
741
|
-
const directory = queueDirectory(home);
|
|
742
|
-
let entries;
|
|
1345
|
+
async function getPromptContextWithFallback(config, agent, sessionId, sourceEventId, prompt, gitContext, fetchImplementation, store, now) {
|
|
743
1346
|
try {
|
|
744
|
-
|
|
745
|
-
.filter((entry) => entry.endsWith(".json"))
|
|
746
|
-
.sort();
|
|
1347
|
+
return await getPromptContext(config, agent, sessionId, sourceEventId, prompt, gitContext, fetchImplementation, store);
|
|
747
1348
|
}
|
|
748
|
-
catch {
|
|
749
|
-
|
|
1349
|
+
catch (error) {
|
|
1350
|
+
if (isContextTransportFailure(error)) {
|
|
1351
|
+
const cached = await getCachedPromptContext(config, prompt, gitContext, store, now).catch(() => null);
|
|
1352
|
+
if (cached !== null) {
|
|
1353
|
+
return cached;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return {
|
|
1357
|
+
...emptyDelivery(),
|
|
1358
|
+
reliability: {
|
|
1359
|
+
status: "failed",
|
|
1360
|
+
source: "none",
|
|
1361
|
+
fallback: "none",
|
|
1362
|
+
reasons: [
|
|
1363
|
+
...(isContextTransportFailure(error) ? ["live_unavailable"] : []),
|
|
1364
|
+
isContextTransportFailure(error)
|
|
1365
|
+
? "no_usable_cache"
|
|
1366
|
+
: "invalid_response",
|
|
1367
|
+
],
|
|
1368
|
+
freshness: {
|
|
1369
|
+
state: "unknown",
|
|
1370
|
+
asOf: null,
|
|
1371
|
+
ageMs: null,
|
|
1372
|
+
validUntil: null,
|
|
1373
|
+
},
|
|
1374
|
+
policyVersion: null,
|
|
1375
|
+
},
|
|
1376
|
+
};
|
|
750
1377
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1378
|
+
}
|
|
1379
|
+
function formatAge(ageMs) {
|
|
1380
|
+
if (ageMs === null || ageMs < 60_000) {
|
|
1381
|
+
return "less than a minute";
|
|
1382
|
+
}
|
|
1383
|
+
if (ageMs < 60 * 60_000) {
|
|
1384
|
+
return `${Math.floor(ageMs / 60_000)}m`;
|
|
1385
|
+
}
|
|
1386
|
+
if (ageMs < 24 * 60 * 60_000) {
|
|
1387
|
+
return `${Math.floor(ageMs / (60 * 60_000))}h`;
|
|
754
1388
|
}
|
|
755
|
-
|
|
756
|
-
.slice(0, excess)
|
|
757
|
-
.map(async (entry) => rm(resolve(directory, entry), { force: true })));
|
|
1389
|
+
return `${Math.floor(ageMs / (24 * 60 * 60_000))}d`;
|
|
758
1390
|
}
|
|
759
|
-
async function
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1391
|
+
async function retrievalNotice(delivery, sessionId, store, now) {
|
|
1392
|
+
const reliability = delivery.reliability;
|
|
1393
|
+
if (reliability === undefined || reliability.status === "ok") {
|
|
1394
|
+
return undefined;
|
|
1395
|
+
}
|
|
1396
|
+
if (reliability.fallback === "cached_context") {
|
|
1397
|
+
return `Lore used verified ${reliability.freshness.state} cached context (${formatAge(reliability.freshness.ageMs)} old) because live retrieval was unavailable.`;
|
|
1398
|
+
}
|
|
1399
|
+
const notice = reliability.fallback === "live_lexical"
|
|
1400
|
+
? "Lore used live lexical retrieval because semantic retrieval was unavailable."
|
|
1401
|
+
: reliability.reasons.includes("invalid_response")
|
|
1402
|
+
? "Lore context could not be trusted; continuing without injected context."
|
|
1403
|
+
: "Lore context is temporarily unavailable; continuing without injected context.";
|
|
1404
|
+
const key = `context-notice:${sessionId}`;
|
|
1405
|
+
const previous = await store.readState(key).catch(() => null);
|
|
1406
|
+
if (isObject(previous) &&
|
|
1407
|
+
previous.notice === notice &&
|
|
1408
|
+
typeof previous.at === "string" &&
|
|
1409
|
+
now.getTime() - Date.parse(previous.at) < CONTEXT_NOTICE_COOLDOWN_MS) {
|
|
1410
|
+
return undefined;
|
|
1411
|
+
}
|
|
1412
|
+
await store
|
|
1413
|
+
.writeState(key, { notice, at: now.toISOString() }, now)
|
|
1414
|
+
.catch(() => undefined);
|
|
1415
|
+
return notice;
|
|
1416
|
+
}
|
|
1417
|
+
function queueDirectory(home) {
|
|
1418
|
+
return resolve(loreDirectory(home), "queue");
|
|
1419
|
+
}
|
|
1420
|
+
async function enqueueCapture(store, queued) {
|
|
1421
|
+
return (await store.enqueue({
|
|
1422
|
+
kind: queued.kind,
|
|
1423
|
+
idempotencyKey: queued.request.eventId,
|
|
1424
|
+
payload: queued,
|
|
1425
|
+
})).entry;
|
|
763
1426
|
}
|
|
764
1427
|
function isTurnRequest(value) {
|
|
765
1428
|
return (isObject(value) &&
|
|
@@ -799,38 +1462,57 @@ function queuedRequest(value) {
|
|
|
799
1462
|
}
|
|
800
1463
|
return null;
|
|
801
1464
|
}
|
|
802
|
-
async function
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}
|
|
810
|
-
catch {
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
if (first === undefined) {
|
|
814
|
-
return;
|
|
1465
|
+
async function flushOne(config, fetchImplementation, store) {
|
|
1466
|
+
const claimed = await store.claimNext({
|
|
1467
|
+
workerId: `native-hook:${process.pid}`,
|
|
1468
|
+
kinds: ["turn", "prompt"],
|
|
1469
|
+
});
|
|
1470
|
+
if (claimed === null || claimed.claim === undefined) {
|
|
1471
|
+
return null;
|
|
815
1472
|
}
|
|
816
|
-
const path = resolve(directory, first);
|
|
817
1473
|
try {
|
|
818
|
-
const
|
|
819
|
-
const queued = queuedRequest(parsed);
|
|
1474
|
+
const queued = queuedRequest(claimed.payload);
|
|
820
1475
|
if (queued === null) {
|
|
821
|
-
await
|
|
822
|
-
|
|
1476
|
+
const failed = await store.failClaim({
|
|
1477
|
+
claimId: claimed.claim.id,
|
|
1478
|
+
classification: "incompatible",
|
|
1479
|
+
message: "Durable capture payload is incompatible",
|
|
1480
|
+
});
|
|
1481
|
+
return {
|
|
1482
|
+
idempotencyKey: claimed.idempotencyKey,
|
|
1483
|
+
state: failed.state === "auth-blocked" ? "auth-blocked" : "dead",
|
|
1484
|
+
};
|
|
823
1485
|
}
|
|
1486
|
+
let delivery;
|
|
824
1487
|
if (queued.kind === "turn") {
|
|
825
|
-
await postTurn(config, queued.request, fetchImplementation);
|
|
1488
|
+
delivery = await postTurn(config, queued.request, fetchImplementation, store);
|
|
826
1489
|
}
|
|
827
1490
|
else {
|
|
828
|
-
await postPromptObservation(config, queued.request, fetchImplementation);
|
|
1491
|
+
await postPromptObservation(config, queued.request, fetchImplementation, store);
|
|
829
1492
|
}
|
|
830
|
-
await
|
|
1493
|
+
await store.acknowledgeClaim(claimed.claim.id);
|
|
1494
|
+
return {
|
|
1495
|
+
idempotencyKey: claimed.idempotencyKey,
|
|
1496
|
+
state: "acknowledged",
|
|
1497
|
+
...(delivery === undefined ? {} : { delivery }),
|
|
1498
|
+
};
|
|
831
1499
|
}
|
|
832
|
-
catch {
|
|
833
|
-
|
|
1500
|
+
catch (error) {
|
|
1501
|
+
const failed = await store.failClaim({
|
|
1502
|
+
claimId: claimed.claim.id,
|
|
1503
|
+
classification: error instanceof SignedSnapshotError
|
|
1504
|
+
? "incompatible"
|
|
1505
|
+
: classifyRetryFailure(error),
|
|
1506
|
+
message: error instanceof Error ? error.message : "Lore upload failed",
|
|
1507
|
+
});
|
|
1508
|
+
return {
|
|
1509
|
+
idempotencyKey: claimed.idempotencyKey,
|
|
1510
|
+
state: failed.state === "auth-blocked"
|
|
1511
|
+
? "auth-blocked"
|
|
1512
|
+
: failed.state === "dead"
|
|
1513
|
+
? "dead"
|
|
1514
|
+
: "ready",
|
|
1515
|
+
};
|
|
834
1516
|
}
|
|
835
1517
|
}
|
|
836
1518
|
function receiptMessage(config, agent, delivery) {
|
|
@@ -979,6 +1661,7 @@ function guardResultFromResponse(value) {
|
|
|
979
1661
|
}
|
|
980
1662
|
const scope = isObject(item.scope) ? item.scope : {};
|
|
981
1663
|
const source = isObject(item.source) ? item.source : {};
|
|
1664
|
+
const explanation = guardExplanationFromValue(item.explanation);
|
|
982
1665
|
return [
|
|
983
1666
|
{
|
|
984
1667
|
content: redactSecrets(item.content),
|
|
@@ -992,6 +1675,7 @@ function guardResultFromResponse(value) {
|
|
|
992
1675
|
...(typeof source.agent === "string"
|
|
993
1676
|
? { sourceAgent: source.agent }
|
|
994
1677
|
: {}),
|
|
1678
|
+
...(explanation === undefined ? {} : { explanation }),
|
|
995
1679
|
},
|
|
996
1680
|
];
|
|
997
1681
|
})
|
|
@@ -1013,6 +1697,33 @@ function guardResultFromResponse(value) {
|
|
|
1013
1697
|
conflictSummaries,
|
|
1014
1698
|
};
|
|
1015
1699
|
}
|
|
1700
|
+
function guardExplanationFromValue(value) {
|
|
1701
|
+
if (!isObject(value) ||
|
|
1702
|
+
typeof value.summary !== "string" ||
|
|
1703
|
+
!isObject(value.provenance) ||
|
|
1704
|
+
typeof value.provenance.agent !== "string" ||
|
|
1705
|
+
typeof value.status !== "string" ||
|
|
1706
|
+
!isObject(value.freshness) ||
|
|
1707
|
+
typeof value.freshness.updatedAt !== "string") {
|
|
1708
|
+
return undefined;
|
|
1709
|
+
}
|
|
1710
|
+
const scope = isObject(value.scope) ? value.scope : {};
|
|
1711
|
+
const scopeParts = [
|
|
1712
|
+
typeof scope.organization === "string"
|
|
1713
|
+
? `org ${scope.organization}`
|
|
1714
|
+
: undefined,
|
|
1715
|
+
typeof scope.project === "string" ? `project ${scope.project}` : undefined,
|
|
1716
|
+
typeof scope.repo === "string" ? `repo ${scope.repo}` : undefined,
|
|
1717
|
+
typeof scope.path === "string" ? `path ${scope.path}` : undefined,
|
|
1718
|
+
typeof scope.component === "string"
|
|
1719
|
+
? `component ${scope.component}`
|
|
1720
|
+
: undefined,
|
|
1721
|
+
].filter((part) => part !== undefined);
|
|
1722
|
+
const session = typeof value.provenance.sessionId === "string"
|
|
1723
|
+
? ` session ${value.provenance.sessionId}`
|
|
1724
|
+
: "";
|
|
1725
|
+
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)}.`;
|
|
1726
|
+
}
|
|
1016
1727
|
function guardItemSource(item) {
|
|
1017
1728
|
const scope = item.repo !== undefined
|
|
1018
1729
|
? `${item.repo} repo`
|
|
@@ -1027,10 +1738,7 @@ function guardAssistContext(result) {
|
|
|
1027
1738
|
const lines = ["Relevant Lore context:"];
|
|
1028
1739
|
for (const item of result.items) {
|
|
1029
1740
|
lines.push(`- ${item.content}`);
|
|
1030
|
-
|
|
1031
|
-
lines.push("", "Sources:");
|
|
1032
|
-
for (const item of result.items) {
|
|
1033
|
-
lines.push(`- ${guardItemSource(item)}`);
|
|
1741
|
+
lines.push(` ${item.explanation ?? `Source: ${guardItemSource(item)}.`}`);
|
|
1034
1742
|
}
|
|
1035
1743
|
if (result.conflictSummaries.length > 0) {
|
|
1036
1744
|
lines.push("", "Conflicting Lore context (do not silently pick a winner):");
|
|
@@ -1045,17 +1753,21 @@ function guardConfirmationReason(result) {
|
|
|
1045
1753
|
const shown = required.length > 0 ? required : result.items;
|
|
1046
1754
|
const lines = [
|
|
1047
1755
|
"Lore Guard: this action conflicts with a required rule.",
|
|
1048
|
-
...shown.
|
|
1756
|
+
...shown.flatMap((item) => [
|
|
1757
|
+
`- ${item.content}`,
|
|
1758
|
+
` ${item.explanation ?? `Source: ${guardItemSource(item)}.`}`,
|
|
1759
|
+
]),
|
|
1049
1760
|
];
|
|
1050
1761
|
lines.push(result.confirmCommand === undefined
|
|
1051
1762
|
? "Ask the user to confirm before proceeding."
|
|
1052
1763
|
: `Approve here to proceed, or the user can run: ${result.confirmCommand}`);
|
|
1053
1764
|
return boundedContext(lines.join("\n"));
|
|
1054
1765
|
}
|
|
1055
|
-
async function postGuardCheck(config, request, fetchImplementation) {
|
|
1766
|
+
async function postGuardCheck(config, request, fetchImplementation, store) {
|
|
1056
1767
|
const response = await fetchImplementation(guardCheckUrl(config.apiUrl), {
|
|
1057
1768
|
method: "POST",
|
|
1058
1769
|
headers: {
|
|
1770
|
+
accept: runtimeAccept(config),
|
|
1059
1771
|
authorization: `Bearer ${config.token}`,
|
|
1060
1772
|
"content-type": "application/json",
|
|
1061
1773
|
"user-agent": `lore-cli/${RUNTIME_VERSION}`,
|
|
@@ -1064,9 +1776,26 @@ async function postGuardCheck(config, request, fetchImplementation) {
|
|
|
1064
1776
|
signal: AbortSignal.timeout(Math.min(config.timeoutMs ?? GUARD_TIMEOUT_MS, GUARD_TIMEOUT_MS)),
|
|
1065
1777
|
});
|
|
1066
1778
|
if (!response.ok) {
|
|
1067
|
-
throw new
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1779
|
+
throw new RuntimeHttpError(`Lore guard check failed with HTTP ${response.status}`, response.status);
|
|
1780
|
+
}
|
|
1781
|
+
const value = await protocolResponseJson(response);
|
|
1782
|
+
if (config.version === 1) {
|
|
1783
|
+
return guardResultFromResponse(value);
|
|
1784
|
+
}
|
|
1785
|
+
if (!isObject(value) ||
|
|
1786
|
+
!isObject(value.check) ||
|
|
1787
|
+
typeof value.policyVersion !== "string" ||
|
|
1788
|
+
typeof value.policySnapshot !== "string") {
|
|
1789
|
+
throw new IncompatibleReliabilityError("Lore Guard response has no signed policy snapshot");
|
|
1790
|
+
}
|
|
1791
|
+
const payload = await verifySnapshot(value.policySnapshot, "guard_policy", config, store, fetchImplementation);
|
|
1792
|
+
if (payload.policyVersion !== value.policyVersion ||
|
|
1793
|
+
(config.workspaceId !== undefined &&
|
|
1794
|
+
payload.workspaceId !== config.workspaceId)) {
|
|
1795
|
+
throw new IncompatibleReliabilityError("Lore Guard snapshot does not bind the returned policy");
|
|
1796
|
+
}
|
|
1797
|
+
await store.writePolicySnapshot("current", value.policySnapshot);
|
|
1798
|
+
return guardResultFromResponse(value.check);
|
|
1070
1799
|
}
|
|
1071
1800
|
async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
1072
1801
|
if (agent === "polytoken") {
|
|
@@ -1081,10 +1810,12 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1081
1810
|
if (trigger === null) {
|
|
1082
1811
|
return undefined;
|
|
1083
1812
|
}
|
|
1813
|
+
const store = await runtimeStore(config, options.home);
|
|
1084
1814
|
const state = await readGuardState(agent, sessionId, options.home);
|
|
1085
1815
|
if (state.mode === "off" &&
|
|
1086
1816
|
state.modeCheckedAt !== undefined &&
|
|
1087
1817
|
now.getTime() - Date.parse(state.modeCheckedAt) < GUARD_MODE_TTL_MS) {
|
|
1818
|
+
await recordLocalGuardMetric(store, { outcome: "reused_decision" }, now).catch(() => undefined);
|
|
1088
1819
|
return undefined;
|
|
1089
1820
|
}
|
|
1090
1821
|
const cwd = stringField(input.cwd);
|
|
@@ -1112,24 +1843,38 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1112
1843
|
const cachedAt = state.keys[key];
|
|
1113
1844
|
if (cachedAt !== undefined &&
|
|
1114
1845
|
now.getTime() - Date.parse(cachedAt) < GUARD_KEY_COOLDOWN_MS) {
|
|
1846
|
+
await recordLocalGuardMetric(store, { outcome: "reused_decision" }, now).catch(() => undefined);
|
|
1115
1847
|
return undefined;
|
|
1116
1848
|
}
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
? {}
|
|
1128
|
-
|
|
1129
|
-
|
|
1849
|
+
const startedAt = Date.now();
|
|
1850
|
+
let result;
|
|
1851
|
+
try {
|
|
1852
|
+
result = await postGuardCheck(config, {
|
|
1853
|
+
connector: "lore-cli",
|
|
1854
|
+
agent,
|
|
1855
|
+
sessionId,
|
|
1856
|
+
action: trigger.action,
|
|
1857
|
+
tool: toolName,
|
|
1858
|
+
...(scope?.repo === undefined ? {} : { repo: scope.repo }),
|
|
1859
|
+
...(scope?.path === undefined ? {} : { path: scope.path }),
|
|
1860
|
+
...(files.length === 0 ? {} : { files }),
|
|
1861
|
+
...(trigger.command === undefined
|
|
1862
|
+
? {}
|
|
1863
|
+
: { command: redactSecrets(trigger.command).slice(0, 10_000) }),
|
|
1864
|
+
}, options.fetch ?? globalThis.fetch, store);
|
|
1865
|
+
}
|
|
1866
|
+
catch (error) {
|
|
1867
|
+
await recordLocalGuardMetric(store, { outcome: "failed" }, now).catch(() => undefined);
|
|
1868
|
+
throw error;
|
|
1869
|
+
}
|
|
1130
1870
|
if (result === null) {
|
|
1131
|
-
|
|
1871
|
+
await recordLocalGuardMetric(store, { outcome: "failed" }, now).catch(() => undefined);
|
|
1872
|
+
throw new IncompatibleReliabilityError("Lore Guard response is invalid");
|
|
1132
1873
|
}
|
|
1874
|
+
await recordLocalGuardMetric(store, {
|
|
1875
|
+
outcome: "live_check",
|
|
1876
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
1877
|
+
}, now).catch(() => undefined);
|
|
1133
1878
|
state.mode = result.mode;
|
|
1134
1879
|
state.modeCheckedAt = now.toISOString();
|
|
1135
1880
|
if (!result.requiresConfirmation) {
|
|
@@ -1141,6 +1886,12 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1141
1886
|
}
|
|
1142
1887
|
if (result.requiresConfirmation) {
|
|
1143
1888
|
const reason = guardConfirmationReason(result);
|
|
1889
|
+
if (agent === "copilot-cli") {
|
|
1890
|
+
return {
|
|
1891
|
+
permissionDecision: "ask",
|
|
1892
|
+
permissionDecisionReason: reason,
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1144
1895
|
if (agent === "cursor") {
|
|
1145
1896
|
// Cursor's "ask" verdict is unenforced upstream; deny is the only
|
|
1146
1897
|
// reliable gate. The message carries the approve-and-retry path.
|
|
@@ -1161,6 +1912,11 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1161
1912
|
if (result.items.length === 0 && result.conflictSummaries.length === 0) {
|
|
1162
1913
|
return undefined;
|
|
1163
1914
|
}
|
|
1915
|
+
if (agent === "copilot-cli") {
|
|
1916
|
+
// Copilot CLI's preToolUse output has no context-injection field. Prompt
|
|
1917
|
+
// context was already added by userPromptTransformed.
|
|
1918
|
+
return undefined;
|
|
1919
|
+
}
|
|
1164
1920
|
if (agent === "cursor") {
|
|
1165
1921
|
// Cursor cannot inject agent context at the shell boundary on allow.
|
|
1166
1922
|
return undefined;
|
|
@@ -1174,14 +1930,16 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1174
1930
|
}
|
|
1175
1931
|
export async function handleHookEvent(value, agent, options = {}) {
|
|
1176
1932
|
if (!isObject(value)) {
|
|
1177
|
-
|
|
1933
|
+
throw new InvalidHookInputError();
|
|
1178
1934
|
}
|
|
1179
1935
|
const normalized = normalizeHookInput(value, agent, options.environment ?? process.env);
|
|
1180
1936
|
const input = normalized.input;
|
|
1181
1937
|
const eventName = normalized.eventName;
|
|
1182
1938
|
const sessionId = normalized.sessionId;
|
|
1183
|
-
if (eventName === undefined ||
|
|
1184
|
-
|
|
1939
|
+
if (eventName === undefined ||
|
|
1940
|
+
sessionId === undefined ||
|
|
1941
|
+
!validHookInput(input, agent, eventName)) {
|
|
1942
|
+
throw new InvalidHookInputError();
|
|
1185
1943
|
}
|
|
1186
1944
|
const config = await readRuntimeConfig(options.home);
|
|
1187
1945
|
if (config === null || !config.agents.includes(agent)) {
|
|
@@ -1203,20 +1961,72 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1203
1961
|
return await handleGuardEvent(input, agent, config, sessionId, options, now);
|
|
1204
1962
|
}
|
|
1205
1963
|
catch {
|
|
1206
|
-
|
|
1207
|
-
|
|
1964
|
+
const warning = "Lore Guard is unavailable; this action is proceeding without a live policy decision.";
|
|
1965
|
+
if (agent === "cursor") {
|
|
1966
|
+
return {
|
|
1967
|
+
userMessage: warning,
|
|
1968
|
+
agentMessage: warning,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
if (agent === "copilot-cli") {
|
|
1972
|
+
return undefined;
|
|
1973
|
+
}
|
|
1974
|
+
return {
|
|
1975
|
+
hookSpecificOutput: {
|
|
1976
|
+
hookEventName: "PreToolUse",
|
|
1977
|
+
additionalContext: warning,
|
|
1978
|
+
},
|
|
1979
|
+
};
|
|
1208
1980
|
}
|
|
1209
1981
|
}
|
|
1210
|
-
if (eventName !== "UserPromptSubmit") {
|
|
1211
|
-
return undefined;
|
|
1212
|
-
}
|
|
1213
1982
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
1214
|
-
await retryOne(config, fetchImplementation, options.home);
|
|
1215
|
-
const pending = await consumePending(agent, sessionId, options.home);
|
|
1216
1983
|
const prompt = stringField(input.prompt);
|
|
1217
1984
|
if (prompt === undefined) {
|
|
1218
|
-
|
|
1985
|
+
throw new InvalidHookInputError();
|
|
1219
1986
|
}
|
|
1987
|
+
const notices = [];
|
|
1988
|
+
let store;
|
|
1989
|
+
try {
|
|
1990
|
+
store = await runtimeStore(config, options.home);
|
|
1991
|
+
}
|
|
1992
|
+
catch (error) {
|
|
1993
|
+
const detail = error instanceof ReliabilityStoreError &&
|
|
1994
|
+
error.code === "CAPACITY_EXCEEDED"
|
|
1995
|
+
? "the local outbox is full"
|
|
1996
|
+
: "local durable storage is unavailable";
|
|
1997
|
+
notices.push(`Lore could not acknowledge this capture because ${detail}. Run lore doctor.`);
|
|
1998
|
+
const notice = notices.join(" ");
|
|
1999
|
+
return agent === "polytoken"
|
|
2000
|
+
? { outcome: "accept", additional_context: notice }
|
|
2001
|
+
: agent === "cursor"
|
|
2002
|
+
? {
|
|
2003
|
+
continue: true,
|
|
2004
|
+
hookSpecificOutput: {
|
|
2005
|
+
hookEventName: "UserPromptSubmit",
|
|
2006
|
+
additionalContext: notice,
|
|
2007
|
+
},
|
|
2008
|
+
}
|
|
2009
|
+
: agent === "copilot-cli"
|
|
2010
|
+
? {
|
|
2011
|
+
modifiedTransformedPrompt: `${notice}\n\n${stringField(input.transformed_prompt) ?? prompt}`,
|
|
2012
|
+
}
|
|
2013
|
+
: { systemMessage: notice };
|
|
2014
|
+
}
|
|
2015
|
+
const noticeForFlush = (result) => {
|
|
2016
|
+
if (result?.state === "auth-blocked") {
|
|
2017
|
+
notices.push("Lore retained a capture locally, but upload is blocked by authentication. Run lore connect again.");
|
|
2018
|
+
}
|
|
2019
|
+
else if (result?.state === "dead") {
|
|
2020
|
+
notices.push("Lore retained a capture as a dead letter because the server response was incompatible. Run lore doctor.");
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
try {
|
|
2024
|
+
noticeForFlush(await flushOne(config, fetchImplementation, store));
|
|
2025
|
+
}
|
|
2026
|
+
catch {
|
|
2027
|
+
notices.push("Lore retained queued captures locally, but replay could not run. Run lore doctor.");
|
|
2028
|
+
}
|
|
2029
|
+
const pending = await readPending(agent, sessionId, options.home);
|
|
1220
2030
|
const lineage = await readLineageMetadata(options.home, options.environment ?? process.env);
|
|
1221
2031
|
let delivery = emptyDelivery();
|
|
1222
2032
|
if (pending !== null) {
|
|
@@ -1224,26 +2034,61 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1224
2034
|
if (request === null) {
|
|
1225
2035
|
return undefined;
|
|
1226
2036
|
}
|
|
2037
|
+
let enqueued = false;
|
|
1227
2038
|
try {
|
|
1228
|
-
|
|
2039
|
+
await enqueueCapture(store, { kind: "turn", request });
|
|
2040
|
+
enqueued = true;
|
|
2041
|
+
await clearPending(agent, sessionId, options.home);
|
|
1229
2042
|
}
|
|
1230
|
-
catch {
|
|
2043
|
+
catch (error) {
|
|
2044
|
+
notices.push(error instanceof ReliabilityStoreError &&
|
|
2045
|
+
error.code === "CAPACITY_EXCEEDED"
|
|
2046
|
+
? "Lore could not acknowledge this capture because the local outbox is full. Run lore status."
|
|
2047
|
+
: "Lore could not acknowledge this capture because local persistence failed. Run lore doctor.");
|
|
2048
|
+
}
|
|
2049
|
+
if (enqueued) {
|
|
1231
2050
|
try {
|
|
1232
|
-
await
|
|
2051
|
+
const flushed = await flushOne(config, fetchImplementation, store);
|
|
2052
|
+
noticeForFlush(flushed);
|
|
2053
|
+
if (flushed?.idempotencyKey === request.eventId &&
|
|
2054
|
+
flushed.state === "acknowledged" &&
|
|
2055
|
+
flushed.delivery !== undefined) {
|
|
2056
|
+
delivery = flushed.delivery;
|
|
2057
|
+
}
|
|
2058
|
+
else {
|
|
2059
|
+
notices.push("Lore saved this capture locally; server upload is pending.");
|
|
2060
|
+
}
|
|
1233
2061
|
}
|
|
1234
2062
|
catch {
|
|
1235
|
-
|
|
2063
|
+
notices.push("Lore saved this capture locally; server upload is pending.");
|
|
1236
2064
|
}
|
|
2065
|
+
}
|
|
2066
|
+
if (delivery.context === "") {
|
|
1237
2067
|
try {
|
|
1238
|
-
delivery = await
|
|
2068
|
+
delivery = await getPromptContextWithFallback(config, agent, sessionId, request.eventId, prompt, {
|
|
1239
2069
|
...(request.scope === undefined ? {} : { scope: request.scope }),
|
|
1240
2070
|
learningScope: request.learningScope,
|
|
1241
2071
|
...(request.diff === undefined ? {} : { diff: request.diff }),
|
|
1242
2072
|
...(request.files === undefined ? {} : { files: request.files }),
|
|
1243
|
-
}, fetchImplementation);
|
|
2073
|
+
}, fetchImplementation, store, now);
|
|
1244
2074
|
}
|
|
1245
2075
|
catch {
|
|
1246
|
-
|
|
2076
|
+
delivery = {
|
|
2077
|
+
...emptyDelivery(),
|
|
2078
|
+
reliability: {
|
|
2079
|
+
status: "failed",
|
|
2080
|
+
source: "none",
|
|
2081
|
+
fallback: "none",
|
|
2082
|
+
reasons: ["invalid_response"],
|
|
2083
|
+
freshness: {
|
|
2084
|
+
state: "unknown",
|
|
2085
|
+
asOf: null,
|
|
2086
|
+
ageMs: null,
|
|
2087
|
+
validUntil: null,
|
|
2088
|
+
},
|
|
2089
|
+
policyVersion: null,
|
|
2090
|
+
},
|
|
2091
|
+
};
|
|
1247
2092
|
}
|
|
1248
2093
|
}
|
|
1249
2094
|
}
|
|
@@ -1253,45 +2098,110 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1253
2098
|
if (observation === null) {
|
|
1254
2099
|
return undefined;
|
|
1255
2100
|
}
|
|
2101
|
+
let enqueued = false;
|
|
1256
2102
|
try {
|
|
1257
|
-
await
|
|
2103
|
+
await enqueueCapture(store, { kind: "prompt", request: observation });
|
|
2104
|
+
enqueued = true;
|
|
1258
2105
|
}
|
|
1259
|
-
catch {
|
|
2106
|
+
catch (error) {
|
|
2107
|
+
notices.push(error instanceof ReliabilityStoreError &&
|
|
2108
|
+
error.code === "CAPACITY_EXCEEDED"
|
|
2109
|
+
? "Lore could not acknowledge this capture because the local outbox is full. Run lore status."
|
|
2110
|
+
: "Lore could not acknowledge this capture because local persistence failed. Run lore doctor.");
|
|
2111
|
+
}
|
|
2112
|
+
if (enqueued) {
|
|
1260
2113
|
try {
|
|
1261
|
-
await
|
|
2114
|
+
const flushed = await flushOne(config, fetchImplementation, store);
|
|
2115
|
+
noticeForFlush(flushed);
|
|
2116
|
+
if (flushed?.idempotencyKey !== observation.eventId ||
|
|
2117
|
+
flushed.state !== "acknowledged") {
|
|
2118
|
+
notices.push("Lore saved this capture locally; server upload is pending.");
|
|
2119
|
+
}
|
|
1262
2120
|
}
|
|
1263
2121
|
catch {
|
|
1264
|
-
|
|
2122
|
+
notices.push("Lore saved this capture locally; server upload is pending.");
|
|
1265
2123
|
}
|
|
1266
2124
|
}
|
|
1267
2125
|
try {
|
|
1268
|
-
delivery = await
|
|
2126
|
+
delivery = await getPromptContextWithFallback(config, agent, sessionId, observation.eventId, prompt, gitContext, fetchImplementation, store, now);
|
|
1269
2127
|
}
|
|
1270
2128
|
catch {
|
|
1271
|
-
|
|
2129
|
+
delivery = {
|
|
2130
|
+
...emptyDelivery(),
|
|
2131
|
+
reliability: {
|
|
2132
|
+
status: "failed",
|
|
2133
|
+
source: "none",
|
|
2134
|
+
fallback: "none",
|
|
2135
|
+
reasons: ["invalid_response"],
|
|
2136
|
+
freshness: {
|
|
2137
|
+
state: "unknown",
|
|
2138
|
+
asOf: null,
|
|
2139
|
+
ageMs: null,
|
|
2140
|
+
validUntil: null,
|
|
2141
|
+
},
|
|
2142
|
+
policyVersion: null,
|
|
2143
|
+
},
|
|
2144
|
+
};
|
|
1272
2145
|
}
|
|
1273
2146
|
}
|
|
2147
|
+
const reliability = delivery.reliability;
|
|
2148
|
+
await recordLocalRetrievalMetric(store, {
|
|
2149
|
+
outcome: reliability?.fallback === "cached_context"
|
|
2150
|
+
? "cached_fallback"
|
|
2151
|
+
: reliability?.fallback === "live_lexical"
|
|
2152
|
+
? "live_lexical_fallback"
|
|
2153
|
+
: reliability?.status === "failed" ||
|
|
2154
|
+
reliability?.source === "none"
|
|
2155
|
+
? "failed"
|
|
2156
|
+
: "live_primary",
|
|
2157
|
+
...(reliability?.freshness.ageMs === undefined
|
|
2158
|
+
? {}
|
|
2159
|
+
: { cacheAgeMs: reliability.freshness.ageMs }),
|
|
2160
|
+
}, now).catch(() => undefined);
|
|
2161
|
+
const contextReliabilityNotice = await retrievalNotice(delivery, sessionId, store, now);
|
|
2162
|
+
if (contextReliabilityNotice !== undefined) {
|
|
2163
|
+
notices.push(contextReliabilityNotice);
|
|
2164
|
+
}
|
|
2165
|
+
const uniqueNotices = [...new Set(notices)];
|
|
2166
|
+
const reliabilityNotice = uniqueNotices.length === 0 ? undefined : uniqueNotices.join(" ");
|
|
2167
|
+
const receipt = receiptMessage(config, agent, delivery);
|
|
1274
2168
|
const systemMessage = agent === "cursor" || agent === "polytoken"
|
|
1275
2169
|
? undefined
|
|
1276
|
-
:
|
|
1277
|
-
|
|
2170
|
+
: [receipt, reliabilityNotice].filter(Boolean).join(" · ") || undefined;
|
|
2171
|
+
const injectedContext = agent === "cursor" || agent === "polytoken"
|
|
2172
|
+
? [delivery.context, reliabilityNotice]
|
|
2173
|
+
.filter((entry) => entry !== undefined && entry !== "")
|
|
2174
|
+
.join("\n\n")
|
|
2175
|
+
: delivery.context;
|
|
2176
|
+
if (injectedContext === "" && systemMessage === undefined) {
|
|
1278
2177
|
return undefined;
|
|
1279
2178
|
}
|
|
1280
2179
|
if (agent === "polytoken") {
|
|
1281
2180
|
return {
|
|
1282
2181
|
outcome: "accept",
|
|
1283
|
-
additional_context:
|
|
2182
|
+
additional_context: injectedContext,
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
if (agent === "copilot-cli") {
|
|
2186
|
+
const copilotContext = [delivery.context, receipt, reliabilityNotice]
|
|
2187
|
+
.filter((entry) => entry !== undefined && entry !== "")
|
|
2188
|
+
.join("\n\n");
|
|
2189
|
+
if (copilotContext === "") {
|
|
2190
|
+
return undefined;
|
|
2191
|
+
}
|
|
2192
|
+
return {
|
|
2193
|
+
modifiedTransformedPrompt: `${copilotContext}\n\n${stringField(input.transformed_prompt) ?? prompt}`,
|
|
1284
2194
|
};
|
|
1285
2195
|
}
|
|
1286
2196
|
return {
|
|
1287
2197
|
...(agent === "cursor" ? { continue: true } : {}),
|
|
1288
2198
|
...(systemMessage === undefined ? {} : { systemMessage }),
|
|
1289
|
-
...(
|
|
2199
|
+
...(injectedContext === ""
|
|
1290
2200
|
? {}
|
|
1291
2201
|
: {
|
|
1292
2202
|
hookSpecificOutput: {
|
|
1293
2203
|
hookEventName: "UserPromptSubmit",
|
|
1294
|
-
additionalContext:
|
|
2204
|
+
additionalContext: injectedContext,
|
|
1295
2205
|
},
|
|
1296
2206
|
}),
|
|
1297
2207
|
};
|
|
@@ -1312,25 +2222,75 @@ function parseAgent(args) {
|
|
|
1312
2222
|
const value = index < 0 ? undefined : args[index + 1];
|
|
1313
2223
|
return isCommandHookAgent(value) ? value : null;
|
|
1314
2224
|
}
|
|
2225
|
+
function nativeIntegrationId(agent) {
|
|
2226
|
+
return agent === "copilot-vscode"
|
|
2227
|
+
? "preview/copilot-vscode"
|
|
2228
|
+
: `native/${agent}`;
|
|
2229
|
+
}
|
|
2230
|
+
async function beginHookInvocation(agent) {
|
|
2231
|
+
const config = await readRuntimeConfig();
|
|
2232
|
+
if (config === null || !config.agents.includes(agent)) {
|
|
2233
|
+
return null;
|
|
2234
|
+
}
|
|
2235
|
+
const workspaceKey = reliabilityWorkspaceKey(config);
|
|
2236
|
+
const home = homeDirectory();
|
|
2237
|
+
const attempt = createIntegrationInvocationAttempt(nativeIntegrationId(agent), { runtimeVersion: RUNTIME_VERSION });
|
|
2238
|
+
const recorded = await writeInvocationAttemptBounded(workspaceKey, attempt, { home, standalone: IS_STANDALONE_RUNTIME });
|
|
2239
|
+
return recorded ? { workspaceKey, home, attempt } : null;
|
|
2240
|
+
}
|
|
2241
|
+
async function finishHookInvocation(tracker, outcome) {
|
|
2242
|
+
if (tracker === null) {
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
await writeInvocationCompletionBounded(tracker.workspaceKey, tracker.attempt, outcome, { home: tracker.home, standalone: IS_STANDALONE_RUNTIME }).catch(() => false);
|
|
2246
|
+
}
|
|
1315
2247
|
export async function runHook(args = process.argv.slice(2)) {
|
|
1316
2248
|
const agent = parseAgent(args);
|
|
1317
2249
|
if (agent === null) {
|
|
1318
2250
|
return;
|
|
1319
2251
|
}
|
|
2252
|
+
const tracker = await beginHookInvocation(agent).catch(() => null);
|
|
2253
|
+
let input;
|
|
1320
2254
|
try {
|
|
1321
|
-
|
|
2255
|
+
input = await readStdin();
|
|
2256
|
+
}
|
|
2257
|
+
catch {
|
|
2258
|
+
await finishHookInvocation(tracker, {
|
|
2259
|
+
success: false,
|
|
2260
|
+
failureCode: "invalid_input",
|
|
2261
|
+
});
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
try {
|
|
2265
|
+
const result = await handleHookEvent(input, agent);
|
|
1322
2266
|
if (result !== undefined) {
|
|
1323
2267
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
1324
2268
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
2269
|
+
await finishHookInvocation(tracker, { success: true });
|
|
2270
|
+
}
|
|
2271
|
+
catch (error) {
|
|
2272
|
+
await finishHookInvocation(tracker, {
|
|
2273
|
+
success: false,
|
|
2274
|
+
failureCode: error instanceof InvalidHookInputError
|
|
2275
|
+
? "invalid_input"
|
|
2276
|
+
: "runtime_error",
|
|
2277
|
+
});
|
|
1327
2278
|
// Native hooks must never block or add error noise to an agent session.
|
|
1328
2279
|
}
|
|
1329
2280
|
}
|
|
1330
2281
|
const entryPath = process.argv[1];
|
|
2282
|
+
const isRuntimeEntrypoint = entryPath !== undefined &&
|
|
2283
|
+
(() => {
|
|
2284
|
+
try {
|
|
2285
|
+
return (realpathSync(resolve(entryPath)) ===
|
|
2286
|
+
realpathSync(fileURLToPath(import.meta.url)));
|
|
2287
|
+
}
|
|
2288
|
+
catch {
|
|
2289
|
+
return import.meta.url === pathToFileURL(resolve(entryPath)).href;
|
|
2290
|
+
}
|
|
2291
|
+
})();
|
|
1331
2292
|
if (!IS_STANDALONE_RUNTIME &&
|
|
1332
|
-
|
|
1333
|
-
import.meta.url === pathToFileURL(resolve(entryPath)).href) {
|
|
2293
|
+
isRuntimeEntrypoint) {
|
|
1334
2294
|
void runHook();
|
|
1335
2295
|
}
|
|
1336
2296
|
//# sourceMappingURL=runtime.js.map
|