@lore-co/cli 0.1.18 → 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/dist/runtime.js CHANGED
@@ -12,10 +12,13 @@ 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",
18
19
  "claude",
20
+ "copilot-cli",
21
+ "copilot-vscode",
19
22
  "cursor",
20
23
  "polytoken",
21
24
  ];
@@ -125,24 +128,72 @@ function stringField(value) {
125
128
  function normalizeHookInput(input, agent, environment) {
126
129
  const rawEventName = stringField(input.hook_event_name) ??
127
130
  stringField(input.event) ??
131
+ (agent === "copilot-cli"
132
+ ? stringField(environment.LORE_HOOK_EVENT)
133
+ : undefined) ??
128
134
  (agent === "polytoken"
129
135
  ? stringField(environment.POLYTOKEN_HOOK_EVENT)
130
136
  : undefined);
131
- if (agent !== "cursor" && agent !== "polytoken") {
137
+ if (agent !== "cursor" &&
138
+ agent !== "polytoken" &&
139
+ agent !== "copilot-cli") {
132
140
  return {
133
141
  input,
134
142
  eventName: rawEventName,
135
143
  sessionId: stringField(input.session_id),
136
144
  };
137
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
+ }
155
+ const eventName = rawEventName === "userPromptTransformed"
156
+ ? "UserPromptSubmit"
157
+ : rawEventName === "preToolUse"
158
+ ? "PreToolUse"
159
+ : rawEventName === "agentStop"
160
+ ? "Stop"
161
+ : rawEventName === "sessionEnd"
162
+ ? "SessionEnd"
163
+ : rawEventName;
164
+ const sessionId = stringField(input.sessionId) ?? stringField(input.session_id);
165
+ return {
166
+ input: {
167
+ ...input,
168
+ ...(rawEventName === undefined ? {} : { event: rawEventName }),
169
+ ...(sessionId === undefined ? {} : { session_id: sessionId }),
170
+ ...(stringField(input.transformedPrompt) === undefined
171
+ ? {}
172
+ : { transformed_prompt: input.transformedPrompt }),
173
+ ...(eventName === "PreToolUse"
174
+ ? {
175
+ tool_name: input.toolName,
176
+ tool_input: toolArgs,
177
+ }
178
+ : {}),
179
+ ...(eventName === "Stop"
180
+ ? { last_assistant_message: input.response }
181
+ : {}),
182
+ },
183
+ eventName,
184
+ sessionId,
185
+ };
186
+ }
138
187
  if (agent === "polytoken") {
139
188
  const eventName = rawEventName === "pre_user_prompt"
140
189
  ? "UserPromptSubmit"
141
190
  : rawEventName === "post_model_turn"
142
191
  ? "AssistantResponse"
143
- : rawEventName === "session_start"
144
- ? "SessionStart"
145
- : rawEventName;
192
+ : rawEventName === "pre_tool_use"
193
+ ? "PreToolUse"
194
+ : rawEventName === "session_start"
195
+ ? "SessionStart"
196
+ : rawEventName;
146
197
  const sessionId = stringField(input.session_id) ??
147
198
  stringField(environment.POLYTOKEN_SESSION_ID);
148
199
  const cwd = stringField(input.cwd) ??
@@ -165,6 +216,7 @@ function normalizeHookInput(input, agent, environment) {
165
216
  ...input,
166
217
  ...(sessionId === undefined ? {} : { session_id: sessionId }),
167
218
  ...(cwd === undefined ? {} : { cwd }),
219
+ ...(eventName === "PreToolUse" ? { tool_input: input.input ?? input.tool_input } : {}),
168
220
  ...(prompt === undefined ? {} : { prompt }),
169
221
  ...(assistantMessage === undefined
170
222
  ? {}
@@ -183,15 +235,17 @@ function normalizeHookInput(input, agent, environment) {
183
235
  : [];
184
236
  const cwd = stringField(input.cwd) ??
185
237
  (workspaceRoots.length === 1 ? workspaceRoots[0] : undefined);
186
- const eventName = rawEventName === "beforeSubmitPrompt"
187
- ? "UserPromptSubmit"
188
- : rawEventName === "afterAgentResponse"
189
- ? "AssistantResponse"
190
- : rawEventName === "sessionEnd"
191
- ? "SessionEnd"
192
- : rawEventName === "beforeShellExecution"
193
- ? "PreToolUse"
194
- : 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;
195
249
  return {
196
250
  input: {
197
251
  ...input,
@@ -225,9 +279,10 @@ class InvalidHookInputError extends Error {
225
279
  }
226
280
  function validHookInput(input, agent, eventName) {
227
281
  const recognized = agent === "polytoken"
228
- ? eventName === "UserPromptSubmit" || eventName === "AssistantResponse"
282
+ ? eventName === "UserPromptSubmit" || eventName === "AssistantResponse" || eventName === "PreToolUse"
229
283
  : agent === "cursor"
230
- ? eventName === "UserPromptSubmit" ||
284
+ ? eventName === "SessionStart" ||
285
+ eventName === "UserPromptSubmit" ||
231
286
  eventName === "AssistantResponse" ||
232
287
  eventName === "PreToolUse" ||
233
288
  eventName === "SessionEnd"
@@ -242,6 +297,10 @@ function validHookInput(input, agent, eventName) {
242
297
  return stringField(input.prompt) !== undefined;
243
298
  }
244
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;
245
304
  return stringField(input.last_assistant_message) !== undefined;
246
305
  }
247
306
  if (eventName === "PreToolUse") {
@@ -249,11 +308,8 @@ function validHookInput(input, agent, eventName) {
249
308
  if (toolName === undefined || !isObject(input.tool_input)) {
250
309
  return false;
251
310
  }
252
- if (GUARD_EDIT_TOOLS.has(toolName)) {
253
- return true;
254
- }
255
- if (GUARD_SHELL_TOOLS.has(toolName)) {
256
- return stringField(input.tool_input.command) !== undefined;
311
+ if (GUARD_SHELL_TOOLS.has(toolName.split(".").at(-1))) {
312
+ return stringField(input.tool_input.command ?? input.tool_input.cmd) !== undefined;
257
313
  }
258
314
  return true;
259
315
  }
@@ -587,7 +643,7 @@ function createPromptObservationRequest(input, agent, sessionId, now, gitContext
587
643
  connector: "lore-cli",
588
644
  agent,
589
645
  sessionId,
590
- eventId: deterministicUuid(`lore-prompt\0${agent}\0${sessionId}\0${promptId ?? "first"}`),
646
+ eventId: deterministicUuid(`lore-prompt\0${agent}\0${sessionId}\0${promptId ?? idempotencyKey}`),
591
647
  prompt: redactSecrets(prompt),
592
648
  ...(promptId === undefined ? {} : { promptId }),
593
649
  timestamp: now.toISOString(),
@@ -907,12 +963,12 @@ function runtimeAccept(config) {
907
963
  }
908
964
  async function runtimeStore(config, home) {
909
965
  const store = new ReliabilityStore(reliabilityWorkspaceKey(config), {
910
- ...(home === undefined ? {} : { home }),
966
+ home: homeDirectory(home),
911
967
  });
912
968
  await store.initialize();
913
969
  await store.migrateLegacyQueue(queueDirectory(home));
914
970
  if (config.workspaceId !== undefined) {
915
- const credentialStore = new ReliabilityStore(credentialReliabilityWorkspaceKey(config), { ...(home === undefined ? {} : { home }) });
971
+ const credentialStore = new ReliabilityStore(credentialReliabilityWorkspaceKey(config), { home: homeDirectory(home) });
916
972
  await credentialStore.transferPendingTo(store).catch(() => undefined);
917
973
  }
918
974
  return store;
@@ -1425,12 +1481,25 @@ function queuedRequest(value) {
1425
1481
  async function flushOne(config, fetchImplementation, store) {
1426
1482
  const claimed = await store.claimNext({
1427
1483
  workerId: `native-hook:${process.pid}`,
1428
- kinds: ["turn", "prompt"],
1484
+ kinds: ["turn", "prompt", "guard_override"],
1429
1485
  });
1430
1486
  if (claimed === null || claimed.claim === undefined) {
1431
1487
  return null;
1432
1488
  }
1433
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
+ }
1434
1503
  const queued = queuedRequest(claimed.payload);
1435
1504
  if (queued === null) {
1436
1505
  const failed = await store.failClaim({
@@ -1515,8 +1584,8 @@ function receiptMessage(config, agent, delivery) {
1515
1584
  }
1516
1585
  // ---------------------------------------------------------------------------
1517
1586
  // Context Guard: proactive checks at tool boundaries (PreToolUse and Cursor
1518
- // beforeShellExecution). Fail-open by design a slow or unreachable hub must
1519
- // never stall an agent's tool call.
1587
+ // beforeShellExecution). Assist degrades on outages; known Enforce uses a
1588
+ // verified saved policy or denies actions whose policy cannot be verified.
1520
1589
  // ---------------------------------------------------------------------------
1521
1590
  /** How long a locally cached "guard is off" verdict suppresses network calls. */
1522
1591
  const GUARD_MODE_TTL_MS = 5 * 60_000;
@@ -1524,51 +1593,14 @@ const GUARD_MODE_TTL_MS = 5 * 60_000;
1524
1593
  const GUARD_KEY_COOLDOWN_MS = 15 * 60_000;
1525
1594
  const GUARD_MAX_CACHED_KEYS = 200;
1526
1595
  const GUARD_TIMEOUT_MS = 4_000;
1527
- const GUARD_EDIT_TOOLS = new Set([
1528
- "Edit",
1529
- "Write",
1530
- "MultiEdit",
1531
- "NotebookEdit",
1532
- "ApplyPatch",
1533
- "apply_patch",
1534
- "edit",
1535
- "write",
1536
- "patch",
1537
- "str_replace_editor",
1538
- ]);
1539
- const GUARD_SHELL_TOOLS = new Set(["Bash", "bash", "Shell", "shell"]);
1540
- const GUARD_COMMIT_COMMAND = /\bgit\s+(?:commit|push)\b/iu;
1541
- 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;
1542
- 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"]);
1543
1597
  /**
1544
1598
  * Decides whether a tool call is a meaningful action boundary. File edits
1545
1599
  * always qualify; shell commands qualify only when they look high-impact
1546
1600
  * (commit, push, deploy, publish, destructive) to keep the guard quiet.
1547
1601
  */
1548
1602
  export function guardTrigger(toolName, toolInput) {
1549
- if (GUARD_EDIT_TOOLS.has(toolName)) {
1550
- const files = [toolInput.file_path, toolInput.notebook_path, toolInput.path]
1551
- .map((value) => stringField(value))
1552
- .filter((value) => value !== undefined);
1553
- return { action: "edit", ...(files.length === 0 ? {} : { files }) };
1554
- }
1555
- if (GUARD_SHELL_TOOLS.has(toolName)) {
1556
- const command = stringField(toolInput.command);
1557
- if (command === undefined) {
1558
- return null;
1559
- }
1560
- if (GUARD_COMMIT_COMMAND.test(command)) {
1561
- return { action: "commit", command };
1562
- }
1563
- if (GUARD_DEPLOY_COMMAND.test(command)) {
1564
- return { action: "deploy", command };
1565
- }
1566
- if (GUARD_DESTRUCTIVE_COMMAND.test(command)) {
1567
- return { action: "command", command };
1568
- }
1569
- return null;
1570
- }
1571
- return null;
1603
+ return guardToolTrigger(toolName, toolInput);
1572
1604
  }
1573
1605
  function guardStatePath(agent, sessionId, home) {
1574
1606
  return resolve(loreDirectory(home), "state", "guard", `${sha256(`${agent}\0${sessionId}`)}.json`);
@@ -1649,6 +1681,7 @@ function guardResultFromResponse(value) {
1649
1681
  return {
1650
1682
  ...(typeof value.checkId === "string" ? { checkId: value.checkId } : {}),
1651
1683
  mode: value.mode,
1684
+ ...(value.policyIssue === "unsupported_required_rules" ? { policyUnavailable: true } : {}),
1652
1685
  requiresConfirmation: value.requiresConfirmation === true,
1653
1686
  ...(typeof confirm.command === "string"
1654
1687
  ? { confirmCommand: confirm.command }
@@ -1701,7 +1734,7 @@ function guardAssistContext(result) {
1701
1734
  lines.push(` ${item.explanation ?? `Source: ${guardItemSource(item)}.`}`);
1702
1735
  }
1703
1736
  if (result.conflictSummaries.length > 0) {
1704
- lines.push("", "Conflicting Lore context (do not silently pick a winner):");
1737
+ lines.push("", "Lore policy notes:");
1705
1738
  for (const summary of result.conflictSummaries) {
1706
1739
  lines.push(`- ${summary}`);
1707
1740
  }
@@ -1709,18 +1742,23 @@ function guardAssistContext(result) {
1709
1742
  return boundedContext(lines.join("\n"));
1710
1743
  }
1711
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.";
1712
1747
  const required = result.items.filter((item) => item.enforcement === "required");
1713
1748
  const shown = required.length > 0 ? required : result.items;
1714
1749
  const lines = [
1715
- "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,
1716
1752
  ...shown.flatMap((item) => [
1717
1753
  `- ${item.content}`,
1718
1754
  ` ${item.explanation ?? `Source: ${guardItemSource(item)}.`}`,
1719
1755
  ]),
1720
1756
  ];
1721
- lines.push(result.confirmCommand === undefined
1722
- ? "Ask the user to confirm before proceeding."
1723
- : `Approve here to proceed, or the user can run: ${result.confirmCommand}`);
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}`);
1724
1762
  return boundedContext(lines.join("\n"));
1725
1763
  }
1726
1764
  async function postGuardCheck(config, request, fetchImplementation, store) {
@@ -1755,13 +1793,65 @@ async function postGuardCheck(config, request, fetchImplementation, store) {
1755
1793
  throw new IncompatibleReliabilityError("Lore Guard snapshot does not bind the returned policy");
1756
1794
  }
1757
1795
  await store.writePolicySnapshot("current", value.policySnapshot);
1796
+ await store.writeState("guard-policy-state", { mode: payload.mode, overrides: payload.overrides, policyVersion: payload.policyVersion });
1758
1797
  return guardResultFromResponse(value.check);
1759
1798
  }
1760
- async function handleGuardEvent(input, agent, config, sessionId, options, now) {
1761
- if (agent === "polytoken") {
1762
- // Polytoken has no tool boundary; its guard runs at prompt injection.
1763
- return undefined;
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."] };
1764
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) {
1765
1855
  const toolName = stringField(input.tool_name);
1766
1856
  if (toolName === undefined) {
1767
1857
  return undefined;
@@ -1772,7 +1862,11 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
1772
1862
  }
1773
1863
  const store = await runtimeStore(config, options.home);
1774
1864
  const state = await readGuardState(agent, sessionId, options.home);
1775
- if (state.mode === "off" &&
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" &&
1776
1870
  state.modeCheckedAt !== undefined &&
1777
1871
  now.getTime() - Date.parse(state.modeCheckedAt) < GUARD_MODE_TTL_MS) {
1778
1872
  await recordLocalGuardMetric(store, { outcome: "reused_decision" }, now).catch(() => undefined);
@@ -1792,8 +1886,7 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
1792
1886
  ? normalizedRepositoryPath(file)
1793
1887
  : normalizedRepositoryPath(relativePath);
1794
1888
  })
1795
- .filter((file) => file !== "")
1796
- .slice(0, 100);
1889
+ .filter((file) => file !== "");
1797
1890
  const key = sha256([
1798
1891
  "guard",
1799
1892
  trigger.action,
@@ -1802,50 +1895,64 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
1802
1895
  ].join("\0"));
1803
1896
  const cachedAt = state.keys[key];
1804
1897
  if (cachedAt !== undefined &&
1898
+ !enforcementMayHaveChanged &&
1899
+ state.mode !== "enforce" &&
1805
1900
  now.getTime() - Date.parse(cachedAt) < GUARD_KEY_COOLDOWN_MS) {
1806
1901
  await recordLocalGuardMetric(store, { outcome: "reused_decision" }, now).catch(() => undefined);
1807
1902
  return undefined;
1808
1903
  }
1809
1904
  const startedAt = Date.now();
1810
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
+ };
1811
1919
  try {
1812
- result = await postGuardCheck(config, {
1813
- connector: "lore-cli",
1814
- agent,
1815
- sessionId,
1816
- action: trigger.action,
1817
- tool: toolName,
1818
- ...(scope?.repo === undefined ? {} : { repo: scope.repo }),
1819
- ...(scope?.path === undefined ? {} : { path: scope.path }),
1820
- ...(files.length === 0 ? {} : { files }),
1821
- ...(trigger.command === undefined
1822
- ? {}
1823
- : { command: redactSecrets(trigger.command).slice(0, 10_000) }),
1824
- }, options.fetch ?? globalThis.fetch, store);
1920
+ result = await postGuardCheck(config, guardRequest, options.fetch ?? globalThis.fetch, store);
1825
1921
  }
1826
1922
  catch (error) {
1827
1923
  await recordLocalGuardMetric(store, { outcome: "failed" }, now).catch(() => undefined);
1828
- throw error;
1924
+ result = await offlineGuard(config, store, guardRequest, now, state.mode === "enforce", isContextTransportFailure(error));
1925
+ if (result === null)
1926
+ throw error;
1829
1927
  }
1830
1928
  if (result === null) {
1831
1929
  await recordLocalGuardMetric(store, { outcome: "failed" }, now).catch(() => undefined);
1832
1930
  throw new IncompatibleReliabilityError("Lore Guard response is invalid");
1833
1931
  }
1834
- await recordLocalGuardMetric(store, {
1835
- outcome: "live_check",
1836
- durationMs: Math.max(0, Date.now() - startedAt),
1837
- }, now).catch(() => undefined);
1932
+ if (!result.offline)
1933
+ await recordLocalGuardMetric(store, {
1934
+ outcome: "live_check",
1935
+ durationMs: Math.max(0, Date.now() - startedAt),
1936
+ }, now).catch(() => undefined);
1838
1937
  state.mode = result.mode;
1839
1938
  state.modeCheckedAt = now.toISOString();
1840
1939
  if (!result.requiresConfirmation) {
1841
1940
  state.keys[key] = now.toISOString();
1842
1941
  }
1843
- await writeGuardState(agent, sessionId, state, options.home);
1942
+ await writeGuardState(agent, sessionId, state, options.home).catch(() => undefined);
1844
1943
  if (result.mode === "off") {
1845
1944
  return undefined;
1846
1945
  }
1847
1946
  if (result.requiresConfirmation) {
1848
1947
  const reason = guardConfirmationReason(result);
1948
+ if (agent === "polytoken")
1949
+ return { outcome: "deny", reason };
1950
+ if (agent === "copilot-cli") {
1951
+ return {
1952
+ permissionDecision: result.offline || result.policyUnavailable ? "deny" : "ask",
1953
+ permissionDecisionReason: reason,
1954
+ };
1955
+ }
1849
1956
  if (agent === "cursor") {
1850
1957
  // Cursor's "ask" verdict is unenforced upstream; deny is the only
1851
1958
  // reliable gate. The message carries the approve-and-retry path.
@@ -1858,7 +1965,8 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
1858
1965
  return {
1859
1966
  hookSpecificOutput: {
1860
1967
  hookEventName: "PreToolUse",
1861
- permissionDecision: "ask",
1968
+ // Codex parses `ask` as unsupported and then continues the tool call.
1969
+ permissionDecision: agent === "codex" || result.offline || result.policyUnavailable ? "deny" : "ask",
1862
1970
  permissionDecisionReason: reason,
1863
1971
  },
1864
1972
  };
@@ -1866,6 +1974,13 @@ async function handleGuardEvent(input, agent, config, sessionId, options, now) {
1866
1974
  if (result.items.length === 0 && result.conflictSummaries.length === 0) {
1867
1975
  return undefined;
1868
1976
  }
1977
+ if (agent === "copilot-cli") {
1978
+ // Copilot CLI's preToolUse output has no context-injection field. Prompt
1979
+ // context was already added by userPromptTransformed.
1980
+ return undefined;
1981
+ }
1982
+ if (agent === "polytoken")
1983
+ return { outcome: "allow" };
1869
1984
  if (agent === "cursor") {
1870
1985
  // Cursor cannot inject agent context at the shell boundary on allow.
1871
1986
  return undefined;
@@ -1895,6 +2010,20 @@ export async function handleHookEvent(value, agent, options = {}) {
1895
2010
  return undefined;
1896
2011
  }
1897
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
+ }
1898
2027
  if (eventName === "Stop" || eventName === "AssistantResponse") {
1899
2028
  await savePending(input, agent, sessionId, now, options.home);
1900
2029
  return undefined;
@@ -1910,6 +2039,20 @@ export async function handleHookEvent(value, agent, options = {}) {
1910
2039
  return await handleGuardEvent(input, agent, config, sessionId, options, now);
1911
2040
  }
1912
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
+ }
1913
2056
  const warning = "Lore Guard is unavailable; this action is proceeding without a live policy decision.";
1914
2057
  if (agent === "cursor") {
1915
2058
  return {
@@ -1917,6 +2060,9 @@ export async function handleHookEvent(value, agent, options = {}) {
1917
2060
  agentMessage: warning,
1918
2061
  };
1919
2062
  }
2063
+ if (agent === "copilot-cli" || agent === "polytoken") {
2064
+ return undefined;
2065
+ }
1920
2066
  return {
1921
2067
  hookSpecificOutput: {
1922
2068
  hookEventName: "PreToolUse",
@@ -1925,6 +2071,55 @@ export async function handleHookEvent(value, agent, options = {}) {
1925
2071
  };
1926
2072
  }
1927
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) {
1928
2123
  const fetchImplementation = options.fetch ?? globalThis.fetch;
1929
2124
  const prompt = stringField(input.prompt);
1930
2125
  if (prompt === undefined) {
@@ -1945,14 +2140,12 @@ export async function handleHookEvent(value, agent, options = {}) {
1945
2140
  return agent === "polytoken"
1946
2141
  ? { outcome: "accept", additional_context: notice }
1947
2142
  : agent === "cursor"
1948
- ? {
1949
- continue: true,
1950
- hookSpecificOutput: {
1951
- hookEventName: "UserPromptSubmit",
1952
- additionalContext: notice,
1953
- },
1954
- }
1955
- : { systemMessage: notice };
2143
+ ? { additional_context: notice }
2144
+ : agent === "copilot-cli"
2145
+ ? {
2146
+ modifiedTransformedPrompt: `${notice}\n\n${stringField(input.transformed_prompt) ?? prompt}`,
2147
+ }
2148
+ : { systemMessage: notice };
1956
2149
  }
1957
2150
  const noticeForFlush = (result) => {
1958
2151
  if (result?.state === "auth-blocked") {
@@ -1968,7 +2161,7 @@ export async function handleHookEvent(value, agent, options = {}) {
1968
2161
  catch {
1969
2162
  notices.push("Lore retained queued captures locally, but replay could not run. Run lore doctor.");
1970
2163
  }
1971
- const pending = await readPending(agent, sessionId, options.home);
2164
+ const pending = capture ? await readPending(agent, sessionId, options.home) : null;
1972
2165
  const lineage = await readLineageMetadata(options.home, options.environment ?? process.env);
1973
2166
  let delivery = emptyDelivery();
1974
2167
  if (pending !== null) {
@@ -2042,8 +2235,10 @@ export async function handleHookEvent(value, agent, options = {}) {
2042
2235
  }
2043
2236
  let enqueued = false;
2044
2237
  try {
2045
- await enqueueCapture(store, { kind: "prompt", request: observation });
2046
- enqueued = true;
2238
+ if (capture) {
2239
+ await enqueueCapture(store, { kind: "prompt", request: observation });
2240
+ enqueued = true;
2241
+ }
2047
2242
  }
2048
2243
  catch (error) {
2049
2244
  notices.push(error instanceof ReliabilityStoreError &&
@@ -2124,8 +2319,20 @@ export async function handleHookEvent(value, agent, options = {}) {
2124
2319
  additional_context: injectedContext,
2125
2320
  };
2126
2321
  }
2322
+ if (agent === "cursor")
2323
+ return { additional_context: injectedContext };
2324
+ if (agent === "copilot-cli") {
2325
+ const copilotContext = [delivery.context, receipt, reliabilityNotice]
2326
+ .filter((entry) => entry !== undefined && entry !== "")
2327
+ .join("\n\n");
2328
+ if (copilotContext === "") {
2329
+ return undefined;
2330
+ }
2331
+ return {
2332
+ modifiedTransformedPrompt: `${copilotContext}\n\n${stringField(input.transformed_prompt) ?? prompt}`,
2333
+ };
2334
+ }
2127
2335
  return {
2128
- ...(agent === "cursor" ? { continue: true } : {}),
2129
2336
  ...(systemMessage === undefined ? {} : { systemMessage }),
2130
2337
  ...(injectedContext === ""
2131
2338
  ? {}
@@ -2154,7 +2361,9 @@ function parseAgent(args) {
2154
2361
  return isCommandHookAgent(value) ? value : null;
2155
2362
  }
2156
2363
  function nativeIntegrationId(agent) {
2157
- return `native/${agent}`;
2364
+ return agent === "copilot-vscode"
2365
+ ? "preview/copilot-vscode"
2366
+ : `native/${agent}`;
2158
2367
  }
2159
2368
  async function beginHookInvocation(agent) {
2160
2369
  const config = await readRuntimeConfig();