@lore-co/cli 0.1.19 → 0.1.22
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 +40 -5
- package/dist/cli.d.ts +10 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +89 -9
- package/dist/cli.js.map +1 -1
- package/dist/cursor-mcp.d.ts +5 -0
- package/dist/cursor-mcp.d.ts.map +1 -0
- package/dist/cursor-mcp.js +41 -0
- package/dist/cursor-mcp.js.map +1 -0
- package/dist/devin-client.d.ts.map +1 -1
- package/dist/devin-client.js +6 -3
- package/dist/devin-client.js.map +1 -1
- 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/guard.d.ts +1 -1
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +32 -1
- package/dist/guard.js.map +1 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +29 -2
- package/dist/live.js.map +1 -1
- package/dist/remember.d.ts +3 -0
- package/dist/remember.d.ts.map +1 -0
- package/dist/remember.js +159 -0
- package/dist/remember.js.map +1 -0
- package/dist/runtime.d.ts +7 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +298 -149
- 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.map +1 -1
- package/dist/signed-snapshot.js +3 -3
- package/dist/signed-snapshot.js.map +1 -1
- package/dist/update.js +3 -3
- package/package.json +4 -3
package/dist/runtime.js
CHANGED
|
@@ -12,6 +12,7 @@ import { writeInvocationAttemptBounded, writeInvocationCompletionBounded, } from
|
|
|
12
12
|
import { SignedSnapshotError, verifySignedSnapshot, } from "./signed-snapshot.js";
|
|
13
13
|
import { CONTEXT_CACHE_MAX_STALE_MS, isContextTransportFailure, selectCachedContext, } from "./context-fallback.js";
|
|
14
14
|
import { RUNTIME_VERSION } from "./runtime-version.js";
|
|
15
|
+
import { guardRuleConflict, guardToolTrigger, supportedGuardRule, guardEmergencyCommand, emergencyOverrideApplies } from "@lore-co/core/guard-evaluation";
|
|
15
16
|
const IS_STANDALONE_RUNTIME = typeof __LORE_STANDALONE__ === "boolean" && __LORE_STANDALONE__;
|
|
16
17
|
export const COMMAND_HOOK_AGENT_NAMES = [
|
|
17
18
|
"codex",
|
|
@@ -143,6 +144,14 @@ function normalizeHookInput(input, agent, environment) {
|
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
146
|
if (agent === "copilot-cli") {
|
|
147
|
+
// CLI command hooks encode toolArgs as JSON; SDK-style inputs are objects.
|
|
148
|
+
let toolArgs = input.toolArgs;
|
|
149
|
+
if (typeof toolArgs === "string") {
|
|
150
|
+
try {
|
|
151
|
+
toolArgs = JSON.parse(toolArgs);
|
|
152
|
+
}
|
|
153
|
+
catch { /* Invalid input stays invalid. */ }
|
|
154
|
+
}
|
|
146
155
|
const eventName = rawEventName === "userPromptTransformed"
|
|
147
156
|
? "UserPromptSubmit"
|
|
148
157
|
: rawEventName === "preToolUse"
|
|
@@ -164,7 +173,7 @@ function normalizeHookInput(input, agent, environment) {
|
|
|
164
173
|
...(eventName === "PreToolUse"
|
|
165
174
|
? {
|
|
166
175
|
tool_name: input.toolName,
|
|
167
|
-
tool_input:
|
|
176
|
+
tool_input: toolArgs,
|
|
168
177
|
}
|
|
169
178
|
: {}),
|
|
170
179
|
...(eventName === "Stop"
|
|
@@ -180,9 +189,11 @@ function normalizeHookInput(input, agent, environment) {
|
|
|
180
189
|
? "UserPromptSubmit"
|
|
181
190
|
: rawEventName === "post_model_turn"
|
|
182
191
|
? "AssistantResponse"
|
|
183
|
-
: rawEventName === "
|
|
184
|
-
? "
|
|
185
|
-
: rawEventName
|
|
192
|
+
: rawEventName === "pre_tool_use"
|
|
193
|
+
? "PreToolUse"
|
|
194
|
+
: rawEventName === "session_start"
|
|
195
|
+
? "SessionStart"
|
|
196
|
+
: rawEventName;
|
|
186
197
|
const sessionId = stringField(input.session_id) ??
|
|
187
198
|
stringField(environment.POLYTOKEN_SESSION_ID);
|
|
188
199
|
const cwd = stringField(input.cwd) ??
|
|
@@ -205,6 +216,7 @@ function normalizeHookInput(input, agent, environment) {
|
|
|
205
216
|
...input,
|
|
206
217
|
...(sessionId === undefined ? {} : { session_id: sessionId }),
|
|
207
218
|
...(cwd === undefined ? {} : { cwd }),
|
|
219
|
+
...(eventName === "PreToolUse" ? { tool_input: input.input ?? input.tool_input } : {}),
|
|
208
220
|
...(prompt === undefined ? {} : { prompt }),
|
|
209
221
|
...(assistantMessage === undefined
|
|
210
222
|
? {}
|
|
@@ -223,15 +235,17 @@ function normalizeHookInput(input, agent, environment) {
|
|
|
223
235
|
: [];
|
|
224
236
|
const cwd = stringField(input.cwd) ??
|
|
225
237
|
(workspaceRoots.length === 1 ? workspaceRoots[0] : undefined);
|
|
226
|
-
const eventName = rawEventName === "
|
|
227
|
-
? "
|
|
228
|
-
: rawEventName === "
|
|
229
|
-
? "
|
|
230
|
-
: rawEventName === "
|
|
231
|
-
? "
|
|
232
|
-
: rawEventName === "
|
|
233
|
-
? "
|
|
234
|
-
: rawEventName
|
|
238
|
+
const eventName = rawEventName === "sessionStart"
|
|
239
|
+
? "SessionStart"
|
|
240
|
+
: rawEventName === "beforeSubmitPrompt"
|
|
241
|
+
? "UserPromptSubmit"
|
|
242
|
+
: rawEventName === "afterAgentResponse"
|
|
243
|
+
? "AssistantResponse"
|
|
244
|
+
: rawEventName === "sessionEnd"
|
|
245
|
+
? "SessionEnd"
|
|
246
|
+
: rawEventName === "beforeShellExecution"
|
|
247
|
+
? "PreToolUse"
|
|
248
|
+
: rawEventName;
|
|
235
249
|
return {
|
|
236
250
|
input: {
|
|
237
251
|
...input,
|
|
@@ -265,9 +279,10 @@ class InvalidHookInputError extends Error {
|
|
|
265
279
|
}
|
|
266
280
|
function validHookInput(input, agent, eventName) {
|
|
267
281
|
const recognized = agent === "polytoken"
|
|
268
|
-
? eventName === "UserPromptSubmit" || eventName === "AssistantResponse"
|
|
282
|
+
? eventName === "UserPromptSubmit" || eventName === "AssistantResponse" || eventName === "PreToolUse"
|
|
269
283
|
: agent === "cursor"
|
|
270
|
-
? eventName === "
|
|
284
|
+
? eventName === "SessionStart" ||
|
|
285
|
+
eventName === "UserPromptSubmit" ||
|
|
271
286
|
eventName === "AssistantResponse" ||
|
|
272
287
|
eventName === "PreToolUse" ||
|
|
273
288
|
eventName === "SessionEnd"
|
|
@@ -282,6 +297,10 @@ function validHookInput(input, agent, eventName) {
|
|
|
282
297
|
return stringField(input.prompt) !== undefined;
|
|
283
298
|
}
|
|
284
299
|
if (eventName === "Stop" || eventName === "AssistantResponse") {
|
|
300
|
+
// Copilot agentStop may carry only transcript metadata. Never parse that
|
|
301
|
+
// transcript or invent assistant text; standalone prompt capture still works.
|
|
302
|
+
if (agent === "copilot-cli" && eventName === "Stop")
|
|
303
|
+
return true;
|
|
285
304
|
return stringField(input.last_assistant_message) !== undefined;
|
|
286
305
|
}
|
|
287
306
|
if (eventName === "PreToolUse") {
|
|
@@ -289,11 +308,8 @@ function validHookInput(input, agent, eventName) {
|
|
|
289
308
|
if (toolName === undefined || !isObject(input.tool_input)) {
|
|
290
309
|
return false;
|
|
291
310
|
}
|
|
292
|
-
if (
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
if (GUARD_SHELL_TOOLS.has(toolName)) {
|
|
296
|
-
return stringField(input.tool_input.command) !== undefined;
|
|
311
|
+
if (GUARD_SHELL_TOOLS.has(toolName.split(".").at(-1))) {
|
|
312
|
+
return stringField(input.tool_input.command ?? input.tool_input.cmd) !== undefined;
|
|
297
313
|
}
|
|
298
314
|
return true;
|
|
299
315
|
}
|
|
@@ -627,7 +643,7 @@ function createPromptObservationRequest(input, agent, sessionId, now, gitContext
|
|
|
627
643
|
connector: "lore-cli",
|
|
628
644
|
agent,
|
|
629
645
|
sessionId,
|
|
630
|
-
eventId: deterministicUuid(`lore-prompt\0${agent}\0${sessionId}\0${promptId ??
|
|
646
|
+
eventId: deterministicUuid(`lore-prompt\0${agent}\0${sessionId}\0${promptId ?? idempotencyKey}`),
|
|
631
647
|
prompt: redactSecrets(prompt),
|
|
632
648
|
...(promptId === undefined ? {} : { promptId }),
|
|
633
649
|
timestamp: now.toISOString(),
|
|
@@ -947,12 +963,12 @@ function runtimeAccept(config) {
|
|
|
947
963
|
}
|
|
948
964
|
async function runtimeStore(config, home) {
|
|
949
965
|
const store = new ReliabilityStore(reliabilityWorkspaceKey(config), {
|
|
950
|
-
|
|
966
|
+
home: homeDirectory(home),
|
|
951
967
|
});
|
|
952
968
|
await store.initialize();
|
|
953
969
|
await store.migrateLegacyQueue(queueDirectory(home));
|
|
954
970
|
if (config.workspaceId !== undefined) {
|
|
955
|
-
const credentialStore = new ReliabilityStore(credentialReliabilityWorkspaceKey(config), {
|
|
971
|
+
const credentialStore = new ReliabilityStore(credentialReliabilityWorkspaceKey(config), { home: homeDirectory(home) });
|
|
956
972
|
await credentialStore.transferPendingTo(store).catch(() => undefined);
|
|
957
973
|
}
|
|
958
974
|
return store;
|
|
@@ -1465,12 +1481,25 @@ function queuedRequest(value) {
|
|
|
1465
1481
|
async function flushOne(config, fetchImplementation, store) {
|
|
1466
1482
|
const claimed = await store.claimNext({
|
|
1467
1483
|
workerId: `native-hook:${process.pid}`,
|
|
1468
|
-
kinds: ["turn", "prompt"],
|
|
1484
|
+
kinds: ["turn", "prompt", "guard_override"],
|
|
1469
1485
|
});
|
|
1470
1486
|
if (claimed === null || claimed.claim === undefined) {
|
|
1471
1487
|
return null;
|
|
1472
1488
|
}
|
|
1473
1489
|
try {
|
|
1490
|
+
if (claimed.kind === "guard_override") {
|
|
1491
|
+
const response = await fetchImplementation(`${config.apiUrl.replace(/\/+$/u, "")}/v1/guard/overrides`, {
|
|
1492
|
+
method: "POST", headers: { authorization: `Bearer ${config.token}`, "content-type": "application/json" },
|
|
1493
|
+
body: JSON.stringify(claimed.payload), signal: AbortSignal.timeout(config.timeoutMs ?? 2_500),
|
|
1494
|
+
});
|
|
1495
|
+
if (!response.ok)
|
|
1496
|
+
throw new RuntimeHttpError("Override audit upload failed", response.status);
|
|
1497
|
+
const acknowledgement = await protocolResponseJson(response);
|
|
1498
|
+
if (!isObject(acknowledgement) || acknowledgement.overrideId !== claimed.idempotencyKey)
|
|
1499
|
+
throw new IncompatibleReliabilityError("Override acknowledgement is invalid");
|
|
1500
|
+
await store.acknowledgeClaim(claimed.claim.id);
|
|
1501
|
+
return { idempotencyKey: claimed.idempotencyKey, state: "acknowledged" };
|
|
1502
|
+
}
|
|
1474
1503
|
const queued = queuedRequest(claimed.payload);
|
|
1475
1504
|
if (queued === null) {
|
|
1476
1505
|
const failed = await store.failClaim({
|
|
@@ -1555,8 +1584,8 @@ function receiptMessage(config, agent, delivery) {
|
|
|
1555
1584
|
}
|
|
1556
1585
|
// ---------------------------------------------------------------------------
|
|
1557
1586
|
// Context Guard: proactive checks at tool boundaries (PreToolUse and Cursor
|
|
1558
|
-
// beforeShellExecution).
|
|
1559
|
-
//
|
|
1587
|
+
// beforeShellExecution). Assist degrades on outages; known Enforce uses a
|
|
1588
|
+
// verified saved policy or denies actions whose policy cannot be verified.
|
|
1560
1589
|
// ---------------------------------------------------------------------------
|
|
1561
1590
|
/** How long a locally cached "guard is off" verdict suppresses network calls. */
|
|
1562
1591
|
const GUARD_MODE_TTL_MS = 5 * 60_000;
|
|
@@ -1564,51 +1593,14 @@ const GUARD_MODE_TTL_MS = 5 * 60_000;
|
|
|
1564
1593
|
const GUARD_KEY_COOLDOWN_MS = 15 * 60_000;
|
|
1565
1594
|
const GUARD_MAX_CACHED_KEYS = 200;
|
|
1566
1595
|
const GUARD_TIMEOUT_MS = 4_000;
|
|
1567
|
-
const
|
|
1568
|
-
"Edit",
|
|
1569
|
-
"Write",
|
|
1570
|
-
"MultiEdit",
|
|
1571
|
-
"NotebookEdit",
|
|
1572
|
-
"ApplyPatch",
|
|
1573
|
-
"apply_patch",
|
|
1574
|
-
"edit",
|
|
1575
|
-
"write",
|
|
1576
|
-
"patch",
|
|
1577
|
-
"str_replace_editor",
|
|
1578
|
-
]);
|
|
1579
|
-
const GUARD_SHELL_TOOLS = new Set(["Bash", "bash", "Shell", "shell"]);
|
|
1580
|
-
const GUARD_COMMIT_COMMAND = /\bgit\s+(?:commit|push)\b/iu;
|
|
1581
|
-
const GUARD_DEPLOY_COMMAND = /\b(?:terraform\s+(?:apply|destroy)|pulumi\s+up|kubectl\s+(?:apply|delete|rollout)|helm\s+(?:install|upgrade|uninstall)|docker\s+push|fly(?:ctl)?\s+deploy|vercel(?:\s+deploy|\s+--prod)|railway\s+up|serverless\s+deploy|sls\s+deploy|cdk\s+deploy|eb\s+deploy|gcloud\s+(?:app|run|functions)\s+deploy|az\s+webapp\s+(?:up|deploy)|(?:npm|pnpm|yarn)\s+publish|prisma\s+migrate\s+deploy|drizzle-kit\s+(?:push|migrate))\b/iu;
|
|
1582
|
-
const GUARD_DESTRUCTIVE_COMMAND = /\b(?:rm\s+(?:-[a-z]*\s+)*-[a-z]*r[a-z]*f|drop\s+(?:table|database)|truncate\s+table)\b/iu;
|
|
1596
|
+
const GUARD_SHELL_TOOLS = new Set(["Bash", "bash", "Shell", "shell", "exec_command", "shell_command", "run_terminal_cmd", "shell_exec", "shell_monitor"]);
|
|
1583
1597
|
/**
|
|
1584
1598
|
* Decides whether a tool call is a meaningful action boundary. File edits
|
|
1585
1599
|
* always qualify; shell commands qualify only when they look high-impact
|
|
1586
1600
|
* (commit, push, deploy, publish, destructive) to keep the guard quiet.
|
|
1587
1601
|
*/
|
|
1588
1602
|
export function guardTrigger(toolName, toolInput) {
|
|
1589
|
-
|
|
1590
|
-
const files = [toolInput.file_path, toolInput.notebook_path, toolInput.path]
|
|
1591
|
-
.map((value) => stringField(value))
|
|
1592
|
-
.filter((value) => value !== undefined);
|
|
1593
|
-
return { action: "edit", ...(files.length === 0 ? {} : { files }) };
|
|
1594
|
-
}
|
|
1595
|
-
if (GUARD_SHELL_TOOLS.has(toolName)) {
|
|
1596
|
-
const command = stringField(toolInput.command);
|
|
1597
|
-
if (command === undefined) {
|
|
1598
|
-
return null;
|
|
1599
|
-
}
|
|
1600
|
-
if (GUARD_COMMIT_COMMAND.test(command)) {
|
|
1601
|
-
return { action: "commit", command };
|
|
1602
|
-
}
|
|
1603
|
-
if (GUARD_DEPLOY_COMMAND.test(command)) {
|
|
1604
|
-
return { action: "deploy", command };
|
|
1605
|
-
}
|
|
1606
|
-
if (GUARD_DESTRUCTIVE_COMMAND.test(command)) {
|
|
1607
|
-
return { action: "command", command };
|
|
1608
|
-
}
|
|
1609
|
-
return null;
|
|
1610
|
-
}
|
|
1611
|
-
return null;
|
|
1603
|
+
return guardToolTrigger(toolName, toolInput);
|
|
1612
1604
|
}
|
|
1613
1605
|
function guardStatePath(agent, sessionId, home) {
|
|
1614
1606
|
return resolve(loreDirectory(home), "state", "guard", `${sha256(`${agent}\0${sessionId}`)}.json`);
|
|
@@ -1689,6 +1681,7 @@ function guardResultFromResponse(value) {
|
|
|
1689
1681
|
return {
|
|
1690
1682
|
...(typeof value.checkId === "string" ? { checkId: value.checkId } : {}),
|
|
1691
1683
|
mode: value.mode,
|
|
1684
|
+
...(value.policyIssue === "unsupported_required_rules" ? { policyUnavailable: true } : {}),
|
|
1692
1685
|
requiresConfirmation: value.requiresConfirmation === true,
|
|
1693
1686
|
...(typeof confirm.command === "string"
|
|
1694
1687
|
? { confirmCommand: confirm.command }
|
|
@@ -1741,7 +1734,7 @@ function guardAssistContext(result) {
|
|
|
1741
1734
|
lines.push(` ${item.explanation ?? `Source: ${guardItemSource(item)}.`}`);
|
|
1742
1735
|
}
|
|
1743
1736
|
if (result.conflictSummaries.length > 0) {
|
|
1744
|
-
lines.push("", "
|
|
1737
|
+
lines.push("", "Lore policy notes:");
|
|
1745
1738
|
for (const summary of result.conflictSummaries) {
|
|
1746
1739
|
lines.push(`- ${summary}`);
|
|
1747
1740
|
}
|
|
@@ -1749,18 +1742,23 @@ function guardAssistContext(result) {
|
|
|
1749
1742
|
return boundedContext(lines.join("\n"));
|
|
1750
1743
|
}
|
|
1751
1744
|
function guardConfirmationReason(result) {
|
|
1745
|
+
if (result.policyUnavailable)
|
|
1746
|
+
return "Lore Guard cannot evaluate an existing required rule. An owner or admin must rewrite it using a supported prohibition or change it to advisory before retrying.";
|
|
1752
1747
|
const required = result.items.filter((item) => item.enforcement === "required");
|
|
1753
1748
|
const shown = required.length > 0 ? required : result.items;
|
|
1754
1749
|
const lines = [
|
|
1755
|
-
"Lore Guard: this action conflicts with a required rule.",
|
|
1750
|
+
result.items.length === 0 ? "Lore Guard cannot verify the enforcement policy." : "Lore Guard: this action conflicts with a required rule.",
|
|
1751
|
+
...result.conflictSummaries,
|
|
1756
1752
|
...shown.flatMap((item) => [
|
|
1757
1753
|
`- ${item.content}`,
|
|
1758
1754
|
` ${item.explanation ?? `Source: ${guardItemSource(item)}.`}`,
|
|
1759
1755
|
]),
|
|
1760
1756
|
];
|
|
1761
|
-
lines.push(result.confirmCommand
|
|
1762
|
-
?
|
|
1763
|
-
:
|
|
1757
|
+
lines.push(result.offline && result.confirmCommand !== undefined
|
|
1758
|
+
? `Restore Lore or run: ${result.confirmCommand}`
|
|
1759
|
+
: result.confirmCommand === undefined
|
|
1760
|
+
? "Ask the user to confirm before proceeding."
|
|
1761
|
+
: `Approve here to proceed, or the user can run: ${result.confirmCommand}`);
|
|
1764
1762
|
return boundedContext(lines.join("\n"));
|
|
1765
1763
|
}
|
|
1766
1764
|
async function postGuardCheck(config, request, fetchImplementation, store) {
|
|
@@ -1795,13 +1793,65 @@ async function postGuardCheck(config, request, fetchImplementation, store) {
|
|
|
1795
1793
|
throw new IncompatibleReliabilityError("Lore Guard snapshot does not bind the returned policy");
|
|
1796
1794
|
}
|
|
1797
1795
|
await store.writePolicySnapshot("current", value.policySnapshot);
|
|
1796
|
+
await store.writeState("guard-policy-state", { mode: payload.mode, overrides: payload.overrides, policyVersion: payload.policyVersion });
|
|
1798
1797
|
return guardResultFromResponse(value.check);
|
|
1799
1798
|
}
|
|
1800
|
-
async function
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1799
|
+
async function offlineGuard(config, store, request, now, previouslyEnforced, allowCachedPolicy) {
|
|
1800
|
+
const remembered = await store.readState("guard-policy-state");
|
|
1801
|
+
const emergency = await store.readState("guard-emergency-override");
|
|
1802
|
+
if (emergencyOverrideApplies(emergency, String(request.sessionId), typeof request.repo === "string" ? request.repo : undefined, now)) {
|
|
1803
|
+
return { offline: true, mode: "enforce", requiresConfirmation: false, items: [], conflictSummaries: ["An audited, time-limited emergency override is active for this session and repository."] };
|
|
1804
|
+
}
|
|
1805
|
+
const overrides = isObject(remembered) && Array.isArray(remembered.overrides) ? remembered.overrides : [];
|
|
1806
|
+
const override = overrides.find((item) => isObject(item) && item.repo === request.repo);
|
|
1807
|
+
const knownEnforce = previouslyEnforced || (isObject(override) ? override.mode : isObject(remembered) ? remembered.mode : undefined) === "enforce";
|
|
1808
|
+
try {
|
|
1809
|
+
if (!allowCachedPolicy)
|
|
1810
|
+
throw new Error("A live policy could not be authenticated");
|
|
1811
|
+
const compactJws = await store.readPolicySnapshot("current");
|
|
1812
|
+
const keys = await cachedSnapshotKeys(config, store, now);
|
|
1813
|
+
if (compactJws === null || keys === null)
|
|
1814
|
+
throw new Error("Policy unavailable");
|
|
1815
|
+
const payload = verifySignedSnapshot(compactJws, {
|
|
1816
|
+
workspaceId: store.workspaceId, keys, kind: "guard_policy", now,
|
|
1817
|
+
}).payload;
|
|
1818
|
+
const cachedOverride = Array.isArray(payload.overrides) ? payload.overrides.find((item) => isObject(item) && item.repo === request.repo) : undefined;
|
|
1819
|
+
const mode = isObject(cachedOverride) ? cachedOverride.mode : payload.mode;
|
|
1820
|
+
if (isObject(remembered) && remembered.policyVersion !== payload.policyVersion)
|
|
1821
|
+
throw new Error("Policy has changed");
|
|
1822
|
+
if (knownEnforce && mode !== "enforce")
|
|
1823
|
+
throw new Error("Cached policy cannot loosen known enforcement");
|
|
1824
|
+
if (mode !== "enforce")
|
|
1825
|
+
return null;
|
|
1826
|
+
if (!Array.isArray(payload.requiredRules) || payload.requiredRules.some((rule) => !isObject(rule) || !isObject(rule.scope) || typeof rule.content !== "string" || !supportedGuardRule(rule))) {
|
|
1827
|
+
throw new Error("Policy contains unsupported required rules");
|
|
1828
|
+
}
|
|
1829
|
+
const rules = Array.isArray(payload.requiredRules) ? payload.requiredRules.filter((rule) => isObject(rule) && isObject(rule.scope) && typeof rule.content === "string" &&
|
|
1830
|
+
guardRuleConflict(rule, {
|
|
1831
|
+
action: String(request.action),
|
|
1832
|
+
...(typeof request.repo === "string" ? { repo: request.repo } : {}),
|
|
1833
|
+
...(typeof request.path === "string" ? { path: request.path } : {}),
|
|
1834
|
+
...(typeof request.command === "string" ? { command: request.command } : {}),
|
|
1835
|
+
...(Array.isArray(request.files) ? { files: request.files.filter((file) => typeof file === "string") } : {}),
|
|
1836
|
+
}) !== null) : [];
|
|
1837
|
+
return {
|
|
1838
|
+
offline: true, mode: "enforce", requiresConfirmation: rules.length > 0,
|
|
1839
|
+
items: rules.map((rule) => ({ content: String(rule.content), enforcement: "required" })),
|
|
1840
|
+
conflictSummaries: ["Lore is offline; this action was checked against its verified, unexpired saved policy."],
|
|
1841
|
+
confirmCommand: guardEmergencyCommand(String(request.sessionId), String(request.repo ?? "")),
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
catch {
|
|
1845
|
+
if (!knownEnforce)
|
|
1846
|
+
return null;
|
|
1847
|
+
return {
|
|
1848
|
+
offline: true, mode: "enforce", requiresConfirmation: true, items: [],
|
|
1849
|
+
conflictSummaries: ["Lore cannot verify the current enforcement policy. Restore the connection or use an audited emergency override."],
|
|
1850
|
+
confirmCommand: guardEmergencyCommand(String(request.sessionId), String(request.repo ?? "")),
|
|
1851
|
+
};
|
|
1804
1852
|
}
|
|
1853
|
+
}
|
|
1854
|
+
async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
1805
1855
|
const toolName = stringField(input.tool_name);
|
|
1806
1856
|
if (toolName === undefined) {
|
|
1807
1857
|
return undefined;
|
|
@@ -1812,7 +1862,11 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1812
1862
|
}
|
|
1813
1863
|
const store = await runtimeStore(config, options.home);
|
|
1814
1864
|
const state = await readGuardState(agent, sessionId, options.home);
|
|
1815
|
-
|
|
1865
|
+
const rememberedPolicy = await store.readState("guard-policy-state");
|
|
1866
|
+
const enforcementMayHaveChanged = isObject(rememberedPolicy) && (rememberedPolicy.mode === "enforce" ||
|
|
1867
|
+
(Array.isArray(rememberedPolicy.overrides) && rememberedPolicy.overrides.some((entry) => isObject(entry) && entry.mode === "enforce")));
|
|
1868
|
+
if (!enforcementMayHaveChanged &&
|
|
1869
|
+
state.mode === "off" &&
|
|
1816
1870
|
state.modeCheckedAt !== undefined &&
|
|
1817
1871
|
now.getTime() - Date.parse(state.modeCheckedAt) < GUARD_MODE_TTL_MS) {
|
|
1818
1872
|
await recordLocalGuardMetric(store, { outcome: "reused_decision" }, now).catch(() => undefined);
|
|
@@ -1832,8 +1886,7 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1832
1886
|
? normalizedRepositoryPath(file)
|
|
1833
1887
|
: normalizedRepositoryPath(relativePath);
|
|
1834
1888
|
})
|
|
1835
|
-
.filter((file) => file !== "")
|
|
1836
|
-
.slice(0, 100);
|
|
1889
|
+
.filter((file) => file !== "");
|
|
1837
1890
|
const key = sha256([
|
|
1838
1891
|
"guard",
|
|
1839
1892
|
trigger.action,
|
|
@@ -1842,53 +1895,66 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1842
1895
|
].join("\0"));
|
|
1843
1896
|
const cachedAt = state.keys[key];
|
|
1844
1897
|
if (cachedAt !== undefined &&
|
|
1898
|
+
!enforcementMayHaveChanged &&
|
|
1899
|
+
state.mode !== "enforce" &&
|
|
1845
1900
|
now.getTime() - Date.parse(cachedAt) < GUARD_KEY_COOLDOWN_MS) {
|
|
1846
1901
|
await recordLocalGuardMetric(store, { outcome: "reused_decision" }, now).catch(() => undefined);
|
|
1847
1902
|
return undefined;
|
|
1848
1903
|
}
|
|
1849
1904
|
const startedAt = Date.now();
|
|
1850
1905
|
let result;
|
|
1906
|
+
const guardRequest = {
|
|
1907
|
+
connector: "lore-cli",
|
|
1908
|
+
agent,
|
|
1909
|
+
sessionId,
|
|
1910
|
+
action: trigger.action,
|
|
1911
|
+
tool: toolName,
|
|
1912
|
+
...(scope?.repo === undefined ? {} : { repo: scope.repo }),
|
|
1913
|
+
...(scope?.path === undefined ? {} : { path: scope.path }),
|
|
1914
|
+
...(files.length === 0 ? {} : { files }),
|
|
1915
|
+
...(trigger.command === undefined
|
|
1916
|
+
? {}
|
|
1917
|
+
: { command: redactSecrets(trigger.command) }),
|
|
1918
|
+
};
|
|
1851
1919
|
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);
|
|
1920
|
+
result = await postGuardCheck(config, guardRequest, options.fetch ?? globalThis.fetch, store);
|
|
1865
1921
|
}
|
|
1866
1922
|
catch (error) {
|
|
1867
1923
|
await recordLocalGuardMetric(store, { outcome: "failed" }, now).catch(() => undefined);
|
|
1868
|
-
|
|
1924
|
+
result = await offlineGuard(config, store, guardRequest, now, state.mode === "enforce", isContextTransportFailure(error));
|
|
1925
|
+
if (result === null)
|
|
1926
|
+
throw error;
|
|
1869
1927
|
}
|
|
1870
1928
|
if (result === null) {
|
|
1871
1929
|
await recordLocalGuardMetric(store, { outcome: "failed" }, now).catch(() => undefined);
|
|
1872
1930
|
throw new IncompatibleReliabilityError("Lore Guard response is invalid");
|
|
1873
1931
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1932
|
+
if (!result.offline)
|
|
1933
|
+
await recordLocalGuardMetric(store, {
|
|
1934
|
+
outcome: "live_check",
|
|
1935
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
1936
|
+
}, now).catch(() => undefined);
|
|
1878
1937
|
state.mode = result.mode;
|
|
1879
1938
|
state.modeCheckedAt = now.toISOString();
|
|
1880
|
-
|
|
1939
|
+
// A policy-blocked action (unsupported required rule) must not be cached as
|
|
1940
|
+
// an allow: the row has to be re-checked until an owner repairs the policy.
|
|
1941
|
+
if (!result.requiresConfirmation && !result.policyUnavailable) {
|
|
1881
1942
|
state.keys[key] = now.toISOString();
|
|
1882
1943
|
}
|
|
1883
|
-
await writeGuardState(agent, sessionId, state, options.home);
|
|
1944
|
+
await writeGuardState(agent, sessionId, state, options.home).catch(() => undefined);
|
|
1884
1945
|
if (result.mode === "off") {
|
|
1885
1946
|
return undefined;
|
|
1886
1947
|
}
|
|
1887
|
-
|
|
1948
|
+
// `policyUnavailable` (an unsupported required rule) blocks the action
|
|
1949
|
+
// independently of `requiresConfirmation`: the producer reports it via
|
|
1950
|
+
// `policyIssue` rather than `confirm`, so it never sets `requiresConfirmation`.
|
|
1951
|
+
if (result.requiresConfirmation || result.policyUnavailable) {
|
|
1888
1952
|
const reason = guardConfirmationReason(result);
|
|
1953
|
+
if (agent === "polytoken")
|
|
1954
|
+
return { outcome: "deny", reason };
|
|
1889
1955
|
if (agent === "copilot-cli") {
|
|
1890
1956
|
return {
|
|
1891
|
-
permissionDecision: "ask",
|
|
1957
|
+
permissionDecision: result.offline || result.policyUnavailable ? "deny" : "ask",
|
|
1892
1958
|
permissionDecisionReason: reason,
|
|
1893
1959
|
};
|
|
1894
1960
|
}
|
|
@@ -1904,7 +1970,8 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1904
1970
|
return {
|
|
1905
1971
|
hookSpecificOutput: {
|
|
1906
1972
|
hookEventName: "PreToolUse",
|
|
1907
|
-
|
|
1973
|
+
// Codex parses `ask` as unsupported and then continues the tool call.
|
|
1974
|
+
permissionDecision: agent === "codex" || result.offline || result.policyUnavailable ? "deny" : "ask",
|
|
1908
1975
|
permissionDecisionReason: reason,
|
|
1909
1976
|
},
|
|
1910
1977
|
};
|
|
@@ -1917,6 +1984,8 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1917
1984
|
// context was already added by userPromptTransformed.
|
|
1918
1985
|
return undefined;
|
|
1919
1986
|
}
|
|
1987
|
+
if (agent === "polytoken")
|
|
1988
|
+
return { outcome: "allow" };
|
|
1920
1989
|
if (agent === "cursor") {
|
|
1921
1990
|
// Cursor cannot inject agent context at the shell boundary on allow.
|
|
1922
1991
|
return undefined;
|
|
@@ -1946,6 +2015,20 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1946
2015
|
return undefined;
|
|
1947
2016
|
}
|
|
1948
2017
|
const now = (options.now ?? (() => new Date()))();
|
|
2018
|
+
if (agent === "cursor" && eventName === "SessionStart") {
|
|
2019
|
+
const cwd = stringField(input.cwd);
|
|
2020
|
+
if (!cwd)
|
|
2021
|
+
return { additional_context: "Lore needs a single repository workspace to provide shared context." };
|
|
2022
|
+
const store = await runtimeStore(config, options.home);
|
|
2023
|
+
await store.writeState(cursorSessionKey(sessionId), { sessionId, cwd, updatedAt: now.toISOString() });
|
|
2024
|
+
return { additional_context: [
|
|
2025
|
+
"Lore shared context is connected through the lore-native MCP server.",
|
|
2026
|
+
`For this conversation use sessionId ${JSON.stringify(sessionId)}.`,
|
|
2027
|
+
"Before answering each user request or using other tools, call lore_context with that sessionId and the full user message as prompt. This retrieves relevant shared knowledge and records durable teachings through Lore's normal capture pipeline.",
|
|
2028
|
+
"Use the returned context when relevant. If retrieval fails, report that limitation; do not invent shared knowledge.",
|
|
2029
|
+
"After completing each response, call lore_complete with the same sessionId and a concise response summary so the next user correction retains the prior attempt. These Lore calls are available through MCP and require no shell commands.",
|
|
2030
|
+
].join("\n") };
|
|
2031
|
+
}
|
|
1949
2032
|
if (eventName === "Stop" || eventName === "AssistantResponse") {
|
|
1950
2033
|
await savePending(input, agent, sessionId, now, options.home);
|
|
1951
2034
|
return undefined;
|
|
@@ -1961,6 +2044,20 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1961
2044
|
return await handleGuardEvent(input, agent, config, sessionId, options, now);
|
|
1962
2045
|
}
|
|
1963
2046
|
catch {
|
|
2047
|
+
const prior = await readGuardState(agent, sessionId, options.home).catch(() => null);
|
|
2048
|
+
const saved = await runtimeStore(config, options.home).then((store) => store.readState("guard-policy-state")).catch(() => null);
|
|
2049
|
+
const scope = await repositoryScope(stringField(input.cwd)).catch(() => undefined);
|
|
2050
|
+
const override = isObject(saved) && Array.isArray(saved.overrides) ? saved.overrides.find((entry) => isObject(entry) && entry.repo === scope?.repo) : undefined;
|
|
2051
|
+
if (prior?.mode === "enforce" || (isObject(override) ? override.mode : isObject(saved) ? saved.mode : undefined) === "enforce") {
|
|
2052
|
+
const reason = "Lore Guard cannot verify its enforcement policy. Restore Lore before retrying.";
|
|
2053
|
+
if (agent === "cursor")
|
|
2054
|
+
return { permission: "deny", userMessage: reason, agentMessage: reason };
|
|
2055
|
+
if (agent === "copilot-cli")
|
|
2056
|
+
return { permissionDecision: "deny", permissionDecisionReason: reason };
|
|
2057
|
+
if (agent === "polytoken")
|
|
2058
|
+
return { outcome: "deny", reason };
|
|
2059
|
+
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } };
|
|
2060
|
+
}
|
|
1964
2061
|
const warning = "Lore Guard is unavailable; this action is proceeding without a live policy decision.";
|
|
1965
2062
|
if (agent === "cursor") {
|
|
1966
2063
|
return {
|
|
@@ -1968,7 +2065,7 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1968
2065
|
agentMessage: warning,
|
|
1969
2066
|
};
|
|
1970
2067
|
}
|
|
1971
|
-
if (agent === "copilot-cli") {
|
|
2068
|
+
if (agent === "copilot-cli" || agent === "polytoken") {
|
|
1972
2069
|
return undefined;
|
|
1973
2070
|
}
|
|
1974
2071
|
return {
|
|
@@ -1979,6 +2076,57 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1979
2076
|
};
|
|
1980
2077
|
}
|
|
1981
2078
|
}
|
|
2079
|
+
if (agent === "cursor") {
|
|
2080
|
+
const store = await runtimeStore(config, options.home);
|
|
2081
|
+
const existing = await store.readState(cursorSessionKey(sessionId));
|
|
2082
|
+
const cwd = stringField(input.cwd);
|
|
2083
|
+
if (cwd)
|
|
2084
|
+
await store.writeState(cursorSessionKey(sessionId), {
|
|
2085
|
+
...(isObject(existing) ? existing : {}), sessionId, cwd,
|
|
2086
|
+
...(stringField(input.prompt) ? { prompt: redactSecrets(String(input.prompt)) } : {}),
|
|
2087
|
+
...(stringField(input.prompt_id) ? { promptId: input.prompt_id } : {}), updatedAt: now.toISOString(),
|
|
2088
|
+
});
|
|
2089
|
+
// Cursor's prompt hook supports submission control only. The MCP call
|
|
2090
|
+
// delivers the context; capture here retains native editor event identity.
|
|
2091
|
+
// Retrieve is skipped so the notice cooldown is not primed by a discarded
|
|
2092
|
+
// result: the MCP `lore_context` call surfaces the reliability notice.
|
|
2093
|
+
await handlePromptEvent(input, agent, sessionId, config, options, now, true, false);
|
|
2094
|
+
return { continue: true };
|
|
2095
|
+
}
|
|
2096
|
+
return handlePromptEvent(input, agent, sessionId, config, options, now);
|
|
2097
|
+
}
|
|
2098
|
+
function cursorSessionKey(sessionId) {
|
|
2099
|
+
return `cursor-session-${sha256(sessionId)}`;
|
|
2100
|
+
}
|
|
2101
|
+
/** MCP requests are bound to a session/workspace observed by the native hook. */
|
|
2102
|
+
async function cursorSession(sessionId, options) {
|
|
2103
|
+
const config = await readRuntimeConfig(options.home);
|
|
2104
|
+
if (!config?.agents.includes("cursor"))
|
|
2105
|
+
throw new Error("Reconnect Cursor to Lore before requesting context.");
|
|
2106
|
+
const store = await runtimeStore(config, options.home);
|
|
2107
|
+
const state = await store.readState(cursorSessionKey(sessionId));
|
|
2108
|
+
if (!isObject(state) || state.sessionId !== sessionId || !stringField(state.cwd)) {
|
|
2109
|
+
throw new Error("Start a new Cursor conversation to register this Lore session.");
|
|
2110
|
+
}
|
|
2111
|
+
return { config, store, state, cwd: stringField(state.cwd) };
|
|
2112
|
+
}
|
|
2113
|
+
export async function getCursorContext(sessionId, prompt, options = {}) {
|
|
2114
|
+
const { config, state, cwd } = await cursorSession(sessionId, options);
|
|
2115
|
+
const now = (options.now ?? (() => new Date()))();
|
|
2116
|
+
const result = await handlePromptEvent({ session_id: sessionId, cwd,
|
|
2117
|
+
prompt: stringField(state.prompt) ?? prompt,
|
|
2118
|
+
...(stringField(state.promptId) ? { prompt_id: state.promptId } : {}),
|
|
2119
|
+
}, "cursor", sessionId, config, options, now, stringField(state.prompt) === undefined);
|
|
2120
|
+
return result?.additional_context || "No relevant shared knowledge was found for this request.";
|
|
2121
|
+
}
|
|
2122
|
+
export async function completeCursorResponse(sessionId, response, options = {}) {
|
|
2123
|
+
const { store, state, cwd } = await cursorSession(sessionId, options);
|
|
2124
|
+
const now = (options.now ?? (() => new Date()))();
|
|
2125
|
+
await savePending({ cwd, last_assistant_message: response }, "cursor", sessionId, now, options.home);
|
|
2126
|
+
const { prompt: _prompt, promptId: _promptId, ...rest } = state;
|
|
2127
|
+
await store.writeState(cursorSessionKey(sessionId), { ...rest, updatedAt: now.toISOString() });
|
|
2128
|
+
}
|
|
2129
|
+
async function handlePromptEvent(input, agent, sessionId, config, options, now, capture = true, retrieve = true) {
|
|
1982
2130
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
1983
2131
|
const prompt = stringField(input.prompt);
|
|
1984
2132
|
if (prompt === undefined) {
|
|
@@ -1999,13 +2147,7 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1999
2147
|
return agent === "polytoken"
|
|
2000
2148
|
? { outcome: "accept", additional_context: notice }
|
|
2001
2149
|
: agent === "cursor"
|
|
2002
|
-
? {
|
|
2003
|
-
continue: true,
|
|
2004
|
-
hookSpecificOutput: {
|
|
2005
|
-
hookEventName: "UserPromptSubmit",
|
|
2006
|
-
additionalContext: notice,
|
|
2007
|
-
},
|
|
2008
|
-
}
|
|
2150
|
+
? { additional_context: notice }
|
|
2009
2151
|
: agent === "copilot-cli"
|
|
2010
2152
|
? {
|
|
2011
2153
|
modifiedTransformedPrompt: `${notice}\n\n${stringField(input.transformed_prompt) ?? prompt}`,
|
|
@@ -2026,7 +2168,7 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
2026
2168
|
catch {
|
|
2027
2169
|
notices.push("Lore retained queued captures locally, but replay could not run. Run lore doctor.");
|
|
2028
2170
|
}
|
|
2029
|
-
const pending = await readPending(agent, sessionId, options.home);
|
|
2171
|
+
const pending = capture ? await readPending(agent, sessionId, options.home) : null;
|
|
2030
2172
|
const lineage = await readLineageMetadata(options.home, options.environment ?? process.env);
|
|
2031
2173
|
let delivery = emptyDelivery();
|
|
2032
2174
|
if (pending !== null) {
|
|
@@ -2063,7 +2205,7 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
2063
2205
|
notices.push("Lore saved this capture locally; server upload is pending.");
|
|
2064
2206
|
}
|
|
2065
2207
|
}
|
|
2066
|
-
if (delivery.context === "") {
|
|
2208
|
+
if (retrieve && delivery.context === "") {
|
|
2067
2209
|
try {
|
|
2068
2210
|
delivery = await getPromptContextWithFallback(config, agent, sessionId, request.eventId, prompt, {
|
|
2069
2211
|
...(request.scope === undefined ? {} : { scope: request.scope }),
|
|
@@ -2100,8 +2242,10 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
2100
2242
|
}
|
|
2101
2243
|
let enqueued = false;
|
|
2102
2244
|
try {
|
|
2103
|
-
|
|
2104
|
-
|
|
2245
|
+
if (capture) {
|
|
2246
|
+
await enqueueCapture(store, { kind: "prompt", request: observation });
|
|
2247
|
+
enqueued = true;
|
|
2248
|
+
}
|
|
2105
2249
|
}
|
|
2106
2250
|
catch (error) {
|
|
2107
2251
|
notices.push(error instanceof ReliabilityStoreError &&
|
|
@@ -2122,45 +2266,49 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
2122
2266
|
notices.push("Lore saved this capture locally; server upload is pending.");
|
|
2123
2267
|
}
|
|
2124
2268
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2269
|
+
if (retrieve) {
|
|
2270
|
+
try {
|
|
2271
|
+
delivery = await getPromptContextWithFallback(config, agent, sessionId, observation.eventId, prompt, gitContext, fetchImplementation, store, now);
|
|
2272
|
+
}
|
|
2273
|
+
catch {
|
|
2274
|
+
delivery = {
|
|
2275
|
+
...emptyDelivery(),
|
|
2276
|
+
reliability: {
|
|
2277
|
+
status: "failed",
|
|
2278
|
+
source: "none",
|
|
2279
|
+
fallback: "none",
|
|
2280
|
+
reasons: ["invalid_response"],
|
|
2281
|
+
freshness: {
|
|
2282
|
+
state: "unknown",
|
|
2283
|
+
asOf: null,
|
|
2284
|
+
ageMs: null,
|
|
2285
|
+
validUntil: null,
|
|
2286
|
+
},
|
|
2287
|
+
policyVersion: null,
|
|
2141
2288
|
},
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
};
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2145
2291
|
}
|
|
2146
2292
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
reliability?.
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2293
|
+
if (retrieve) {
|
|
2294
|
+
const reliability = delivery.reliability;
|
|
2295
|
+
await recordLocalRetrievalMetric(store, {
|
|
2296
|
+
outcome: reliability?.fallback === "cached_context"
|
|
2297
|
+
? "cached_fallback"
|
|
2298
|
+
: reliability?.fallback === "live_lexical"
|
|
2299
|
+
? "live_lexical_fallback"
|
|
2300
|
+
: reliability?.status === "failed" ||
|
|
2301
|
+
reliability?.source === "none"
|
|
2302
|
+
? "failed"
|
|
2303
|
+
: "live_primary",
|
|
2304
|
+
...(reliability?.freshness.ageMs === undefined
|
|
2305
|
+
? {}
|
|
2306
|
+
: { cacheAgeMs: reliability.freshness.ageMs }),
|
|
2307
|
+
}, now).catch(() => undefined);
|
|
2308
|
+
const contextReliabilityNotice = await retrievalNotice(delivery, sessionId, store, now);
|
|
2309
|
+
if (contextReliabilityNotice !== undefined) {
|
|
2310
|
+
notices.push(contextReliabilityNotice);
|
|
2311
|
+
}
|
|
2164
2312
|
}
|
|
2165
2313
|
const uniqueNotices = [...new Set(notices)];
|
|
2166
2314
|
const reliabilityNotice = uniqueNotices.length === 0 ? undefined : uniqueNotices.join(" ");
|
|
@@ -2182,6 +2330,8 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
2182
2330
|
additional_context: injectedContext,
|
|
2183
2331
|
};
|
|
2184
2332
|
}
|
|
2333
|
+
if (agent === "cursor")
|
|
2334
|
+
return { additional_context: injectedContext };
|
|
2185
2335
|
if (agent === "copilot-cli") {
|
|
2186
2336
|
const copilotContext = [delivery.context, receipt, reliabilityNotice]
|
|
2187
2337
|
.filter((entry) => entry !== undefined && entry !== "")
|
|
@@ -2194,7 +2344,6 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
2194
2344
|
};
|
|
2195
2345
|
}
|
|
2196
2346
|
return {
|
|
2197
|
-
...(agent === "cursor" ? { continue: true } : {}),
|
|
2198
2347
|
...(systemMessage === undefined ? {} : { systemMessage }),
|
|
2199
2348
|
...(injectedContext === ""
|
|
2200
2349
|
? {}
|