@lore-co/cli 0.1.19 → 0.1.21
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 +34 -2
- package/dist/cli.d.ts +8 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +87 -7
- 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.map +1 -1
- package/dist/guard.js +31 -1
- package/dist/guard.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 +248 -110
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +1 -1
- package/dist/self-host.js +2 -2
- 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
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
|
+
};
|
|
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,61 @@ 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
|
if (!result.requiresConfirmation) {
|
|
1881
1940
|
state.keys[key] = now.toISOString();
|
|
1882
1941
|
}
|
|
1883
|
-
await writeGuardState(agent, sessionId, state, options.home);
|
|
1942
|
+
await writeGuardState(agent, sessionId, state, options.home).catch(() => undefined);
|
|
1884
1943
|
if (result.mode === "off") {
|
|
1885
1944
|
return undefined;
|
|
1886
1945
|
}
|
|
1887
1946
|
if (result.requiresConfirmation) {
|
|
1888
1947
|
const reason = guardConfirmationReason(result);
|
|
1948
|
+
if (agent === "polytoken")
|
|
1949
|
+
return { outcome: "deny", reason };
|
|
1889
1950
|
if (agent === "copilot-cli") {
|
|
1890
1951
|
return {
|
|
1891
|
-
permissionDecision: "ask",
|
|
1952
|
+
permissionDecision: result.offline || result.policyUnavailable ? "deny" : "ask",
|
|
1892
1953
|
permissionDecisionReason: reason,
|
|
1893
1954
|
};
|
|
1894
1955
|
}
|
|
@@ -1904,7 +1965,8 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1904
1965
|
return {
|
|
1905
1966
|
hookSpecificOutput: {
|
|
1906
1967
|
hookEventName: "PreToolUse",
|
|
1907
|
-
|
|
1968
|
+
// Codex parses `ask` as unsupported and then continues the tool call.
|
|
1969
|
+
permissionDecision: agent === "codex" || result.offline || result.policyUnavailable ? "deny" : "ask",
|
|
1908
1970
|
permissionDecisionReason: reason,
|
|
1909
1971
|
},
|
|
1910
1972
|
};
|
|
@@ -1917,6 +1979,8 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
|
1917
1979
|
// context was already added by userPromptTransformed.
|
|
1918
1980
|
return undefined;
|
|
1919
1981
|
}
|
|
1982
|
+
if (agent === "polytoken")
|
|
1983
|
+
return { outcome: "allow" };
|
|
1920
1984
|
if (agent === "cursor") {
|
|
1921
1985
|
// Cursor cannot inject agent context at the shell boundary on allow.
|
|
1922
1986
|
return undefined;
|
|
@@ -1946,6 +2010,20 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1946
2010
|
return undefined;
|
|
1947
2011
|
}
|
|
1948
2012
|
const now = (options.now ?? (() => new Date()))();
|
|
2013
|
+
if (agent === "cursor" && eventName === "SessionStart") {
|
|
2014
|
+
const cwd = stringField(input.cwd);
|
|
2015
|
+
if (!cwd)
|
|
2016
|
+
return { additional_context: "Lore needs a single repository workspace to provide shared context." };
|
|
2017
|
+
const store = await runtimeStore(config, options.home);
|
|
2018
|
+
await store.writeState(cursorSessionKey(sessionId), { sessionId, cwd, updatedAt: now.toISOString() });
|
|
2019
|
+
return { additional_context: [
|
|
2020
|
+
"Lore shared context is connected through the lore-native MCP server.",
|
|
2021
|
+
`For this conversation use sessionId ${JSON.stringify(sessionId)}.`,
|
|
2022
|
+
"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.",
|
|
2023
|
+
"Use the returned context when relevant. If retrieval fails, report that limitation; do not invent shared knowledge.",
|
|
2024
|
+
"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.",
|
|
2025
|
+
].join("\n") };
|
|
2026
|
+
}
|
|
1949
2027
|
if (eventName === "Stop" || eventName === "AssistantResponse") {
|
|
1950
2028
|
await savePending(input, agent, sessionId, now, options.home);
|
|
1951
2029
|
return undefined;
|
|
@@ -1961,6 +2039,20 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1961
2039
|
return await handleGuardEvent(input, agent, config, sessionId, options, now);
|
|
1962
2040
|
}
|
|
1963
2041
|
catch {
|
|
2042
|
+
const prior = await readGuardState(agent, sessionId, options.home).catch(() => null);
|
|
2043
|
+
const saved = await runtimeStore(config, options.home).then((store) => store.readState("guard-policy-state")).catch(() => null);
|
|
2044
|
+
const scope = await repositoryScope(stringField(input.cwd)).catch(() => undefined);
|
|
2045
|
+
const override = isObject(saved) && Array.isArray(saved.overrides) ? saved.overrides.find((entry) => isObject(entry) && entry.repo === scope?.repo) : undefined;
|
|
2046
|
+
if (prior?.mode === "enforce" || (isObject(override) ? override.mode : isObject(saved) ? saved.mode : undefined) === "enforce") {
|
|
2047
|
+
const reason = "Lore Guard cannot verify its enforcement policy. Restore Lore before retrying.";
|
|
2048
|
+
if (agent === "cursor")
|
|
2049
|
+
return { permission: "deny", userMessage: reason, agentMessage: reason };
|
|
2050
|
+
if (agent === "copilot-cli")
|
|
2051
|
+
return { permissionDecision: "deny", permissionDecisionReason: reason };
|
|
2052
|
+
if (agent === "polytoken")
|
|
2053
|
+
return { outcome: "deny", reason };
|
|
2054
|
+
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } };
|
|
2055
|
+
}
|
|
1964
2056
|
const warning = "Lore Guard is unavailable; this action is proceeding without a live policy decision.";
|
|
1965
2057
|
if (agent === "cursor") {
|
|
1966
2058
|
return {
|
|
@@ -1968,7 +2060,7 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1968
2060
|
agentMessage: warning,
|
|
1969
2061
|
};
|
|
1970
2062
|
}
|
|
1971
|
-
if (agent === "copilot-cli") {
|
|
2063
|
+
if (agent === "copilot-cli" || agent === "polytoken") {
|
|
1972
2064
|
return undefined;
|
|
1973
2065
|
}
|
|
1974
2066
|
return {
|
|
@@ -1979,6 +2071,55 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1979
2071
|
};
|
|
1980
2072
|
}
|
|
1981
2073
|
}
|
|
2074
|
+
if (agent === "cursor") {
|
|
2075
|
+
const store = await runtimeStore(config, options.home);
|
|
2076
|
+
const existing = await store.readState(cursorSessionKey(sessionId));
|
|
2077
|
+
const cwd = stringField(input.cwd);
|
|
2078
|
+
if (cwd)
|
|
2079
|
+
await store.writeState(cursorSessionKey(sessionId), {
|
|
2080
|
+
...(isObject(existing) ? existing : {}), sessionId, cwd,
|
|
2081
|
+
...(stringField(input.prompt) ? { prompt: redactSecrets(String(input.prompt)) } : {}),
|
|
2082
|
+
...(stringField(input.prompt_id) ? { promptId: input.prompt_id } : {}), updatedAt: now.toISOString(),
|
|
2083
|
+
});
|
|
2084
|
+
// Cursor's prompt hook supports submission control only. The MCP call
|
|
2085
|
+
// delivers the context; capture here retains native editor event identity.
|
|
2086
|
+
await handlePromptEvent(input, agent, sessionId, config, options, now);
|
|
2087
|
+
return { continue: true };
|
|
2088
|
+
}
|
|
2089
|
+
return handlePromptEvent(input, agent, sessionId, config, options, now);
|
|
2090
|
+
}
|
|
2091
|
+
function cursorSessionKey(sessionId) {
|
|
2092
|
+
return `cursor-session-${sha256(sessionId)}`;
|
|
2093
|
+
}
|
|
2094
|
+
/** MCP requests are bound to a session/workspace observed by the native hook. */
|
|
2095
|
+
async function cursorSession(sessionId, options) {
|
|
2096
|
+
const config = await readRuntimeConfig(options.home);
|
|
2097
|
+
if (!config?.agents.includes("cursor"))
|
|
2098
|
+
throw new Error("Reconnect Cursor to Lore before requesting context.");
|
|
2099
|
+
const store = await runtimeStore(config, options.home);
|
|
2100
|
+
const state = await store.readState(cursorSessionKey(sessionId));
|
|
2101
|
+
if (!isObject(state) || state.sessionId !== sessionId || !stringField(state.cwd)) {
|
|
2102
|
+
throw new Error("Start a new Cursor conversation to register this Lore session.");
|
|
2103
|
+
}
|
|
2104
|
+
return { config, store, state, cwd: stringField(state.cwd) };
|
|
2105
|
+
}
|
|
2106
|
+
export async function getCursorContext(sessionId, prompt, options = {}) {
|
|
2107
|
+
const { config, state, cwd } = await cursorSession(sessionId, options);
|
|
2108
|
+
const now = (options.now ?? (() => new Date()))();
|
|
2109
|
+
const result = await handlePromptEvent({ session_id: sessionId, cwd,
|
|
2110
|
+
prompt: stringField(state.prompt) ?? prompt,
|
|
2111
|
+
...(stringField(state.promptId) ? { prompt_id: state.promptId } : {}),
|
|
2112
|
+
}, "cursor", sessionId, config, options, now, stringField(state.prompt) === undefined);
|
|
2113
|
+
return result?.additional_context || "No relevant shared knowledge was found for this request.";
|
|
2114
|
+
}
|
|
2115
|
+
export async function completeCursorResponse(sessionId, response, options = {}) {
|
|
2116
|
+
const { store, state, cwd } = await cursorSession(sessionId, options);
|
|
2117
|
+
const now = (options.now ?? (() => new Date()))();
|
|
2118
|
+
await savePending({ cwd, last_assistant_message: response }, "cursor", sessionId, now, options.home);
|
|
2119
|
+
const { prompt: _prompt, promptId: _promptId, ...rest } = state;
|
|
2120
|
+
await store.writeState(cursorSessionKey(sessionId), { ...rest, updatedAt: now.toISOString() });
|
|
2121
|
+
}
|
|
2122
|
+
async function handlePromptEvent(input, agent, sessionId, config, options, now, capture = true) {
|
|
1982
2123
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
1983
2124
|
const prompt = stringField(input.prompt);
|
|
1984
2125
|
if (prompt === undefined) {
|
|
@@ -1999,13 +2140,7 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
1999
2140
|
return agent === "polytoken"
|
|
2000
2141
|
? { outcome: "accept", additional_context: notice }
|
|
2001
2142
|
: agent === "cursor"
|
|
2002
|
-
? {
|
|
2003
|
-
continue: true,
|
|
2004
|
-
hookSpecificOutput: {
|
|
2005
|
-
hookEventName: "UserPromptSubmit",
|
|
2006
|
-
additionalContext: notice,
|
|
2007
|
-
},
|
|
2008
|
-
}
|
|
2143
|
+
? { additional_context: notice }
|
|
2009
2144
|
: agent === "copilot-cli"
|
|
2010
2145
|
? {
|
|
2011
2146
|
modifiedTransformedPrompt: `${notice}\n\n${stringField(input.transformed_prompt) ?? prompt}`,
|
|
@@ -2026,7 +2161,7 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
2026
2161
|
catch {
|
|
2027
2162
|
notices.push("Lore retained queued captures locally, but replay could not run. Run lore doctor.");
|
|
2028
2163
|
}
|
|
2029
|
-
const pending = await readPending(agent, sessionId, options.home);
|
|
2164
|
+
const pending = capture ? await readPending(agent, sessionId, options.home) : null;
|
|
2030
2165
|
const lineage = await readLineageMetadata(options.home, options.environment ?? process.env);
|
|
2031
2166
|
let delivery = emptyDelivery();
|
|
2032
2167
|
if (pending !== null) {
|
|
@@ -2100,8 +2235,10 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
2100
2235
|
}
|
|
2101
2236
|
let enqueued = false;
|
|
2102
2237
|
try {
|
|
2103
|
-
|
|
2104
|
-
|
|
2238
|
+
if (capture) {
|
|
2239
|
+
await enqueueCapture(store, { kind: "prompt", request: observation });
|
|
2240
|
+
enqueued = true;
|
|
2241
|
+
}
|
|
2105
2242
|
}
|
|
2106
2243
|
catch (error) {
|
|
2107
2244
|
notices.push(error instanceof ReliabilityStoreError &&
|
|
@@ -2182,6 +2319,8 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
2182
2319
|
additional_context: injectedContext,
|
|
2183
2320
|
};
|
|
2184
2321
|
}
|
|
2322
|
+
if (agent === "cursor")
|
|
2323
|
+
return { additional_context: injectedContext };
|
|
2185
2324
|
if (agent === "copilot-cli") {
|
|
2186
2325
|
const copilotContext = [delivery.context, receipt, reliabilityNotice]
|
|
2187
2326
|
.filter((entry) => entry !== undefined && entry !== "")
|
|
@@ -2194,7 +2333,6 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
2194
2333
|
};
|
|
2195
2334
|
}
|
|
2196
2335
|
return {
|
|
2197
|
-
...(agent === "cursor" ? { continue: true } : {}),
|
|
2198
2336
|
...(systemMessage === undefined ? {} : { systemMessage }),
|
|
2199
2337
|
...(injectedContext === ""
|
|
2200
2338
|
? {}
|