codiedev 0.9.34 → 0.9.36
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/captureLog.d.ts +1 -1
- package/dist/cli.js +11 -1
- package/dist/codie.js +2 -1
- package/dist/codieLauncher.d.ts +7 -0
- package/dist/codieLauncher.js +14 -0
- package/dist/commands/doctor.js +5 -0
- package/dist/commands/security.d.ts +1 -0
- package/dist/commands/security.js +138 -0
- package/dist/connect.js +41 -0
- package/dist/hook.js +169 -2
- package/dist/security.d.ts +64 -1
- package/dist/security.js +287 -0
- package/dist/securityDeepScan.d.ts +41 -0
- package/dist/securityDeepScan.js +248 -0
- package/dist/securityDoctor.d.ts +32 -0
- package/dist/securityDoctor.js +63 -0
- package/dist/securityInstall.d.ts +61 -0
- package/dist/securityInstall.js +217 -0
- package/dist/securityInventory.d.ts +54 -0
- package/dist/securityInventory.js +546 -0
- package/dist/securityPilot.d.ts +19 -0
- package/dist/securityPilot.js +61 -0
- package/dist/securityPolicy.d.ts +23 -0
- package/dist/securityPolicy.js +161 -0
- package/dist/securitySession.d.ts +6 -0
- package/dist/securitySession.js +72 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +200 -2
- package/package.json +9 -2
- package/dist/scope.d.ts +0 -8
- package/dist/scope.js +0 -15
|
@@ -0,0 +1,161 @@
|
|
|
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.trustedPolicyKeysFromEnvironment = trustedPolicyKeysFromEnvironment;
|
|
37
|
+
exports.verifyEndpointSecurityPolicySignature = verifyEndpointSecurityPolicySignature;
|
|
38
|
+
exports.readEndpointSecurityPolicy = readEndpointSecurityPolicy;
|
|
39
|
+
exports.inspectEndpointSecurityPolicy = inspectEndpointSecurityPolicy;
|
|
40
|
+
exports.writeEndpointSecurityPolicy = writeEndpointSecurityPolicy;
|
|
41
|
+
exports.syncEndpointSecurityPolicy = syncEndpointSecurityPolicy;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const crypto = __importStar(require("crypto"));
|
|
44
|
+
const http = __importStar(require("http"));
|
|
45
|
+
const https = __importStar(require("https"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const configDir_1 = require("./configDir");
|
|
48
|
+
const POLICY_FILE = "security-policy.json";
|
|
49
|
+
function stableJson(value) {
|
|
50
|
+
if (Array.isArray(value))
|
|
51
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
52
|
+
if (value && typeof value === "object") {
|
|
53
|
+
return `{${Object.entries(value)
|
|
54
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
55
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`)
|
|
56
|
+
.join(",")}}`;
|
|
57
|
+
}
|
|
58
|
+
return JSON.stringify(value) ?? "null";
|
|
59
|
+
}
|
|
60
|
+
function trustedPolicyKeysFromEnvironment(env = process.env) {
|
|
61
|
+
const raw = env.CODIEDEV_SECURITY_POLICY_PUBLIC_KEYS;
|
|
62
|
+
if (!raw)
|
|
63
|
+
return {};
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
67
|
+
return {};
|
|
68
|
+
return Object.fromEntries(Object.entries(parsed)
|
|
69
|
+
.filter((entry) => typeof entry[1] === "string"));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function verifyEndpointSecurityPolicySignature(policy, trustedKeys = trustedPolicyKeysFromEnvironment()) {
|
|
76
|
+
const configuredKeys = Object.keys(trustedKeys);
|
|
77
|
+
if (configuredKeys.length === 0) {
|
|
78
|
+
return { ok: true, trusted: false, detail: "no endpoint policy public keys are pinned" };
|
|
79
|
+
}
|
|
80
|
+
const envelope = policy.signature;
|
|
81
|
+
if (!envelope || envelope.algorithm !== "Ed25519") {
|
|
82
|
+
return { ok: false, trusted: false, detail: "policy signature is missing" };
|
|
83
|
+
}
|
|
84
|
+
const publicKey = trustedKeys[envelope.keyId];
|
|
85
|
+
if (!publicKey)
|
|
86
|
+
return { ok: false, trusted: false, detail: `policy signing key ${envelope.keyId} is not trusted` };
|
|
87
|
+
const unsigned = { ...policy };
|
|
88
|
+
delete unsigned.signature;
|
|
89
|
+
try {
|
|
90
|
+
const ok = crypto.verify(null, Buffer.from(`${envelope.signedAt}.${stableJson(unsigned)}`), crypto.createPublicKey(publicKey), Buffer.from(envelope.value, "base64"));
|
|
91
|
+
return ok
|
|
92
|
+
? { ok: true, trusted: true, detail: `verified with ${envelope.keyId}` }
|
|
93
|
+
: { ok: false, trusted: false, detail: "policy signature does not match its content" };
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return { ok: false, trusted: false, detail: `policy signature could not be verified: ${error.message}` };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function readEndpointSecurityPolicy(stateDir = (0, configDir_1.codiedevStateDir)(), trustedKeys = trustedPolicyKeysFromEnvironment()) {
|
|
100
|
+
const inspection = inspectEndpointSecurityPolicy(stateDir, trustedKeys);
|
|
101
|
+
return inspection.verification.ok ? inspection.policy : undefined;
|
|
102
|
+
}
|
|
103
|
+
function inspectEndpointSecurityPolicy(stateDir = (0, configDir_1.codiedevStateDir)(), trustedKeys = trustedPolicyKeysFromEnvironment()) {
|
|
104
|
+
const target = path.join(stateDir, POLICY_FILE);
|
|
105
|
+
if (!fs.existsSync(target)) {
|
|
106
|
+
return { exists: false, verification: { ok: true, trusted: false, detail: "no cached endpoint policy" } };
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const value = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
110
|
+
if (value?.schemaVersion !== 1 || !["alert", "block"].includes(value.enforcementMode)) {
|
|
111
|
+
return { exists: true, verification: { ok: false, trusted: false, detail: "cached endpoint policy has an invalid schema" } };
|
|
112
|
+
}
|
|
113
|
+
const policy = value;
|
|
114
|
+
return { exists: true, policy, verification: verifyEndpointSecurityPolicySignature(policy, trustedKeys) };
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return { exists: true, verification: { ok: false, trusted: false, detail: `cached endpoint policy could not be read: ${error.message}` } };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function writeEndpointSecurityPolicy(policy, stateDir = (0, configDir_1.codiedevStateDir)(), trustedKeys = trustedPolicyKeysFromEnvironment()) {
|
|
121
|
+
const verification = verifyEndpointSecurityPolicySignature(policy, trustedKeys);
|
|
122
|
+
if (!verification.ok)
|
|
123
|
+
throw new Error(`Refusing untrusted endpoint policy: ${verification.detail}`);
|
|
124
|
+
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
125
|
+
const target = path.join(stateDir, POLICY_FILE);
|
|
126
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
127
|
+
fs.writeFileSync(temporary, `${JSON.stringify(policy, null, 2)}\n`, { mode: 0o600 });
|
|
128
|
+
fs.chmodSync(temporary, 0o600);
|
|
129
|
+
fs.renameSync(temporary, target);
|
|
130
|
+
}
|
|
131
|
+
async function syncEndpointSecurityPolicy(config, source, timeoutMs = 3_000) {
|
|
132
|
+
const url = new URL("/api/cli/securityPolicy", config.backendUrl);
|
|
133
|
+
if (source)
|
|
134
|
+
url.searchParams.set("source", source);
|
|
135
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
136
|
+
const policy = await new Promise((resolve, reject) => {
|
|
137
|
+
const request = transport.request(url, {
|
|
138
|
+
method: "GET",
|
|
139
|
+
headers: { Authorization: `Bearer ${config.token}`, Accept: "application/json" },
|
|
140
|
+
}, (response) => {
|
|
141
|
+
let raw = "";
|
|
142
|
+
response.setEncoding("utf8");
|
|
143
|
+
response.on("data", (chunk) => { raw += chunk; });
|
|
144
|
+
response.on("end", () => {
|
|
145
|
+
if ((response.statusCode ?? 500) >= 300)
|
|
146
|
+
return reject(new Error(`policy_http_${response.statusCode}`));
|
|
147
|
+
try {
|
|
148
|
+
resolve(JSON.parse(raw));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
reject(new Error("invalid policy response"));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
request.setTimeout(timeoutMs, () => request.destroy(new Error(`policy sync timeout after ${timeoutMs}ms`)));
|
|
156
|
+
request.on("error", reject);
|
|
157
|
+
request.end();
|
|
158
|
+
});
|
|
159
|
+
writeEndpointSecurityPolicy(policy);
|
|
160
|
+
return policy;
|
|
161
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface SessionSignal {
|
|
2
|
+
sensitiveReadAt?: number;
|
|
3
|
+
}
|
|
4
|
+
export declare function readSessionSecuritySignal(sessionId: string | undefined, stateDir?: string, now?: number): SessionSignal | undefined;
|
|
5
|
+
export declare function recordSensitiveReadSignal(sessionId: string | undefined, stateDir?: string, now?: number): void;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
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.readSessionSecuritySignal = readSessionSecuritySignal;
|
|
37
|
+
exports.recordSensitiveReadSignal = recordSensitiveReadSignal;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const configDir_1 = require("./configDir");
|
|
41
|
+
const SIGNAL_FILE = "security-session-signals.json";
|
|
42
|
+
const MAX_AGE_MS = 2 * 60 * 60 * 1_000;
|
|
43
|
+
function readSignals(stateDir, now) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(stateDir, SIGNAL_FILE), "utf8"));
|
|
46
|
+
if (parsed.schemaVersion !== 1 || !parsed.sessions || typeof parsed.sessions !== "object")
|
|
47
|
+
throw new Error("invalid signals");
|
|
48
|
+
parsed.sessions = Object.fromEntries(Object.entries(parsed.sessions)
|
|
49
|
+
.filter(([, signal]) => signal.sensitiveReadAt && now - signal.sensitiveReadAt <= MAX_AGE_MS)
|
|
50
|
+
.slice(-250));
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { schemaVersion: 1, sessions: {} };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function readSessionSecuritySignal(sessionId, stateDir = (0, configDir_1.codiedevStateDir)(), now = Date.now()) {
|
|
58
|
+
if (!sessionId)
|
|
59
|
+
return undefined;
|
|
60
|
+
return readSignals(stateDir, now).sessions[sessionId];
|
|
61
|
+
}
|
|
62
|
+
function recordSensitiveReadSignal(sessionId, stateDir = (0, configDir_1.codiedevStateDir)(), now = Date.now()) {
|
|
63
|
+
if (!sessionId)
|
|
64
|
+
return;
|
|
65
|
+
const signals = readSignals(stateDir, now);
|
|
66
|
+
signals.sessions[sessionId] = { sensitiveReadAt: now };
|
|
67
|
+
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
68
|
+
const target = path.join(stateDir, SIGNAL_FILE);
|
|
69
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
70
|
+
fs.writeFileSync(temporary, `${JSON.stringify(signals)}\n`, { mode: 0o600 });
|
|
71
|
+
fs.renameSync(temporary, target);
|
|
72
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -45,6 +45,13 @@ export declare function collectCaptureKeys(dirs: string[], getBranch?: (d: strin
|
|
|
45
45
|
branches: string[];
|
|
46
46
|
headShas: string[];
|
|
47
47
|
};
|
|
48
|
+
export type CaptureGitRef = {
|
|
49
|
+
repoUrl: string;
|
|
50
|
+
branch?: string;
|
|
51
|
+
headSha?: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function collectCaptureGitRefs(dirs: string[], getRemote?: (d: string) => string | null, getBranch?: (d: string) => string | null, getSha?: (d: string) => string | null): CaptureGitRef[];
|
|
54
|
+
export declare function parsePullRequestUrls(transcriptContent: string): string[];
|
|
48
55
|
export declare function extractShaTokens(transcriptContent: string): string[];
|
|
49
56
|
export declare function parseAuthoredBranches(transcriptContent: string): string[];
|
|
50
57
|
export declare function parseAuthoredShas(transcriptContent: string, repoRoot: string, resolveFullBatch?: (shas: string[]) => Map<string, string>): string[];
|
|
@@ -64,6 +71,8 @@ export interface CapturePayloadInput {
|
|
|
64
71
|
headSha?: string | null;
|
|
65
72
|
branches?: string[];
|
|
66
73
|
headShas?: string[];
|
|
74
|
+
gitRefs?: CaptureGitRef[];
|
|
75
|
+
pullRequestUrls?: string[];
|
|
67
76
|
inputTokens?: number;
|
|
68
77
|
outputTokens?: number;
|
|
69
78
|
cacheTokens?: number;
|
package/dist/utils.js
CHANGED
|
@@ -46,6 +46,8 @@ exports.matchRepo = matchRepo;
|
|
|
46
46
|
exports.resolveCaptureDir = resolveCaptureDir;
|
|
47
47
|
exports.resolveCaptureDirs = resolveCaptureDirs;
|
|
48
48
|
exports.collectCaptureKeys = collectCaptureKeys;
|
|
49
|
+
exports.collectCaptureGitRefs = collectCaptureGitRefs;
|
|
50
|
+
exports.parsePullRequestUrls = parsePullRequestUrls;
|
|
49
51
|
exports.extractShaTokens = extractShaTokens;
|
|
50
52
|
exports.parseAuthoredBranches = parseAuthoredBranches;
|
|
51
53
|
exports.parseAuthoredShas = parseAuthoredShas;
|
|
@@ -450,6 +452,144 @@ function collectCaptureKeys(dirs, getBranch = getGitBranch, getSha = getGitHeadS
|
|
|
450
452
|
}
|
|
451
453
|
return { branch: branches[0] ?? null, headSha: headShas[0] ?? null, branches, headShas };
|
|
452
454
|
}
|
|
455
|
+
function collectCaptureGitRefs(dirs, getRemote = getGitRemoteUrl, getBranch = getGitBranch, getSha = getGitHeadSha) {
|
|
456
|
+
const out = [];
|
|
457
|
+
const seen = new Set();
|
|
458
|
+
for (const dir of dirs) {
|
|
459
|
+
const repoUrl = getRemote(dir)?.trim();
|
|
460
|
+
if (!repoUrl)
|
|
461
|
+
continue;
|
|
462
|
+
const branch = getBranch(dir)?.trim() || undefined;
|
|
463
|
+
const headSha = getSha(dir)?.trim().toLowerCase() || undefined;
|
|
464
|
+
if (!branch && !headSha)
|
|
465
|
+
continue;
|
|
466
|
+
const key = `${normalizeRepoUrl(repoUrl).toLowerCase()}::${branch ?? ""}::${headSha ?? ""}`;
|
|
467
|
+
if (seen.has(key))
|
|
468
|
+
continue;
|
|
469
|
+
seen.add(key);
|
|
470
|
+
out.push({ repoUrl, ...(branch ? { branch } : {}), ...(headSha ? { headSha } : {}) });
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
// Exact PR/MR URLs are the strongest provenance the transcript can provide.
|
|
475
|
+
// Both `gh pr create` and `glab mr create` print one, and the OpenCode capture
|
|
476
|
+
// is normalized through the same parser as Claude Code. A direct URL survives
|
|
477
|
+
// branch deletion, squash/rebase, cherry-picks, and SessionEnd happening after
|
|
478
|
+
// the PR was opened.
|
|
479
|
+
function parsePullRequestUrls(transcriptContent) {
|
|
480
|
+
const out = [];
|
|
481
|
+
const createToolIds = new Set();
|
|
482
|
+
const isCreateCommand = (command) => {
|
|
483
|
+
if (typeof command !== "string")
|
|
484
|
+
return false;
|
|
485
|
+
// A combined `gh pr view || gh pr create` result is ambiguous: when the PR
|
|
486
|
+
// already exists, the VIEW half prints its URL and CREATE never runs.
|
|
487
|
+
if (/\b(?:gh\s+pr\s+(?:view|list|status)|glab\s+mr\s+(?:view|list))\b/i.test(command)) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
return command.split(/&&|\|\||;|\n/).some((statement) => /^(?:(?:env\s+)?[A-Z_][A-Z0-9_]*=\S+\s+)*(?:gh\s+pr\s+create|glab\s+mr\s+create)\b/i
|
|
491
|
+
.test(statement.trim()));
|
|
492
|
+
};
|
|
493
|
+
const scanText = (text) => {
|
|
494
|
+
const normalized = text.replace(/\\\//g, "/");
|
|
495
|
+
const candidates = normalized.match(/https?:\/\/[^\s<>"']+/gi) ?? [];
|
|
496
|
+
for (const raw of candidates) {
|
|
497
|
+
const trimmed = raw.replace(/[),.;:\]}]+$/g, "");
|
|
498
|
+
try {
|
|
499
|
+
const url = new URL(trimmed);
|
|
500
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
501
|
+
const isGithubPr = /^\/.+\/pull\/\d+$/i.test(path);
|
|
502
|
+
const isGitlabMr = /^\/.+\/-\/merge_requests\/\d+$/i.test(path);
|
|
503
|
+
if (!isGithubPr && !isGitlabMr)
|
|
504
|
+
continue;
|
|
505
|
+
const canonical = `${url.protocol}//${url.host}${path}`;
|
|
506
|
+
if (!out.includes(canonical))
|
|
507
|
+
out.push(canonical);
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// Malformed tool output is ignored; capture remains best-effort.
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const scanContent = (value) => {
|
|
515
|
+
if (typeof value === "string") {
|
|
516
|
+
scanText(value);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!Array.isArray(value))
|
|
520
|
+
return;
|
|
521
|
+
for (const part of value) {
|
|
522
|
+
if (typeof part === "string")
|
|
523
|
+
scanText(part);
|
|
524
|
+
else if (part && typeof part === "object") {
|
|
525
|
+
const text = part.text;
|
|
526
|
+
if (typeof text === "string")
|
|
527
|
+
scanText(text);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
const entries = [];
|
|
532
|
+
for (const line of transcriptContent.split("\n")) {
|
|
533
|
+
if (!line.trim())
|
|
534
|
+
continue;
|
|
535
|
+
try {
|
|
536
|
+
const entry = JSON.parse(line);
|
|
537
|
+
entries.push(entry);
|
|
538
|
+
const content = entry?.message?.content;
|
|
539
|
+
if (Array.isArray(content)) {
|
|
540
|
+
for (const block of content) {
|
|
541
|
+
if (block?.type === "tool_use"
|
|
542
|
+
&& typeof block.id === "string"
|
|
543
|
+
&& isCreateCommand(block.input?.command)) {
|
|
544
|
+
createToolIds.add(block.id);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Codex function calls carry arguments as serialized JSON.
|
|
549
|
+
if (entry?.type === "response_item"
|
|
550
|
+
&& entry?.payload?.type === "function_call"
|
|
551
|
+
&& typeof entry.payload.call_id === "string") {
|
|
552
|
+
let args = entry.payload.arguments;
|
|
553
|
+
if (typeof args === "string") {
|
|
554
|
+
try {
|
|
555
|
+
args = JSON.parse(args);
|
|
556
|
+
}
|
|
557
|
+
catch { /* leave as string */ }
|
|
558
|
+
}
|
|
559
|
+
if (isCreateCommand(args?.cmd ?? args?.command ?? args)) {
|
|
560
|
+
createToolIds.add(entry.payload.call_id);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// Never regex the raw line: prompts and assistant prose can contain a PR
|
|
566
|
+
// URL, but only actual tool output is provenance.
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Second pass: a URL is proof only when it came from the result of the
|
|
570
|
+
// specific gh/glab CREATE call above. `gh pr view/list` also prints URLs and
|
|
571
|
+
// must never claim somebody else's already-existing PR for this session.
|
|
572
|
+
for (const entry of entries) {
|
|
573
|
+
const content = entry?.message?.content;
|
|
574
|
+
if (Array.isArray(content)) {
|
|
575
|
+
for (const block of content) {
|
|
576
|
+
if (block?.type === "tool_result"
|
|
577
|
+
&& typeof block.tool_use_id === "string"
|
|
578
|
+
&& createToolIds.has(block.tool_use_id)
|
|
579
|
+
&& block.is_error !== true) {
|
|
580
|
+
scanContent(block.content);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (entry?.type === "response_item"
|
|
585
|
+
&& entry?.payload?.type === "function_call_output"
|
|
586
|
+
&& typeof entry.payload.call_id === "string"
|
|
587
|
+
&& createToolIds.has(entry.payload.call_id)) {
|
|
588
|
+
scanContent(entry.payload.output);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
453
593
|
// Pull the commit SHAs the agent actually authored/pushed straight out of the
|
|
454
594
|
// transcript's git command OUTPUT — independent of end-of-session filesystem
|
|
455
595
|
// state. Catches: commit output "[branch 4c4e790]", push ranges
|
|
@@ -690,6 +830,10 @@ function buildCapturePayload(input) {
|
|
|
690
830
|
// see no change; newer servers index the session under every key.
|
|
691
831
|
branches: input.branches && input.branches.length ? input.branches : undefined,
|
|
692
832
|
headShas: input.headShas && input.headShas.length ? input.headShas : undefined,
|
|
833
|
+
gitRefs: input.gitRefs && input.gitRefs.length ? input.gitRefs : undefined,
|
|
834
|
+
pullRequestUrls: input.pullRequestUrls && input.pullRequestUrls.length
|
|
835
|
+
? input.pullRequestUrls
|
|
836
|
+
: undefined,
|
|
693
837
|
transcript: input.transcriptBase64,
|
|
694
838
|
messageCount: input.messageCount,
|
|
695
839
|
toolCallCount: input.toolCallCount,
|
|
@@ -910,6 +1054,11 @@ function installOpenCodeCapturePlugin() {
|
|
|
910
1054
|
const start = resolveHookInvocation("session-start-opencode");
|
|
911
1055
|
const capture = resolveHookInvocation("capture-opencode");
|
|
912
1056
|
const finish = resolveHookInvocation("capture-opencode-final");
|
|
1057
|
+
const backup = resolveHookInvocation("backup");
|
|
1058
|
+
const prompt = resolveHookInvocation("discover-codie");
|
|
1059
|
+
const securityPre = resolveHookInvocation("security-pre-codie");
|
|
1060
|
+
const securityPost = resolveHookInvocation("security-post-codie");
|
|
1061
|
+
const securityFailure = resolveHookInvocation("security-failure-codie");
|
|
913
1062
|
const source = `// Managed by codiedev connect. Safe to remove and reinstall.\n` +
|
|
914
1063
|
`import { spawn, spawnSync } from "node:child_process";\n` +
|
|
915
1064
|
`import { mkdir, writeFile } from "node:fs/promises";\n` +
|
|
@@ -918,6 +1067,11 @@ function installOpenCodeCapturePlugin() {
|
|
|
918
1067
|
`const START = ${JSON.stringify(start)};\n` +
|
|
919
1068
|
`const CAPTURE = ${JSON.stringify(capture)};\n` +
|
|
920
1069
|
`const FINISH = ${JSON.stringify(finish)};\n` +
|
|
1070
|
+
`const BACKUP = ${JSON.stringify(backup)};\n` +
|
|
1071
|
+
`const PROMPT = ${JSON.stringify(prompt)};\n` +
|
|
1072
|
+
`const SECURITY_PRE = ${JSON.stringify(securityPre)};\n` +
|
|
1073
|
+
`const SECURITY_POST = ${JSON.stringify(securityPost)};\n` +
|
|
1074
|
+
`const SECURITY_FAILURE = ${JSON.stringify(securityFailure)};\n` +
|
|
921
1075
|
`const fingerprints = new Map();\n\n` +
|
|
922
1076
|
`const transcriptPaths = new Map();\n\n` +
|
|
923
1077
|
`function sessionId(event) {\n` +
|
|
@@ -933,6 +1087,18 @@ function installOpenCodeCapturePlugin() {
|
|
|
933
1087
|
`function dispatchSync(invocation, payload) {\n` +
|
|
934
1088
|
` try { spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), stdio: ["pipe", "ignore", "ignore"], timeout: 1500 }); } catch {}\n` +
|
|
935
1089
|
`}\n\n` +
|
|
1090
|
+
`function enforceSync(invocation, payload) {\n` +
|
|
1091
|
+
` const result = spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500 });\n` +
|
|
1092
|
+
` if (result.error) return;\n` +
|
|
1093
|
+
` try {\n` +
|
|
1094
|
+
` const decision = JSON.parse(result.stdout || "{}");\n` +
|
|
1095
|
+
` const denied = decision?.decision === "block" || decision?.hookSpecificOutput?.permissionDecision === "deny";\n` +
|
|
1096
|
+
` if (denied) throw new Error(decision?.reason || decision?.hookSpecificOutput?.permissionDecisionReason || "Codie security policy blocked this action");\n` +
|
|
1097
|
+
` } catch (error) {\n` +
|
|
1098
|
+
` if (error instanceof SyntaxError) return;\n` +
|
|
1099
|
+
` throw error;\n` +
|
|
1100
|
+
` }\n` +
|
|
1101
|
+
`}\n\n` +
|
|
936
1102
|
`export const CodiedevCapture = async ({ client, directory, worktree }) => {\n` +
|
|
937
1103
|
` const finalizeAll = () => {\n` +
|
|
938
1104
|
` for (const [id, transcriptPath] of transcriptPaths) {\n` +
|
|
@@ -942,6 +1108,21 @@ function installOpenCodeCapturePlugin() {
|
|
|
942
1108
|
` };\n` +
|
|
943
1109
|
` process.once("exit", finalizeAll);\n` +
|
|
944
1110
|
` return ({\n` +
|
|
1111
|
+
` "chat.message": async (input, output) => {\n` +
|
|
1112
|
+
` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
|
|
1113
|
+
` const cwd = worktree || directory || process.cwd();\n` +
|
|
1114
|
+
` const prompt = Array.isArray(output?.parts) ? output.parts.filter((part) => part?.type === "text").map((part) => part.text || "").join("\\n") : "";\n` +
|
|
1115
|
+
` enforceSync(PROMPT, { sessionId: id, cwd, prompt });\n` +
|
|
1116
|
+
` },\n` +
|
|
1117
|
+
` "tool.execute.before": async (input, output) => {\n` +
|
|
1118
|
+
` const cwd = worktree || directory || process.cwd();\n` +
|
|
1119
|
+
` enforceSync(SECURITY_PRE, { sessionId: input?.sessionID ?? input?.sessionId ?? "unknown", cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: output?.args ?? output });\n` +
|
|
1120
|
+
` },\n` +
|
|
1121
|
+
` "tool.execute.after": async (input, output) => {\n` +
|
|
1122
|
+
` const cwd = worktree || directory || process.cwd();\n` +
|
|
1123
|
+
` const payload = { sessionId: input?.sessionID ?? input?.sessionId ?? "unknown", cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: input?.args, toolResponse: output };\n` +
|
|
1124
|
+
` dispatch(output?.error ? SECURITY_FAILURE : SECURITY_POST, payload);\n` +
|
|
1125
|
+
` },\n` +
|
|
945
1126
|
` event: async ({ event }) => {\n` +
|
|
946
1127
|
` const id = sessionId(event);\n` +
|
|
947
1128
|
` if (!id) return;\n` +
|
|
@@ -973,6 +1154,7 @@ function installOpenCodeCapturePlugin() {
|
|
|
973
1154
|
` await writeFile(transcriptPath, JSON.stringify(list), { mode: 0o600 });\n` +
|
|
974
1155
|
` transcriptPaths.set(id, transcriptPath);\n` +
|
|
975
1156
|
` dispatch(CAPTURE, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
|
|
1157
|
+
` dispatch(BACKUP, { session_id: id, cwd, workspace_roots: [cwd] });\n` +
|
|
976
1158
|
` } catch {}\n` +
|
|
977
1159
|
` },\n` +
|
|
978
1160
|
` });\n` +
|
|
@@ -1052,6 +1234,11 @@ function installHook() {
|
|
|
1052
1234
|
const hooks = settings.hooks;
|
|
1053
1235
|
// SessionEnd → capture (upload the finished session for the team library).
|
|
1054
1236
|
upsertCodiedevHook(hooks, "SessionEnd", "capture");
|
|
1237
|
+
// Tool boundaries → ADR shadow detection. PreToolUse is the eventual
|
|
1238
|
+
// enforcement point; alert mode always returns allow.
|
|
1239
|
+
upsertCodiedevHook(hooks, "PreToolUse", "security-pre");
|
|
1240
|
+
upsertCodiedevHook(hooks, "PostToolUse", "security-post");
|
|
1241
|
+
upsertCodiedevHook(hooks, "PostToolUseFailure", "security-failure");
|
|
1055
1242
|
// UserPromptSubmit → discover (surface a matching team skill for the task).
|
|
1056
1243
|
upsertCodiedevHook(hooks, "UserPromptSubmit", "discover");
|
|
1057
1244
|
// Stop → backup (auto-back-up changed artifact files to your private library).
|
|
@@ -1161,6 +1348,9 @@ function ensureAllCodiedevHooksInstalled() {
|
|
|
1161
1348
|
const hooks = (settings.hooks ?? {});
|
|
1162
1349
|
const wanted = [
|
|
1163
1350
|
["SessionEnd", "capture"],
|
|
1351
|
+
["PreToolUse", "security-pre"],
|
|
1352
|
+
["PostToolUse", "security-post"],
|
|
1353
|
+
["PostToolUseFailure", "security-failure"],
|
|
1164
1354
|
["UserPromptSubmit", "discover"],
|
|
1165
1355
|
["Stop", "backup"],
|
|
1166
1356
|
["StopFailure", "limit", "rate_limit"],
|
|
@@ -1623,9 +1813,8 @@ function installCodexHook() {
|
|
|
1623
1813
|
hooksFile.hooks = {};
|
|
1624
1814
|
}
|
|
1625
1815
|
const hooks = hooksFile.hooks;
|
|
1626
|
-
if (!hooks.Stop)
|
|
1816
|
+
if (!hooks.Stop)
|
|
1627
1817
|
hooks.Stop = [];
|
|
1628
|
-
}
|
|
1629
1818
|
const existing = hooks.Stop;
|
|
1630
1819
|
const filtered = existing.filter((hook) => {
|
|
1631
1820
|
const inner = hook.hooks;
|
|
@@ -1653,6 +1842,15 @@ function installCodexHook() {
|
|
|
1653
1842
|
],
|
|
1654
1843
|
});
|
|
1655
1844
|
hooks.Stop = filtered;
|
|
1845
|
+
// Codex uses the same Claude-style matcher-group shape. Keep these as
|
|
1846
|
+
// separate entries because Codex hook coverage is still evolving and a
|
|
1847
|
+
// malformed/unavailable post hook must not interfere with PreToolUse.
|
|
1848
|
+
upsertCodiedevHook(hooks, "PreToolUse", "security-pre-codex");
|
|
1849
|
+
upsertCodiedevHook(hooks, "PostToolUse", "security-post-codex");
|
|
1850
|
+
upsertCodiedevHook(hooks, "PostToolUseFailure", "security-failure-codex");
|
|
1851
|
+
// Prompt guard + skill discovery. The guard runs locally before any prompt
|
|
1852
|
+
// content can reach either the model or Codiedev's discovery endpoint.
|
|
1853
|
+
upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex");
|
|
1656
1854
|
if (!fs.existsSync(CODEX_DIR)) {
|
|
1657
1855
|
fs.mkdirSync(CODEX_DIR, { recursive: true });
|
|
1658
1856
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.36",
|
|
4
4
|
"description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codiedev": "dist/cli.js",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dev": "tsc --watch",
|
|
13
13
|
"prepublishOnly": "tsc",
|
|
14
14
|
"test": "vitest run",
|
|
15
|
+
"test:security-pilot": "vitest run src/__tests__/security.test.ts src/__tests__/securityInstall.test.ts src/__tests__/securityPilot.test.ts src/__tests__/codieSecurityAdapterWiring.test.ts && tsc && node dist/cli.js security pilot --json",
|
|
15
16
|
"test:watch": "vitest"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
@@ -35,12 +36,18 @@
|
|
|
35
36
|
"session-capture"
|
|
36
37
|
],
|
|
37
38
|
"homepage": "https://codiedev.com",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/Signal-and-Code/codiedev.git",
|
|
42
|
+
"directory": "packages/codiedev-cli"
|
|
43
|
+
},
|
|
38
44
|
"license": "MIT",
|
|
39
45
|
"engines": {
|
|
40
46
|
"node": ">=18"
|
|
41
47
|
},
|
|
42
48
|
"publishConfig": {
|
|
43
|
-
"access": "public"
|
|
49
|
+
"access": "public",
|
|
50
|
+
"provenance": true
|
|
44
51
|
},
|
|
45
52
|
"devDependencies": {
|
|
46
53
|
"@types/node": "^20.0.0",
|
package/dist/scope.d.ts
DELETED
package/dist/scope.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.formatScope = formatScope;
|
|
4
|
-
exports.printScope = printScope;
|
|
5
|
-
function formatScope(info) {
|
|
6
|
-
if (!info.workspaceName)
|
|
7
|
-
return "→ unknown workspace";
|
|
8
|
-
if (info.matchedRepo) {
|
|
9
|
-
return `→ ${info.workspaceName} (matched ${info.matchedRepo.fullName})`;
|
|
10
|
-
}
|
|
11
|
-
return `→ ${info.workspaceName} (default profile, no repo match)`;
|
|
12
|
-
}
|
|
13
|
-
function printScope(info) {
|
|
14
|
-
console.log(formatScope(info));
|
|
15
|
-
}
|