@lore-co/cli 0.1.15 → 0.1.16
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 +26 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +37 -4
- package/dist/cli.js.map +1 -1
- package/dist/generated-assets.d.ts +4 -4
- package/dist/generated-assets.js +4 -4
- package/dist/generated-assets.js.map +1 -1
- package/dist/guard.d.ts +3 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +170 -0
- package/dist/guard.js.map +1 -0
- package/dist/runtime.d.ts +22 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +320 -2
- 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 +5 -5
- package/dist/update.js.map +1 -1
- package/package.json +3 -3
package/dist/guard.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { GuardApproveResponseSchema, WorkspaceGuardPolicySchema, } from "@lore-co/core";
|
|
4
|
+
import { loreApiRequest } from "./api.js";
|
|
5
|
+
import { repositoryScopeFromGitRoot } from "./repository.js";
|
|
6
|
+
const execFile = promisify(execFileCallback);
|
|
7
|
+
const GUARD_HELP = `lore guard
|
|
8
|
+
Control Context Guard: proactive Lore context at action boundaries.
|
|
9
|
+
|
|
10
|
+
Modes:
|
|
11
|
+
off Lore only responds when asked.
|
|
12
|
+
assist Lore automatically gives agents relevant context before
|
|
13
|
+
important actions. Recommended.
|
|
14
|
+
enforce Lore can require confirmation for actions that conflict with
|
|
15
|
+
required (governed) rules.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
lore guard status [--json]
|
|
19
|
+
lore guard off [--repo [name]]
|
|
20
|
+
lore guard assist [--repo [name]]
|
|
21
|
+
lore guard enforce [--repo [name]]
|
|
22
|
+
lore guard approve <check-id>
|
|
23
|
+
lore guard clear-override [--repo [name]]
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--repo [name] Apply to one repository instead of the workspace default.
|
|
27
|
+
Omit the value to use the current repository.
|
|
28
|
+
--json Print machine-readable output
|
|
29
|
+
|
|
30
|
+
Notes:
|
|
31
|
+
Workspace-level changes need a token attributed to an owner or admin.
|
|
32
|
+
Rules only gate actions in enforce mode when an owner or admin marks
|
|
33
|
+
them "required" in the dashboard.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
lore guard status
|
|
37
|
+
lore guard assist
|
|
38
|
+
lore guard enforce --repo
|
|
39
|
+
lore guard off --repo payments-api
|
|
40
|
+
lore guard approve 6f1d3f9a-4b7e-4c1e-9a2b-1c3d5e7f9a0b
|
|
41
|
+
`;
|
|
42
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
43
|
+
async function detectRepoScope() {
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = await execFile("git", ["rev-parse", "--show-toplevel"], { timeout: 5_000 });
|
|
46
|
+
const root = stdout.trim();
|
|
47
|
+
if (root === "") {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return await repositoryScopeFromGitRoot(root);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolves the --repo option: absent → undefined, bare flag → detected from
|
|
58
|
+
* the current git repository, valued → that value.
|
|
59
|
+
*/
|
|
60
|
+
async function resolveRepoOption(args) {
|
|
61
|
+
const index = args.indexOf("--repo");
|
|
62
|
+
if (index < 0) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const next = args[index + 1];
|
|
66
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
67
|
+
return next;
|
|
68
|
+
}
|
|
69
|
+
const detected = await detectRepoScope();
|
|
70
|
+
if (detected === undefined) {
|
|
71
|
+
throw new Error("Not inside a git repository. Pass the repository name: --repo <name>");
|
|
72
|
+
}
|
|
73
|
+
return detected;
|
|
74
|
+
}
|
|
75
|
+
function isGuardMode(value) {
|
|
76
|
+
return value === "off" || value === "assist" || value === "enforce";
|
|
77
|
+
}
|
|
78
|
+
function describeMode(mode) {
|
|
79
|
+
switch (mode) {
|
|
80
|
+
case "off":
|
|
81
|
+
return "off — Lore only responds when asked";
|
|
82
|
+
case "assist":
|
|
83
|
+
return "assist — relevant context is surfaced before important actions";
|
|
84
|
+
case "enforce":
|
|
85
|
+
return "enforce — required rules can ask for confirmation";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function writeStatus(policy, currentRepo) {
|
|
89
|
+
const override = currentRepo === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: policy.overrides.find((entry) => entry.repo === currentRepo);
|
|
92
|
+
const effective = override?.mode ?? policy.mode;
|
|
93
|
+
const lines = [
|
|
94
|
+
"Context Guard",
|
|
95
|
+
`Mode: ${describeMode(effective)}`,
|
|
96
|
+
`Scope: ${override === undefined ? "workspace default" : `repo override (${override.repo})`}`,
|
|
97
|
+
`Workspace default: ${policy.mode}`,
|
|
98
|
+
];
|
|
99
|
+
if (policy.overrides.length > 0) {
|
|
100
|
+
lines.push("Repo overrides:");
|
|
101
|
+
for (const entry of policy.overrides) {
|
|
102
|
+
lines.push(` ${entry.repo}: ${entry.mode}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
lines.push("Repo overrides: none");
|
|
107
|
+
}
|
|
108
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
109
|
+
}
|
|
110
|
+
export async function runGuardCommand(args, config) {
|
|
111
|
+
if (args.length === 0 ||
|
|
112
|
+
args.includes("--help") ||
|
|
113
|
+
args.includes("-h")) {
|
|
114
|
+
process.stdout.write(GUARD_HELP);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (config === null) {
|
|
118
|
+
throw new Error("Lore is not connected. Run: lore connect --token <workspace-token>");
|
|
119
|
+
}
|
|
120
|
+
const subcommand = args[0];
|
|
121
|
+
const rest = args.slice(1);
|
|
122
|
+
if (subcommand === "status") {
|
|
123
|
+
const policy = await loreApiRequest(config, "GET", "/v1/guard/policy", WorkspaceGuardPolicySchema);
|
|
124
|
+
if (args.includes("--json")) {
|
|
125
|
+
process.stdout.write(`${JSON.stringify(policy, null, 2)}\n`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
writeStatus(policy, await detectRepoScope());
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (subcommand === "mode" || isGuardMode(subcommand)) {
|
|
132
|
+
const mode = subcommand === "mode" ? rest[0] : subcommand;
|
|
133
|
+
if (!isGuardMode(mode)) {
|
|
134
|
+
throw new Error("Usage: lore guard <off|assist|enforce> [--repo [name]]");
|
|
135
|
+
}
|
|
136
|
+
const repo = await resolveRepoOption(subcommand === "mode" ? rest.slice(1) : rest);
|
|
137
|
+
const policy = await loreApiRequest(config, "PATCH", "/v1/guard/policy", WorkspaceGuardPolicySchema, repo === undefined ? { mode } : { setOverride: { repo, mode } });
|
|
138
|
+
process.stdout.write(repo === undefined
|
|
139
|
+
? `Context Guard workspace default is now ${mode}.\n`
|
|
140
|
+
: `Context Guard for ${repo} is now ${mode}.\n`);
|
|
141
|
+
if (mode === "enforce") {
|
|
142
|
+
process.stdout.write("Enforce gates only rules marked \"required\" in the dashboard.\n");
|
|
143
|
+
}
|
|
144
|
+
writeStatus(policy, repo ?? (await detectRepoScope()));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (subcommand === "clear-override") {
|
|
148
|
+
const repo = await resolveRepoOption(rest.includes("--repo") ? rest : [...rest, "--repo"]);
|
|
149
|
+
if (repo === undefined) {
|
|
150
|
+
throw new Error("Usage: lore guard clear-override [--repo [name]]");
|
|
151
|
+
}
|
|
152
|
+
const policy = await loreApiRequest(config, "PATCH", "/v1/guard/policy", WorkspaceGuardPolicySchema, { removeOverride: { repo } });
|
|
153
|
+
process.stdout.write(`Removed the Context Guard override for ${repo}.\n`);
|
|
154
|
+
writeStatus(policy, await detectRepoScope());
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (subcommand === "approve") {
|
|
158
|
+
const checkId = rest[0];
|
|
159
|
+
if (checkId === undefined || !UUID_PATTERN.test(checkId)) {
|
|
160
|
+
throw new Error("Usage: lore guard approve <check-id>");
|
|
161
|
+
}
|
|
162
|
+
const result = await loreApiRequest(config, "POST", "/v1/guard/approvals", GuardApproveResponseSchema, { checkId });
|
|
163
|
+
process.stdout.write(`Approved ${result.approved} rule(s) for this session${result.expiresAt === null
|
|
164
|
+
? ""
|
|
165
|
+
: ` until ${result.expiresAt.slice(0, 16).replace("T", " ")}`}. Retry the action in your agent.\n`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`Unknown guard subcommand: ${subcommand}\nTry: lore guard --help`);
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -62,6 +62,9 @@ interface HookInput {
|
|
|
62
62
|
response?: unknown;
|
|
63
63
|
message?: unknown;
|
|
64
64
|
text?: unknown;
|
|
65
|
+
tool_name?: unknown;
|
|
66
|
+
tool_input?: unknown;
|
|
67
|
+
command?: unknown;
|
|
65
68
|
}
|
|
66
69
|
export interface HookRuntimeOptions {
|
|
67
70
|
home?: string;
|
|
@@ -74,9 +77,15 @@ export interface HookResult {
|
|
|
74
77
|
systemMessage?: string;
|
|
75
78
|
outcome?: "accept";
|
|
76
79
|
additional_context?: string;
|
|
80
|
+
/** Cursor beforeShellExecution verdict ("ask" is unenforced upstream). */
|
|
81
|
+
permission?: "deny";
|
|
82
|
+
userMessage?: string;
|
|
83
|
+
agentMessage?: string;
|
|
77
84
|
hookSpecificOutput?: {
|
|
78
|
-
hookEventName: "UserPromptSubmit";
|
|
79
|
-
additionalContext
|
|
85
|
+
hookEventName: "UserPromptSubmit" | "PreToolUse";
|
|
86
|
+
additionalContext?: string;
|
|
87
|
+
permissionDecision?: "ask";
|
|
88
|
+
permissionDecisionReason?: string;
|
|
80
89
|
};
|
|
81
90
|
}
|
|
82
91
|
export interface LineageMetadata {
|
|
@@ -93,6 +102,17 @@ export interface LineageMetadata {
|
|
|
93
102
|
export declare function readLineageMetadata(home?: string, environment?: NodeJS.ProcessEnv): Promise<LineageMetadata>;
|
|
94
103
|
export declare function redactSecrets(text: string): string;
|
|
95
104
|
export declare function createTurnRequest(input: HookInput, agent: CommandHookAgentName, pending: PendingAssistantMessage, now: Date, lineage?: LineageMetadata): Promise<TurnRequest | null>;
|
|
105
|
+
interface GuardTrigger {
|
|
106
|
+
action: "edit" | "commit" | "deploy" | "command";
|
|
107
|
+
files?: string[];
|
|
108
|
+
command?: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Decides whether a tool call is a meaningful action boundary. File edits
|
|
112
|
+
* always qualify; shell commands qualify only when they look high-impact
|
|
113
|
+
* (commit, push, deploy, publish, destructive) to keep the guard quiet.
|
|
114
|
+
*/
|
|
115
|
+
export declare function guardTrigger(toolName: string, toolInput: Record<string, unknown>): GuardTrigger | null;
|
|
96
116
|
export declare function handleHookEvent(value: unknown, agent: CommandHookAgentName, options?: HookRuntimeOptions): Promise<HookResult | undefined>;
|
|
97
117
|
export declare function runHook(args?: readonly string[]): Promise<void>;
|
|
98
118
|
export {};
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAiCA,eAAO,MAAM,wBAAwB,qDAK3B,CAAC;AACX,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAC7E,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG,oBAAoB,CAAC;AAgB7C,UAAU,uBAAuB;IAC/B,KAAK,EAAE,oBAAoB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,UAAU,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,oBAAoB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,WAAW,EAAE;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,aAAa,EAAE;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE;QACT,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AA2BD,UAAU,SAAS;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime.js
CHANGED
|
@@ -9,7 +9,7 @@ import { promisify } from "node:util";
|
|
|
9
9
|
import { boundedUtf8Text, repositoryScopeFromGitRoot, } from "./repository.js";
|
|
10
10
|
const RUNTIME_VERSION = typeof __LORE_VERSION__ === "string" && __LORE_VERSION__ !== ""
|
|
11
11
|
? __LORE_VERSION__
|
|
12
|
-
: "0.1.
|
|
12
|
+
: "0.1.16";
|
|
13
13
|
const IS_STANDALONE_RUNTIME = typeof __LORE_STANDALONE__ === "boolean" && __LORE_STANDALONE__;
|
|
14
14
|
export const COMMAND_HOOK_AGENT_NAMES = [
|
|
15
15
|
"codex",
|
|
@@ -153,10 +153,18 @@ function normalizeHookInput(input, agent, environment) {
|
|
|
153
153
|
? "AssistantResponse"
|
|
154
154
|
: rawEventName === "sessionEnd"
|
|
155
155
|
? "SessionEnd"
|
|
156
|
-
: rawEventName
|
|
156
|
+
: rawEventName === "beforeShellExecution"
|
|
157
|
+
? "PreToolUse"
|
|
158
|
+
: rawEventName;
|
|
157
159
|
return {
|
|
158
160
|
input: {
|
|
159
161
|
...input,
|
|
162
|
+
...(eventName === "PreToolUse"
|
|
163
|
+
? {
|
|
164
|
+
tool_name: "Bash",
|
|
165
|
+
tool_input: { command: input.command },
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
160
168
|
...(sessionId === undefined ? {} : { session_id: sessionId }),
|
|
161
169
|
...(cwd === undefined ? {} : { cwd }),
|
|
162
170
|
...(stringField(input.prompt_id) !== undefined
|
|
@@ -863,6 +871,307 @@ function receiptMessage(config, agent, delivery) {
|
|
|
863
871
|
}
|
|
864
872
|
return boundedContext(parts.join(" · "));
|
|
865
873
|
}
|
|
874
|
+
// ---------------------------------------------------------------------------
|
|
875
|
+
// Context Guard: proactive checks at tool boundaries (PreToolUse and Cursor
|
|
876
|
+
// beforeShellExecution). Fail-open by design — a slow or unreachable hub must
|
|
877
|
+
// never stall an agent's tool call.
|
|
878
|
+
// ---------------------------------------------------------------------------
|
|
879
|
+
/** How long a locally cached "guard is off" verdict suppresses network calls. */
|
|
880
|
+
const GUARD_MODE_TTL_MS = 5 * 60_000;
|
|
881
|
+
/** How long an already-surfaced action key stays quiet in this session. */
|
|
882
|
+
const GUARD_KEY_COOLDOWN_MS = 15 * 60_000;
|
|
883
|
+
const GUARD_MAX_CACHED_KEYS = 200;
|
|
884
|
+
const GUARD_TIMEOUT_MS = 4_000;
|
|
885
|
+
const GUARD_EDIT_TOOLS = new Set([
|
|
886
|
+
"Edit",
|
|
887
|
+
"Write",
|
|
888
|
+
"MultiEdit",
|
|
889
|
+
"NotebookEdit",
|
|
890
|
+
"ApplyPatch",
|
|
891
|
+
"apply_patch",
|
|
892
|
+
"edit",
|
|
893
|
+
"write",
|
|
894
|
+
"patch",
|
|
895
|
+
"str_replace_editor",
|
|
896
|
+
]);
|
|
897
|
+
const GUARD_SHELL_TOOLS = new Set(["Bash", "bash", "Shell", "shell"]);
|
|
898
|
+
const GUARD_COMMIT_COMMAND = /\bgit\s+(?:commit|push)\b/iu;
|
|
899
|
+
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;
|
|
900
|
+
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;
|
|
901
|
+
/**
|
|
902
|
+
* Decides whether a tool call is a meaningful action boundary. File edits
|
|
903
|
+
* always qualify; shell commands qualify only when they look high-impact
|
|
904
|
+
* (commit, push, deploy, publish, destructive) to keep the guard quiet.
|
|
905
|
+
*/
|
|
906
|
+
export function guardTrigger(toolName, toolInput) {
|
|
907
|
+
if (GUARD_EDIT_TOOLS.has(toolName)) {
|
|
908
|
+
const files = [toolInput.file_path, toolInput.notebook_path, toolInput.path]
|
|
909
|
+
.map((value) => stringField(value))
|
|
910
|
+
.filter((value) => value !== undefined);
|
|
911
|
+
return { action: "edit", ...(files.length === 0 ? {} : { files }) };
|
|
912
|
+
}
|
|
913
|
+
if (GUARD_SHELL_TOOLS.has(toolName)) {
|
|
914
|
+
const command = stringField(toolInput.command);
|
|
915
|
+
if (command === undefined) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
if (GUARD_COMMIT_COMMAND.test(command)) {
|
|
919
|
+
return { action: "commit", command };
|
|
920
|
+
}
|
|
921
|
+
if (GUARD_DEPLOY_COMMAND.test(command)) {
|
|
922
|
+
return { action: "deploy", command };
|
|
923
|
+
}
|
|
924
|
+
if (GUARD_DESTRUCTIVE_COMMAND.test(command)) {
|
|
925
|
+
return { action: "command", command };
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
function guardStatePath(agent, sessionId, home) {
|
|
932
|
+
return resolve(loreDirectory(home), "state", "guard", `${sha256(`${agent}\0${sessionId}`)}.json`);
|
|
933
|
+
}
|
|
934
|
+
async function readGuardState(agent, sessionId, home) {
|
|
935
|
+
try {
|
|
936
|
+
const parsed = JSON.parse(await readFile(guardStatePath(agent, sessionId, home), "utf8"));
|
|
937
|
+
if (isObject(parsed)) {
|
|
938
|
+
return {
|
|
939
|
+
...(typeof parsed.mode === "string" ? { mode: parsed.mode } : {}),
|
|
940
|
+
...(typeof parsed.modeCheckedAt === "string"
|
|
941
|
+
? { modeCheckedAt: parsed.modeCheckedAt }
|
|
942
|
+
: {}),
|
|
943
|
+
keys: isObject(parsed.keys)
|
|
944
|
+
? Object.fromEntries(Object.entries(parsed.keys).filter((entry) => typeof entry[1] === "string"))
|
|
945
|
+
: {},
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
// First guarded action for this session.
|
|
951
|
+
}
|
|
952
|
+
return { keys: {} };
|
|
953
|
+
}
|
|
954
|
+
async function writeGuardState(agent, sessionId, state, home) {
|
|
955
|
+
const entries = Object.entries(state.keys)
|
|
956
|
+
.sort((a, b) => (a[1] < b[1] ? 1 : -1))
|
|
957
|
+
.slice(0, GUARD_MAX_CACHED_KEYS);
|
|
958
|
+
try {
|
|
959
|
+
await atomicWriteJson(guardStatePath(agent, sessionId, home), {
|
|
960
|
+
...state,
|
|
961
|
+
keys: Object.fromEntries(entries),
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
// Cache misses only cost an extra network round trip.
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function guardCheckUrl(apiUrl) {
|
|
969
|
+
return `${apiUrl.replace(/\/+$/u, "")}/v1/guard/check`;
|
|
970
|
+
}
|
|
971
|
+
function guardResultFromResponse(value) {
|
|
972
|
+
if (!isObject(value) || typeof value.mode !== "string") {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
const items = Array.isArray(value.items)
|
|
976
|
+
? value.items.flatMap((item) => {
|
|
977
|
+
if (!isObject(item) || typeof item.content !== "string") {
|
|
978
|
+
return [];
|
|
979
|
+
}
|
|
980
|
+
const scope = isObject(item.scope) ? item.scope : {};
|
|
981
|
+
const source = isObject(item.source) ? item.source : {};
|
|
982
|
+
return [
|
|
983
|
+
{
|
|
984
|
+
content: redactSecrets(item.content),
|
|
985
|
+
enforcement: typeof item.enforcement === "string"
|
|
986
|
+
? item.enforcement
|
|
987
|
+
: "advisory",
|
|
988
|
+
...(typeof scope.repo === "string" ? { repo: scope.repo } : {}),
|
|
989
|
+
...(typeof scope.organization === "string"
|
|
990
|
+
? { organization: scope.organization }
|
|
991
|
+
: {}),
|
|
992
|
+
...(typeof source.agent === "string"
|
|
993
|
+
? { sourceAgent: source.agent }
|
|
994
|
+
: {}),
|
|
995
|
+
},
|
|
996
|
+
];
|
|
997
|
+
})
|
|
998
|
+
: [];
|
|
999
|
+
const conflictSummaries = Array.isArray(value.conflicts)
|
|
1000
|
+
? value.conflicts.flatMap((conflict) => isObject(conflict) && typeof conflict.summary === "string"
|
|
1001
|
+
? [redactSecrets(conflict.summary)]
|
|
1002
|
+
: [])
|
|
1003
|
+
: [];
|
|
1004
|
+
const confirm = isObject(value.confirm) ? value.confirm : {};
|
|
1005
|
+
return {
|
|
1006
|
+
...(typeof value.checkId === "string" ? { checkId: value.checkId } : {}),
|
|
1007
|
+
mode: value.mode,
|
|
1008
|
+
requiresConfirmation: value.requiresConfirmation === true,
|
|
1009
|
+
...(typeof confirm.command === "string"
|
|
1010
|
+
? { confirmCommand: confirm.command }
|
|
1011
|
+
: {}),
|
|
1012
|
+
items,
|
|
1013
|
+
conflictSummaries,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function guardItemSource(item) {
|
|
1017
|
+
const scope = item.repo !== undefined
|
|
1018
|
+
? `${item.repo} repo`
|
|
1019
|
+
: item.organization !== undefined
|
|
1020
|
+
? "organization rule"
|
|
1021
|
+
: "workspace knowledge";
|
|
1022
|
+
return item.sourceAgent === undefined
|
|
1023
|
+
? scope
|
|
1024
|
+
: `${scope} · ${item.sourceAgent} session`;
|
|
1025
|
+
}
|
|
1026
|
+
function guardAssistContext(result) {
|
|
1027
|
+
const lines = ["Relevant Lore context:"];
|
|
1028
|
+
for (const item of result.items) {
|
|
1029
|
+
lines.push(`- ${item.content}`);
|
|
1030
|
+
}
|
|
1031
|
+
lines.push("", "Sources:");
|
|
1032
|
+
for (const item of result.items) {
|
|
1033
|
+
lines.push(`- ${guardItemSource(item)}`);
|
|
1034
|
+
}
|
|
1035
|
+
if (result.conflictSummaries.length > 0) {
|
|
1036
|
+
lines.push("", "Conflicting Lore context (do not silently pick a winner):");
|
|
1037
|
+
for (const summary of result.conflictSummaries) {
|
|
1038
|
+
lines.push(`- ${summary}`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return boundedContext(lines.join("\n"));
|
|
1042
|
+
}
|
|
1043
|
+
function guardConfirmationReason(result) {
|
|
1044
|
+
const required = result.items.filter((item) => item.enforcement === "required");
|
|
1045
|
+
const shown = required.length > 0 ? required : result.items;
|
|
1046
|
+
const lines = [
|
|
1047
|
+
"Lore Guard: this action conflicts with a required rule.",
|
|
1048
|
+
...shown.map((item) => `- ${item.content} (${guardItemSource(item)})`),
|
|
1049
|
+
];
|
|
1050
|
+
lines.push(result.confirmCommand === undefined
|
|
1051
|
+
? "Ask the user to confirm before proceeding."
|
|
1052
|
+
: `Approve here to proceed, or the user can run: ${result.confirmCommand}`);
|
|
1053
|
+
return boundedContext(lines.join("\n"));
|
|
1054
|
+
}
|
|
1055
|
+
async function postGuardCheck(config, request, fetchImplementation) {
|
|
1056
|
+
const response = await fetchImplementation(guardCheckUrl(config.apiUrl), {
|
|
1057
|
+
method: "POST",
|
|
1058
|
+
headers: {
|
|
1059
|
+
authorization: `Bearer ${config.token}`,
|
|
1060
|
+
"content-type": "application/json",
|
|
1061
|
+
"user-agent": `lore-cli/${RUNTIME_VERSION}`,
|
|
1062
|
+
},
|
|
1063
|
+
body: JSON.stringify(request),
|
|
1064
|
+
signal: AbortSignal.timeout(Math.min(config.timeoutMs ?? GUARD_TIMEOUT_MS, GUARD_TIMEOUT_MS)),
|
|
1065
|
+
});
|
|
1066
|
+
if (!response.ok) {
|
|
1067
|
+
throw new Error(`Lore guard check failed with HTTP ${response.status}`);
|
|
1068
|
+
}
|
|
1069
|
+
return guardResultFromResponse(JSON.parse(await response.text()));
|
|
1070
|
+
}
|
|
1071
|
+
async function handleGuardEvent(input, agent, config, sessionId, options, now) {
|
|
1072
|
+
if (agent === "polytoken") {
|
|
1073
|
+
// Polytoken has no tool boundary; its guard runs at prompt injection.
|
|
1074
|
+
return undefined;
|
|
1075
|
+
}
|
|
1076
|
+
const toolName = stringField(input.tool_name);
|
|
1077
|
+
if (toolName === undefined) {
|
|
1078
|
+
return undefined;
|
|
1079
|
+
}
|
|
1080
|
+
const trigger = guardTrigger(toolName, isObject(input.tool_input) ? input.tool_input : {});
|
|
1081
|
+
if (trigger === null) {
|
|
1082
|
+
return undefined;
|
|
1083
|
+
}
|
|
1084
|
+
const state = await readGuardState(agent, sessionId, options.home);
|
|
1085
|
+
if (state.mode === "off" &&
|
|
1086
|
+
state.modeCheckedAt !== undefined &&
|
|
1087
|
+
now.getTime() - Date.parse(state.modeCheckedAt) < GUARD_MODE_TTL_MS) {
|
|
1088
|
+
return undefined;
|
|
1089
|
+
}
|
|
1090
|
+
const cwd = stringField(input.cwd);
|
|
1091
|
+
const scope = await repositoryScope(cwd);
|
|
1092
|
+
const root = cwd === undefined ? null : await repositoryRoot(cwd);
|
|
1093
|
+
const files = (trigger.files ?? [])
|
|
1094
|
+
.map((file) => {
|
|
1095
|
+
const absolute = resolve(cwd ?? ".", file);
|
|
1096
|
+
if (root === null) {
|
|
1097
|
+
return normalizedRepositoryPath(file);
|
|
1098
|
+
}
|
|
1099
|
+
const relativePath = relative(root, absolute);
|
|
1100
|
+
return relativePath.startsWith("..")
|
|
1101
|
+
? normalizedRepositoryPath(file)
|
|
1102
|
+
: normalizedRepositoryPath(relativePath);
|
|
1103
|
+
})
|
|
1104
|
+
.filter((file) => file !== "")
|
|
1105
|
+
.slice(0, 100);
|
|
1106
|
+
const key = sha256([
|
|
1107
|
+
"guard",
|
|
1108
|
+
trigger.action,
|
|
1109
|
+
...files,
|
|
1110
|
+
trigger.command === undefined ? "" : redactSecrets(trigger.command),
|
|
1111
|
+
].join("\0"));
|
|
1112
|
+
const cachedAt = state.keys[key];
|
|
1113
|
+
if (cachedAt !== undefined &&
|
|
1114
|
+
now.getTime() - Date.parse(cachedAt) < GUARD_KEY_COOLDOWN_MS) {
|
|
1115
|
+
return undefined;
|
|
1116
|
+
}
|
|
1117
|
+
const result = await postGuardCheck(config, {
|
|
1118
|
+
connector: "lore-cli",
|
|
1119
|
+
agent,
|
|
1120
|
+
sessionId,
|
|
1121
|
+
action: trigger.action,
|
|
1122
|
+
tool: toolName,
|
|
1123
|
+
...(scope?.repo === undefined ? {} : { repo: scope.repo }),
|
|
1124
|
+
...(scope?.path === undefined ? {} : { path: scope.path }),
|
|
1125
|
+
...(files.length === 0 ? {} : { files }),
|
|
1126
|
+
...(trigger.command === undefined
|
|
1127
|
+
? {}
|
|
1128
|
+
: { command: redactSecrets(trigger.command).slice(0, 10_000) }),
|
|
1129
|
+
}, options.fetch ?? globalThis.fetch);
|
|
1130
|
+
if (result === null) {
|
|
1131
|
+
return undefined;
|
|
1132
|
+
}
|
|
1133
|
+
state.mode = result.mode;
|
|
1134
|
+
state.modeCheckedAt = now.toISOString();
|
|
1135
|
+
if (!result.requiresConfirmation) {
|
|
1136
|
+
state.keys[key] = now.toISOString();
|
|
1137
|
+
}
|
|
1138
|
+
await writeGuardState(agent, sessionId, state, options.home);
|
|
1139
|
+
if (result.mode === "off") {
|
|
1140
|
+
return undefined;
|
|
1141
|
+
}
|
|
1142
|
+
if (result.requiresConfirmation) {
|
|
1143
|
+
const reason = guardConfirmationReason(result);
|
|
1144
|
+
if (agent === "cursor") {
|
|
1145
|
+
// Cursor's "ask" verdict is unenforced upstream; deny is the only
|
|
1146
|
+
// reliable gate. The message carries the approve-and-retry path.
|
|
1147
|
+
return {
|
|
1148
|
+
permission: "deny",
|
|
1149
|
+
userMessage: reason,
|
|
1150
|
+
agentMessage: reason,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
hookSpecificOutput: {
|
|
1155
|
+
hookEventName: "PreToolUse",
|
|
1156
|
+
permissionDecision: "ask",
|
|
1157
|
+
permissionDecisionReason: reason,
|
|
1158
|
+
},
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
if (result.items.length === 0 && result.conflictSummaries.length === 0) {
|
|
1162
|
+
return undefined;
|
|
1163
|
+
}
|
|
1164
|
+
if (agent === "cursor") {
|
|
1165
|
+
// Cursor cannot inject agent context at the shell boundary on allow.
|
|
1166
|
+
return undefined;
|
|
1167
|
+
}
|
|
1168
|
+
return {
|
|
1169
|
+
hookSpecificOutput: {
|
|
1170
|
+
hookEventName: "PreToolUse",
|
|
1171
|
+
additionalContext: guardAssistContext(result),
|
|
1172
|
+
},
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
866
1175
|
export async function handleHookEvent(value, agent, options = {}) {
|
|
867
1176
|
if (!isObject(value)) {
|
|
868
1177
|
return undefined;
|
|
@@ -889,6 +1198,15 @@ export async function handleHookEvent(value, agent, options = {}) {
|
|
|
889
1198
|
// invocation can still capture a correction.
|
|
890
1199
|
return undefined;
|
|
891
1200
|
}
|
|
1201
|
+
if (eventName === "PreToolUse") {
|
|
1202
|
+
try {
|
|
1203
|
+
return await handleGuardEvent(input, agent, config, sessionId, options, now);
|
|
1204
|
+
}
|
|
1205
|
+
catch {
|
|
1206
|
+
// Guard checks fail open: never stall or break a tool call.
|
|
1207
|
+
return undefined;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
892
1210
|
if (eventName !== "UserPromptSubmit") {
|
|
893
1211
|
return undefined;
|
|
894
1212
|
}
|