codiedev 0.9.51 → 0.10.0-beta.1
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 +9 -1
- package/dist/cli.js +6 -11
- package/dist/codieStartup.d.ts +2 -0
- package/dist/codieStartup.js +1 -0
- package/dist/connect.js +5 -2
- package/dist/cursorHooks.d.ts +23 -0
- package/dist/cursorHooks.js +81 -0
- package/dist/hook.js +127 -52
- package/dist/limitEvent.d.ts +8 -1
- package/dist/limitEvent.js +58 -0
- package/dist/limitEventReporter.d.ts +21 -0
- package/dist/limitEventReporter.js +91 -0
- package/dist/mcp.js +1 -93
- package/dist/repoResolver.js +1 -1
- package/dist/security.d.ts +6 -1
- package/dist/security.js +15 -0
- package/dist/securityInstall.d.ts +2 -1
- package/dist/securityInstall.js +23 -11
- package/dist/securityPilot.js +1 -1
- package/dist/securityPolicy.d.ts +2 -1
- package/dist/securityPolicy.js +9 -0
- package/dist/utils.d.ts +10 -2
- package/dist/utils.js +202 -66
- package/package.json +5 -2
- package/dist/commands/post.d.ts +0 -2
- package/dist/commands/post.js +0 -96
package/dist/limitEvent.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.classifyLimitKind = classifyLimitKind;
|
|
4
4
|
exports.parseLimitEventFromTranscript = parseLimitEventFromTranscript;
|
|
5
|
+
exports.parseCodexLimitEventFromTranscript = parseCodexLimitEventFromTranscript;
|
|
5
6
|
exports.tailForLimitEvent = tailForLimitEvent;
|
|
6
7
|
function classifyLimitKind(text) {
|
|
7
8
|
const t = text.toLowerCase();
|
|
@@ -50,6 +51,63 @@ function parseLimitEventFromTranscript(transcript) {
|
|
|
50
51
|
}
|
|
51
52
|
return found;
|
|
52
53
|
}
|
|
54
|
+
function codexLimitKind(reason, spendControlReached) {
|
|
55
|
+
if (spendControlReached || reason.startsWith("workspace_owner_"))
|
|
56
|
+
return "org_spend";
|
|
57
|
+
if (reason.startsWith("workspace_member_"))
|
|
58
|
+
return "user_spend";
|
|
59
|
+
return reason === "rate_limit_reached" ? "session" : "other";
|
|
60
|
+
}
|
|
61
|
+
function codexResetText(rateLimits) {
|
|
62
|
+
const windows = [rateLimits?.primary, rateLimits?.secondary]
|
|
63
|
+
.filter((window) => window && typeof window === "object");
|
|
64
|
+
const reached = windows.filter((window) => Number(window.used_percent) >= 100);
|
|
65
|
+
const candidates = reached.length > 0 ? reached : windows;
|
|
66
|
+
const resetAt = candidates
|
|
67
|
+
.map((window) => Number(window.resets_at))
|
|
68
|
+
.filter((value) => Number.isFinite(value) && value > 0)
|
|
69
|
+
.sort((a, b) => a - b)[0];
|
|
70
|
+
return resetAt ? new Date(resetAt * 1000).toISOString() : null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Codex writes a structured RateLimitSnapshot on token_count events. A normal
|
|
74
|
+
* turn has `rate_limit_reached_type: null`; a blocked turn names the exact
|
|
75
|
+
* boundary. Reading that field avoids locale-dependent terminal copy.
|
|
76
|
+
*/
|
|
77
|
+
function parseCodexLimitEventFromTranscript(transcript) {
|
|
78
|
+
let found = null;
|
|
79
|
+
for (const line of transcript.split("\n")) {
|
|
80
|
+
if (!line.includes('"rate_limit_reached_type"') && !line.includes('"spend_control_reached"'))
|
|
81
|
+
continue;
|
|
82
|
+
let entry;
|
|
83
|
+
try {
|
|
84
|
+
entry = JSON.parse(line);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (entry?.type !== "event_msg" || entry?.payload?.type !== "token_count")
|
|
90
|
+
continue;
|
|
91
|
+
const rateLimits = entry.payload.rate_limits;
|
|
92
|
+
if (!rateLimits || typeof rateLimits !== "object")
|
|
93
|
+
continue;
|
|
94
|
+
const reachedType = typeof rateLimits.rate_limit_reached_type === "string"
|
|
95
|
+
? rateLimits.rate_limit_reached_type
|
|
96
|
+
: null;
|
|
97
|
+
const spendControlReached = rateLimits.spend_control_reached === true;
|
|
98
|
+
if (!reachedType && !spendControlReached)
|
|
99
|
+
continue;
|
|
100
|
+
const reason = reachedType ?? "spend_control_reached";
|
|
101
|
+
const at = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
|
|
102
|
+
found = {
|
|
103
|
+
kind: codexLimitKind(reason, spendControlReached),
|
|
104
|
+
at: Number.isNaN(at) ? Date.now() : at,
|
|
105
|
+
resetText: codexResetText(rateLimits),
|
|
106
|
+
reason,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return found;
|
|
110
|
+
}
|
|
53
111
|
// Tail of the raw transcript to ship with the limit event so the server can
|
|
54
112
|
// summarize "what they were working on" immediately — without waiting for the
|
|
55
113
|
// SessionEnd capture (which can lag hours, or drop entirely on Windows).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type LimitEvent } from "./limitEvent";
|
|
2
|
+
export type LimitEventSource = "claude-code" | "codex";
|
|
3
|
+
export type LimitEventPayload = LimitEvent & {
|
|
4
|
+
source: LimitEventSource;
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
transcriptTail?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function postLimitEvent(config: {
|
|
9
|
+
backendUrl: string;
|
|
10
|
+
token: string;
|
|
11
|
+
}, payload: LimitEventPayload, timeoutMs?: number): Promise<void>;
|
|
12
|
+
export declare function reportTranscriptLimitEvent(args: {
|
|
13
|
+
config: {
|
|
14
|
+
backendUrl: string;
|
|
15
|
+
token: string;
|
|
16
|
+
};
|
|
17
|
+
source: LimitEventSource;
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
transcript: string;
|
|
20
|
+
emitMinimalOnMiss?: boolean;
|
|
21
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.postLimitEvent = postLimitEvent;
|
|
37
|
+
exports.reportTranscriptLimitEvent = reportTranscriptLimitEvent;
|
|
38
|
+
const http = __importStar(require("node:http"));
|
|
39
|
+
const https = __importStar(require("node:https"));
|
|
40
|
+
const version_1 = require("./version");
|
|
41
|
+
const limitEvent_1 = require("./limitEvent");
|
|
42
|
+
function postLimitEvent(config, payload, timeoutMs = 5_000) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const target = new URL("/api/cli/limitEvent", config.backendUrl);
|
|
45
|
+
const data = JSON.stringify(payload);
|
|
46
|
+
const transport = target.protocol === "https:" ? https : http;
|
|
47
|
+
const request = transport.request({
|
|
48
|
+
hostname: target.hostname,
|
|
49
|
+
port: target.port || (target.protocol === "https:" ? 443 : 80),
|
|
50
|
+
path: target.pathname + target.search,
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"Content-Length": Buffer.byteLength(data),
|
|
55
|
+
Authorization: `Bearer ${config.token}`,
|
|
56
|
+
"X-Codiedev-Cli-Version": version_1.CLI_VERSION,
|
|
57
|
+
},
|
|
58
|
+
}, (response) => {
|
|
59
|
+
let responseBody = "";
|
|
60
|
+
response.on("data", (chunk) => { responseBody += chunk; });
|
|
61
|
+
response.on("end", () => {
|
|
62
|
+
if (response.statusCode && response.statusCode >= 200 && response.statusCode < 300) {
|
|
63
|
+
resolve();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
reject(new Error(`HTTP ${response.statusCode}: ${responseBody}`));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
request.on("error", reject);
|
|
71
|
+
request.setTimeout(timeoutMs, () => request.destroy(new Error(`timeout after ${timeoutMs}ms`)));
|
|
72
|
+
request.write(data);
|
|
73
|
+
request.end();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function reportTranscriptLimitEvent(args) {
|
|
77
|
+
const parse = args.source === "codex"
|
|
78
|
+
? limitEvent_1.parseCodexLimitEventFromTranscript
|
|
79
|
+
: limitEvent_1.parseLimitEventFromTranscript;
|
|
80
|
+
const parsed = parse(args.transcript);
|
|
81
|
+
if (!parsed && !args.emitMinimalOnMiss)
|
|
82
|
+
return false;
|
|
83
|
+
const event = parsed ?? { kind: "other", at: Date.now(), resetText: null };
|
|
84
|
+
await postLimitEvent(args.config, {
|
|
85
|
+
source: args.source,
|
|
86
|
+
...event,
|
|
87
|
+
sessionId: args.sessionId,
|
|
88
|
+
transcriptTail: args.transcript ? (0, limitEvent_1.tailForLimitEvent)(args.transcript) : undefined,
|
|
89
|
+
});
|
|
90
|
+
return true;
|
|
91
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -260,68 +260,6 @@ const TOOLS = [
|
|
|
260
260
|
required: ["artifactId"],
|
|
261
261
|
},
|
|
262
262
|
},
|
|
263
|
-
{
|
|
264
|
-
name: "codiedev_post_to_feed",
|
|
265
|
-
description: "Publish an artifact to the team's break-room feed with an intent " +
|
|
266
|
-
"and optional @mentions. Use when the user asks to 'post this to " +
|
|
267
|
-
"the team', 'publish to the feed', 'share with the team', 'ask the " +
|
|
268
|
-
"team', or when they want broader visibility than a single ping. " +
|
|
269
|
-
"Pass the artifact's filename (e.g., 'spec-214.md') and pick an " +
|
|
270
|
-
"intent that matches the request: share = here's a skill/spec; " +
|
|
271
|
-
"request_review = please review; request_expertise = I'm stuck, " +
|
|
272
|
-
"who knows this; link_share = fyi; rfc = proposing a change. " +
|
|
273
|
-
"Optionally @mention teammates (by first name) so they get a " +
|
|
274
|
-
"notification. Returns the feed post id.",
|
|
275
|
-
inputSchema: {
|
|
276
|
-
type: "object",
|
|
277
|
-
properties: {
|
|
278
|
-
filename: {
|
|
279
|
-
type: "string",
|
|
280
|
-
description: "Artifact filename to attach (e.g., 'spec-214.md'). The post " +
|
|
281
|
-
"will render a preview card linking to it.",
|
|
282
|
-
},
|
|
283
|
-
title: {
|
|
284
|
-
type: "string",
|
|
285
|
-
description: "Headline of the post. Short, imperative — e.g., 'Check my " +
|
|
286
|
-
"OAuth middleware spec' or 'Anyone know how we handle X?'.",
|
|
287
|
-
},
|
|
288
|
-
body: {
|
|
289
|
-
type: "string",
|
|
290
|
-
description: "Body of the post in markdown. A short explanation of why the " +
|
|
291
|
-
"user is posting (the artifact carries the detail).",
|
|
292
|
-
},
|
|
293
|
-
intent: {
|
|
294
|
-
type: "string",
|
|
295
|
-
enum: [
|
|
296
|
-
"share",
|
|
297
|
-
"request_review",
|
|
298
|
-
"request_expertise",
|
|
299
|
-
"link_share",
|
|
300
|
-
"rfc",
|
|
301
|
-
],
|
|
302
|
-
description: "Why the user is posting. See tool description.",
|
|
303
|
-
},
|
|
304
|
-
mentions: {
|
|
305
|
-
type: "array",
|
|
306
|
-
items: { type: "string" },
|
|
307
|
-
description: "Teammate first names (or handles) to @mention. Each mention " +
|
|
308
|
-
"creates a notification for that teammate.",
|
|
309
|
-
},
|
|
310
|
-
format: {
|
|
311
|
-
type: "string",
|
|
312
|
-
enum: ["article", "quick", "workflow", "review"],
|
|
313
|
-
description: "Post format. Default 'article' if omitted. Use 'quick' for " +
|
|
314
|
-
"short one-liners.",
|
|
315
|
-
},
|
|
316
|
-
tags: {
|
|
317
|
-
type: "array",
|
|
318
|
-
items: { type: "string" },
|
|
319
|
-
description: "Optional tags, lowercase, hyphen-safe.",
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
required: ["title", "body"],
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
263
|
{
|
|
326
264
|
name: "codiedev_share_with",
|
|
327
265
|
description: "Grant a teammate persistent read (or edit) access to an artifact " +
|
|
@@ -374,7 +312,7 @@ const TOOLS = [
|
|
|
374
312
|
"to that post with X', 'mark as used', or 'upvote'. The 🛠 " +
|
|
375
313
|
"reaction is the signal for 'I used this' and bumps the linked " +
|
|
376
314
|
"artifact's reuse count. Pass the post id (get it from " +
|
|
377
|
-
"codiedev_get_library or a previous
|
|
315
|
+
"codiedev_get_library or a previous feed response).",
|
|
378
316
|
inputSchema: {
|
|
379
317
|
type: "object",
|
|
380
318
|
properties: {
|
|
@@ -602,8 +540,6 @@ async function dispatchTool(name, args, config) {
|
|
|
602
540
|
return await handleNote(args, config);
|
|
603
541
|
case "codiedev_promote":
|
|
604
542
|
return await handlePromote(args, config);
|
|
605
|
-
case "codiedev_post_to_feed":
|
|
606
|
-
return await handlePostToFeed(args, config);
|
|
607
543
|
case "codiedev_share_with":
|
|
608
544
|
return await handleShareWith(args, config);
|
|
609
545
|
case "codiedev_send_to":
|
|
@@ -784,34 +720,6 @@ async function handlePromote(args, config) {
|
|
|
784
720
|
],
|
|
785
721
|
};
|
|
786
722
|
}
|
|
787
|
-
async function handlePostToFeed(args, config) {
|
|
788
|
-
const filename = asStringOrUndefined(args.filename);
|
|
789
|
-
const title = asString(args.title);
|
|
790
|
-
const body = asString(args.body);
|
|
791
|
-
const intent = asStringOrUndefined(args.intent);
|
|
792
|
-
const format = asStringOrUndefined(args.format);
|
|
793
|
-
const mentionsRaw = Array.isArray(args.mentions) ? args.mentions : [];
|
|
794
|
-
const mentions = mentionsRaw.filter((m) => typeof m === "string");
|
|
795
|
-
const tagsRaw = Array.isArray(args.tags) ? args.tags : [];
|
|
796
|
-
const tags = tagsRaw.filter((t) => typeof t === "string");
|
|
797
|
-
if (!title)
|
|
798
|
-
throw new Error("title required");
|
|
799
|
-
if (!body)
|
|
800
|
-
throw new Error("body required");
|
|
801
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/post-to-feed", {
|
|
802
|
-
config,
|
|
803
|
-
body: { filename, title, body, intent, format, mentions, tags },
|
|
804
|
-
});
|
|
805
|
-
const pieces = [`✓ Posted to feed (id=${res.postId}).`];
|
|
806
|
-
if (res.linkedArtifact?.key)
|
|
807
|
-
pieces.push(` attached: ${res.linkedArtifact.key}`);
|
|
808
|
-
if (res.intent)
|
|
809
|
-
pieces.push(` intent: ${res.intent}`);
|
|
810
|
-
if (res.mentions?.length) {
|
|
811
|
-
pieces.push(` mentioned: ${res.mentions.map((m) => m.name).join(", ")}`);
|
|
812
|
-
}
|
|
813
|
-
return { content: [{ type: "text", text: pieces.join("\n") }] };
|
|
814
|
-
}
|
|
815
723
|
async function handleShareWith(args, config) {
|
|
816
724
|
const filename = asString(args.filename);
|
|
817
725
|
const to = asString(args.to);
|
package/dist/repoResolver.js
CHANGED
|
@@ -23,7 +23,7 @@ function parseRemoteUrl(url) {
|
|
|
23
23
|
// Harness checkout loses its repoId even though transcript capture can
|
|
24
24
|
// resolve the same raw remote correctly. Check this after URL forms so the
|
|
25
25
|
// colon in `https://` is never mistaken for an SCP separator.
|
|
26
|
-
const scp = s.match(/^(?:[^@\s]+@)?([^:/\s]+):(
|
|
26
|
+
const scp = s.match(/^(?:[^@\s]+@)?([^:/\s]+):([^/]+\/.+)$/);
|
|
27
27
|
if (scp) {
|
|
28
28
|
return { host: scp[1].toLowerCase(), ownerName: scp[2].toLowerCase() };
|
|
29
29
|
}
|
package/dist/security.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type SecurityHookPhase = "pre" | "post" | "failure";
|
|
2
|
-
export type AgentSurface = "claude-code" | "codex" | "codie-harness";
|
|
2
|
+
export type AgentSurface = "claude-code" | "codex" | "cursor" | "codie-harness";
|
|
3
|
+
export declare const SECURITY_RUNTIME_VERSION = 1;
|
|
3
4
|
export interface RawSecurityHookInput {
|
|
4
5
|
session_id?: string;
|
|
5
6
|
sessionId?: string;
|
|
@@ -36,6 +37,8 @@ export interface SecurityWorkflowEvent {
|
|
|
36
37
|
endpointId?: string;
|
|
37
38
|
endpointName?: string;
|
|
38
39
|
cliVersion?: string;
|
|
40
|
+
securityRuntimeVersion?: number;
|
|
41
|
+
securityPolicyVersion?: number;
|
|
39
42
|
}
|
|
40
43
|
export type LocalSecurityAction = "allow" | "alert" | "block";
|
|
41
44
|
export type SecurityCategory = "secret_in_prompt" | "secret_exfiltration" | "destructive_outside_workspace" | "sensitive_file_read" | "sensitive_file_write" | "protected_agent_config_write" | "unapproved_network_egress" | "untrusted_mcp_tool" | "unapproved_command" | "obfuscated_shell_command" | "package_install" | "sensitive_read_then_egress";
|
|
@@ -70,6 +73,8 @@ export declare function withEndpointIdentity(event: SecurityWorkflowEvent, ident
|
|
|
70
73
|
endpointId?: string;
|
|
71
74
|
endpointName?: string;
|
|
72
75
|
cliVersion?: string;
|
|
76
|
+
securityRuntimeVersion?: number;
|
|
77
|
+
securityPolicyVersion?: number;
|
|
73
78
|
}): SecurityWorkflowEvent;
|
|
74
79
|
/**
|
|
75
80
|
* Runs before a user prompt reaches the model. This is intentionally narrow:
|
package/dist/security.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SECURITY_RUNTIME_VERSION = void 0;
|
|
36
37
|
exports.withEndpointIdentity = withEndpointIdentity;
|
|
37
38
|
exports.evaluatePromptSecurity = evaluatePromptSecurity;
|
|
38
39
|
exports.evaluateToolSecurity = evaluateToolSecurity;
|
|
@@ -41,12 +42,26 @@ exports.buildSecurityWorkflowEvent = buildSecurityWorkflowEvent;
|
|
|
41
42
|
exports.buildSecurityPromptEvent = buildSecurityPromptEvent;
|
|
42
43
|
exports.buildBlockedSecurityPromptEvent = buildBlockedSecurityPromptEvent;
|
|
43
44
|
const crypto = __importStar(require("crypto"));
|
|
45
|
+
// Advances only when endpoint enforcement behavior changes. This is
|
|
46
|
+
// intentionally independent from the package version so security coverage can
|
|
47
|
+
// remain current across unrelated CLI releases.
|
|
48
|
+
exports.SECURITY_RUNTIME_VERSION = 1;
|
|
44
49
|
function withEndpointIdentity(event, identity) {
|
|
45
50
|
return {
|
|
46
51
|
...event,
|
|
47
52
|
...(identity.endpointId ? { endpointId: identity.endpointId.slice(0, 240) } : {}),
|
|
48
53
|
...(identity.endpointName ? { endpointName: identity.endpointName.slice(0, 240) } : {}),
|
|
49
54
|
...(identity.cliVersion ? { cliVersion: identity.cliVersion.slice(0, 80) } : {}),
|
|
55
|
+
...(typeof identity.securityRuntimeVersion === "number"
|
|
56
|
+
&& Number.isInteger(identity.securityRuntimeVersion)
|
|
57
|
+
&& identity.securityRuntimeVersion > 0
|
|
58
|
+
? { securityRuntimeVersion: identity.securityRuntimeVersion }
|
|
59
|
+
: {}),
|
|
60
|
+
...(typeof identity.securityPolicyVersion === "number"
|
|
61
|
+
&& Number.isInteger(identity.securityPolicyVersion)
|
|
62
|
+
&& identity.securityPolicyVersion > 0
|
|
63
|
+
? { securityPolicyVersion: identity.securityPolicyVersion }
|
|
64
|
+
: {}),
|
|
50
65
|
};
|
|
51
66
|
}
|
|
52
67
|
function scopeMatches(value, candidate) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SecuritySurface = "claude-code" | "codex" | "codie-harness";
|
|
1
|
+
export type SecuritySurface = "claude-code" | "codex" | "cursor" | "codie-harness";
|
|
2
2
|
export interface SecurityInstallTarget {
|
|
3
3
|
surface: SecuritySurface;
|
|
4
4
|
configPath: string;
|
|
@@ -58,5 +58,6 @@ export declare function inspectSecurityInstallation(paths?: {
|
|
|
58
58
|
claudeSettingsPath?: string;
|
|
59
59
|
codexHooksPath?: string;
|
|
60
60
|
codexConfigPath?: string;
|
|
61
|
+
cursorHooksPath?: string;
|
|
61
62
|
codiePluginPath?: string;
|
|
62
63
|
}): SecurityInstallInspection[];
|
package/dist/securityInstall.js
CHANGED
|
@@ -153,12 +153,16 @@ function restoreSecurityInstallSnapshot(manifest) {
|
|
|
153
153
|
}
|
|
154
154
|
return { restored, snapshotId: manifest.rollbackSnapshotId };
|
|
155
155
|
}
|
|
156
|
-
function hookCommands(parsed, eventName) {
|
|
156
|
+
function hookCommands(parsed, eventName, allowFlat) {
|
|
157
157
|
const entries = parsed?.hooks?.[eventName];
|
|
158
158
|
if (!Array.isArray(entries))
|
|
159
159
|
return [];
|
|
160
160
|
const commands = [];
|
|
161
161
|
for (const entry of entries) {
|
|
162
|
+
const flatCommand = entry.command;
|
|
163
|
+
if (allowFlat && typeof flatCommand === "string" && (0, detection_1.isCodiedevHookCommand)(flatCommand)) {
|
|
164
|
+
commands.push(flatCommand);
|
|
165
|
+
}
|
|
162
166
|
const nested = entry?.hooks;
|
|
163
167
|
if (!Array.isArray(nested))
|
|
164
168
|
continue;
|
|
@@ -176,17 +180,23 @@ function inspectJsonSurface(surface, filePath, codexConfigPath) {
|
|
|
176
180
|
}
|
|
177
181
|
try {
|
|
178
182
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
const expected = surface === "cursor"
|
|
184
|
+
? [
|
|
185
|
+
["beforeSubmitPrompt", "security-prompt-cursor"],
|
|
186
|
+
["preToolUse", "security-pre-cursor"],
|
|
187
|
+
["postToolUse", "security-post-cursor"],
|
|
188
|
+
["postToolUseFailure", "security-failure-cursor"],
|
|
189
|
+
]
|
|
190
|
+
: [
|
|
191
|
+
["UserPromptSubmit", surface === "codex" ? "discover-codex" : "discover"],
|
|
192
|
+
["PreToolUse", surface === "codex" ? "security-pre-codex" : "security-pre"],
|
|
193
|
+
["PostToolUse", surface === "codex" ? "security-post-codex" : "security-post"],
|
|
194
|
+
...(surface === "claude-code"
|
|
195
|
+
? [["PostToolUseFailure", "security-failure"]]
|
|
196
|
+
: []),
|
|
197
|
+
];
|
|
188
198
|
const missing = expected
|
|
189
|
-
.filter(([eventName, subcommand]) => !hookCommands(parsed, eventName).some((command) => command.trim().endsWith(subcommand)))
|
|
199
|
+
.filter(([eventName, subcommand]) => !hookCommands(parsed, eventName, surface === "cursor").some((command) => command.trim().endsWith(subcommand)))
|
|
190
200
|
.map(([eventName]) => eventName);
|
|
191
201
|
if (missing.length > 0) {
|
|
192
202
|
return { surface, status: "fail", detail: `missing security hooks: ${missing.join(", ")}` };
|
|
@@ -207,10 +217,12 @@ function inspectSecurityInstallation(paths = {}) {
|
|
|
207
217
|
const home = os.homedir();
|
|
208
218
|
const claude = paths.claudeSettingsPath ?? path.join(home, ".claude", "settings.json");
|
|
209
219
|
const codex = paths.codexHooksPath ?? path.join(home, ".codex", "hooks.json");
|
|
220
|
+
const cursor = paths.cursorHooksPath ?? path.join(home, ".cursor", "hooks.json");
|
|
210
221
|
const codie = paths.codiePluginPath ?? path.join(home, ".config", "opencode", "plugins", "codiedev.js");
|
|
211
222
|
const results = [
|
|
212
223
|
inspectJsonSurface("claude-code", claude),
|
|
213
224
|
inspectJsonSurface("codex", codex, paths.codexConfigPath),
|
|
225
|
+
inspectJsonSurface("cursor", cursor),
|
|
214
226
|
];
|
|
215
227
|
if (!fs.existsSync(codie)) {
|
|
216
228
|
results.push({ surface: "codie-harness", status: "not-installed", detail: `${codie} not found` });
|
package/dist/securityPilot.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runSyntheticSecurityPilot = runSyntheticSecurityPilot;
|
|
4
4
|
const security_1 = require("./security");
|
|
5
|
-
const SURFACES = ["claude-code", "codex", "codie-harness"];
|
|
5
|
+
const SURFACES = ["claude-code", "codex", "cursor", "codie-harness"];
|
|
6
6
|
function toolInput(surface, cwd, commandOrPath) {
|
|
7
7
|
const { toolName, ...toolInputValue } = commandOrPath;
|
|
8
8
|
const resolvedToolName = toolName ?? (commandOrPath.file_path ? "Read" : "Bash");
|
package/dist/securityPolicy.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentSurface, type EndpointSecurityPolicy } from "./security";
|
|
2
2
|
export type TrustedPolicyKeys = Record<string, string>;
|
|
3
3
|
/**
|
|
4
4
|
* Vendor policy roots are compiled into the endpoint binary. This prevents a
|
|
@@ -29,4 +29,5 @@ export declare function syncEndpointSecurityPolicy(config: {
|
|
|
29
29
|
backendUrl: string;
|
|
30
30
|
token: string;
|
|
31
31
|
companyId: string;
|
|
32
|
+
deviceId?: string;
|
|
32
33
|
}, source?: AgentSurface, timeoutMs?: number, stateDir?: string, trustedKeys?: TrustedPolicyKeys): Promise<EndpointSecurityPolicy>;
|
package/dist/securityPolicy.js
CHANGED
|
@@ -46,6 +46,8 @@ const http = __importStar(require("http"));
|
|
|
46
46
|
const https = __importStar(require("https"));
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
48
|
const configDir_1 = require("./configDir");
|
|
49
|
+
const security_1 = require("./security");
|
|
50
|
+
const version_1 = require("./version");
|
|
49
51
|
const POLICY_FILE = "security-policy.json";
|
|
50
52
|
function policyFile(companyId) {
|
|
51
53
|
if (!companyId)
|
|
@@ -150,6 +152,13 @@ async function syncEndpointSecurityPolicy(config, source, timeoutMs = 3_000, sta
|
|
|
150
152
|
const url = new URL("/api/cli/securityPolicy", config.backendUrl);
|
|
151
153
|
if (source)
|
|
152
154
|
url.searchParams.set("source", source);
|
|
155
|
+
const cachedPolicy = readEndpointSecurityPolicy(stateDir, trustedKeys, config.companyId);
|
|
156
|
+
url.searchParams.set("securityRuntimeVersion", String(security_1.SECURITY_RUNTIME_VERSION));
|
|
157
|
+
url.searchParams.set("cliVersion", version_1.CLI_VERSION);
|
|
158
|
+
if (cachedPolicy?.policyVersion)
|
|
159
|
+
url.searchParams.set("securityPolicyVersion", String(cachedPolicy.policyVersion));
|
|
160
|
+
if (config.deviceId)
|
|
161
|
+
url.searchParams.set("endpointId", config.deviceId);
|
|
153
162
|
const transport = url.protocol === "https:" ? https : http;
|
|
154
163
|
const policy = await new Promise((resolve, reject) => {
|
|
155
164
|
const request = transport.request(url, {
|
package/dist/utils.d.ts
CHANGED
|
@@ -104,7 +104,7 @@ export declare function installOpenCodeCapturePlugin(agentSecurityEnabled?: bool
|
|
|
104
104
|
* the presence of any `github.copilot*` directory as the Copilot fingerprint.
|
|
105
105
|
*/
|
|
106
106
|
export declare function vscodeCopilotInstalled(): boolean;
|
|
107
|
-
export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string, matcher?: string): void;
|
|
107
|
+
export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string, matcher?: string | null): void;
|
|
108
108
|
/** Remove Codiedev's entries for one event while preserving foreign hooks. */
|
|
109
109
|
export declare function removeCodiedevHook(hooks: Record<string, unknown>, eventName: string): boolean;
|
|
110
110
|
export declare function installHook(agentSecurityEnabled?: boolean): void;
|
|
@@ -170,7 +170,12 @@ export declare function cleanupCodexMcp(): void;
|
|
|
170
170
|
*/
|
|
171
171
|
export declare function installCodexMcp(): boolean;
|
|
172
172
|
export declare function installCodexHook(agentSecurityEnabled?: boolean): void;
|
|
173
|
-
export declare function
|
|
173
|
+
export declare function ensureCodexLimitHookInstalled(): void;
|
|
174
|
+
export declare function installCursorHook(agentSecurityEnabled?: boolean): void;
|
|
175
|
+
/** Repair Cursor's complete flat hook set after upgrades or configuration
|
|
176
|
+
* drift. This is deliberately separate from Claude Code's nested-hook healer:
|
|
177
|
+
* the schemas and event names differ, and mixing them risks corrupting both. */
|
|
178
|
+
export declare function ensureCursorCodiedevHooksInstalled(agentSecurityEnabled?: boolean): void;
|
|
174
179
|
export interface ParsedStats {
|
|
175
180
|
messageCount: number;
|
|
176
181
|
toolCallCount: number;
|
|
@@ -258,6 +263,8 @@ export declare const CAPTURE_THROTTLE_MS: number;
|
|
|
258
263
|
export declare const SWEEP_MIN_AGE_MS: number;
|
|
259
264
|
export declare function resolveCaptureFinal(argv2: string | undefined): boolean | undefined;
|
|
260
265
|
export declare function finishInputTmpFile(sessionId: string | undefined): string;
|
|
266
|
+
export declare function captureUploadTmpFile(sessionId: string | undefined, mode: "immediate" | "deferred", pid?: number, now?: number): string;
|
|
267
|
+
export declare function cancelDeferredCaptureUpload(sessionId: string, deps?: Pick<typeof fs, "unlinkSync">): boolean;
|
|
261
268
|
export declare function decideDispatch(args: {
|
|
262
269
|
final: boolean;
|
|
263
270
|
lastUploadAt: number | null;
|
|
@@ -332,3 +339,4 @@ interface DeferredUploadDeps extends UploadDeps {
|
|
|
332
339
|
}
|
|
333
340
|
export declare function runDeferredUpload(jobFile: string, delayMs: number, deps: DeferredUploadDeps): Promise<void>;
|
|
334
341
|
export declare function transcriptReadSkipReason(err: unknown): string;
|
|
342
|
+
export declare function shouldReportCaptureSkip(final: boolean | undefined): boolean;
|