@glbgpt/cli 0.1.4 → 0.2.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 +82 -40
- package/dist/src/coding/backgroundTasks.d.ts +34 -0
- package/dist/src/coding/backgroundTasks.js +154 -0
- package/dist/src/coding/backgroundTasks.js.map +1 -0
- package/dist/src/coding/backgroundTools.d.ts +9 -0
- package/dist/src/coding/backgroundTools.js +63 -0
- package/dist/src/coding/backgroundTools.js.map +1 -0
- package/dist/src/coding/bashTool.d.ts +27 -0
- package/dist/src/coding/bashTool.js +144 -0
- package/dist/src/coding/bashTool.js.map +1 -0
- package/dist/src/coding/dangerousCommands.d.ts +29 -0
- package/dist/src/coding/dangerousCommands.js +156 -0
- package/dist/src/coding/dangerousCommands.js.map +1 -0
- package/dist/src/coding/editDiff.d.ts +45 -0
- package/dist/src/coding/editDiff.js +156 -0
- package/dist/src/coding/editDiff.js.map +1 -0
- package/dist/src/coding/editTool.d.ts +14 -0
- package/dist/src/coding/editTool.js +154 -0
- package/dist/src/coding/editTool.js.map +1 -0
- package/dist/src/coding/index.d.ts +25 -0
- package/dist/src/coding/index.js +30 -0
- package/dist/src/coding/index.js.map +1 -0
- package/dist/src/coding/outputAccumulator.d.ts +34 -0
- package/dist/src/coding/outputAccumulator.js +111 -0
- package/dist/src/coding/outputAccumulator.js.map +1 -0
- package/dist/src/coding/permissionGate.d.ts +65 -0
- package/dist/src/coding/permissionGate.js +138 -0
- package/dist/src/coding/permissionGate.js.map +1 -0
- package/dist/src/coding/permissionStore.d.ts +36 -0
- package/dist/src/coding/permissionStore.js +56 -0
- package/dist/src/coding/permissionStore.js.map +1 -0
- package/dist/src/coding/permissions.d.ts +34 -0
- package/dist/src/coding/permissions.js +18 -0
- package/dist/src/coding/permissions.js.map +1 -0
- package/dist/src/coding/projectContext.d.ts +21 -0
- package/dist/src/coding/projectContext.js +118 -0
- package/dist/src/coding/projectContext.js.map +1 -0
- package/dist/src/coding/readState.d.ts +22 -0
- package/dist/src/coding/readState.js +30 -0
- package/dist/src/coding/readState.js.map +1 -0
- package/dist/src/coding/truncate.d.ts +39 -0
- package/dist/src/coding/truncate.js +83 -0
- package/dist/src/coding/truncate.js.map +1 -0
- package/dist/src/coding/writeTool.d.ts +14 -0
- package/dist/src/coding/writeTool.js +82 -0
- package/dist/src/coding/writeTool.js.map +1 -0
- package/dist/src/commands/execCommand.d.ts +13 -0
- package/dist/src/commands/execCommand.js +73 -6
- package/dist/src/commands/execCommand.js.map +1 -1
- package/dist/src/commands/updateCommand.d.ts +7 -0
- package/dist/src/commands/updateCommand.js +11 -0
- package/dist/src/commands/updateCommand.js.map +1 -0
- package/dist/src/core/catalog.d.ts +5 -0
- package/dist/src/core/catalog.js +31 -1
- package/dist/src/core/catalog.js.map +1 -1
- package/dist/src/core/compaction.d.ts +52 -0
- package/dist/src/core/compaction.js +151 -0
- package/dist/src/core/compaction.js.map +1 -0
- package/dist/src/core/contextPrune.js +8 -1
- package/dist/src/core/contextPrune.js.map +1 -1
- package/dist/src/core/runChat.d.ts +1 -1
- package/dist/src/core/runChat.js +1 -1
- package/dist/src/core/runChat.js.map +1 -1
- package/dist/src/core/session.d.ts +10 -1
- package/dist/src/core/session.js +11 -2
- package/dist/src/core/session.js.map +1 -1
- package/dist/src/core/turnLoop.d.ts +18 -2
- package/dist/src/core/turnLoop.js +79 -12
- package/dist/src/core/turnLoop.js.map +1 -1
- package/dist/src/core/updateCheck.d.ts +35 -0
- package/dist/src/core/updateCheck.js +71 -2
- package/dist/src/core/updateCheck.js.map +1 -1
- package/dist/src/core/updateRunner.d.ts +7 -0
- package/dist/src/core/updateRunner.js +26 -0
- package/dist/src/core/updateRunner.js.map +1 -0
- package/dist/src/exec/output.d.ts +3 -2
- package/dist/src/exec/output.js +17 -4
- package/dist/src/exec/output.js.map +1 -1
- package/dist/src/fs/index.d.ts +4 -1
- package/dist/src/fs/index.js +7 -2
- package/dist/src/fs/index.js.map +1 -1
- package/dist/src/fs/limits.d.ts +1 -1
- package/dist/src/fs/limits.js +3 -1
- package/dist/src/fs/limits.js.map +1 -1
- package/dist/src/fs/readFileTool.d.ts +2 -1
- package/dist/src/fs/readFileTool.js +4 -1
- package/dist/src/fs/readFileTool.js.map +1 -1
- package/dist/src/fs/workspaceGuard.d.ts +8 -0
- package/dist/src/fs/workspaceGuard.js +48 -0
- package/dist/src/fs/workspaceGuard.js.map +1 -1
- package/dist/src/index.js +44 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/server.js +17 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/tui/App.d.ts +10 -1
- package/dist/src/tui/App.js +114 -15
- package/dist/src/tui/App.js.map +1 -1
- package/dist/src/tui/UpdatePrompt.d.ts +17 -0
- package/dist/src/tui/UpdatePrompt.js +42 -0
- package/dist/src/tui/UpdatePrompt.js.map +1 -0
- package/dist/src/tui/approvalBridge.d.ts +21 -0
- package/dist/src/tui/approvalBridge.js +45 -0
- package/dist/src/tui/approvalBridge.js.map +1 -0
- package/dist/src/tui/chatController.d.ts +21 -0
- package/dist/src/tui/chatController.js +155 -16
- package/dist/src/tui/chatController.js.map +1 -1
- package/dist/src/tui/commands.js +16 -2
- package/dist/src/tui/commands.js.map +1 -1
- package/dist/src/tui/models.d.ts +4 -0
- package/dist/src/tui/models.js +8 -1
- package/dist/src/tui/models.js.map +1 -1
- package/dist/src/tui/run.d.ts +7 -0
- package/dist/src/tui/run.js +107 -22
- package/dist/src/tui/run.js.map +1 -1
- package/dist/src/web/webFetchTool.js +41 -5
- package/dist/src/web/webFetchTool.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/coding/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAKhF,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EACL,oBAAoB,GAKrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAA4D,MAAM,kBAAkB,CAAC;AAC1G,OAAO,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D,sFAAsF;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC;IAC5D,MAAM;IACN,YAAY;IACZ,WAAW;IACX,aAAa;IACb,WAAW;CACZ,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAQjC;IACC,OAAO;QACL,cAAc,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACzG,mBAAmB,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;QACpF,kBAAkB,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;QACnF,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KACzG,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TailTruncation } from './truncate.js';
|
|
2
|
+
/**
|
|
3
|
+
* Bounded-memory accumulator for streaming command output (pi's design):
|
|
4
|
+
* memory only ever holds a rolling tail of ~2x the display byte cap; the
|
|
5
|
+
* moment the stream exceeds the display limits, ALL raw bytes start flowing
|
|
6
|
+
* to a spill file so the model can read the full output afterwards. A command
|
|
7
|
+
* that dumps gigabytes therefore costs disk, not RSS.
|
|
8
|
+
*/
|
|
9
|
+
export declare class OutputAccumulator {
|
|
10
|
+
private readonly maxLines;
|
|
11
|
+
private readonly maxBytes;
|
|
12
|
+
private readonly spillDir;
|
|
13
|
+
private readonly chunks;
|
|
14
|
+
private tailBytes;
|
|
15
|
+
private totalBytes;
|
|
16
|
+
private totalNewlines;
|
|
17
|
+
private spill;
|
|
18
|
+
private spillPath;
|
|
19
|
+
private spillError;
|
|
20
|
+
constructor(maxLines?: number, maxBytes?: number, spillDir?: string);
|
|
21
|
+
append(chunk: Buffer): void;
|
|
22
|
+
private overDisplayLimit;
|
|
23
|
+
private openSpill;
|
|
24
|
+
/**
|
|
25
|
+
* Close the spill (if any) and produce the model-facing text: the truncated
|
|
26
|
+
* tail plus a footnote naming the full-output path. The decoded tail drops
|
|
27
|
+
* its first (possibly chunk-severed) partial line when rolling occurred.
|
|
28
|
+
*/
|
|
29
|
+
finalize(): Promise<{
|
|
30
|
+
text: string;
|
|
31
|
+
truncation: TailTruncation;
|
|
32
|
+
fullOutputPath: string | null;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_MAX_OUTPUT_LINES, truncateTail, truncationFootnote, } from './truncate.js';
|
|
6
|
+
/**
|
|
7
|
+
* Bounded-memory accumulator for streaming command output (pi's design):
|
|
8
|
+
* memory only ever holds a rolling tail of ~2x the display byte cap; the
|
|
9
|
+
* moment the stream exceeds the display limits, ALL raw bytes start flowing
|
|
10
|
+
* to a spill file so the model can read the full output afterwards. A command
|
|
11
|
+
* that dumps gigabytes therefore costs disk, not RSS.
|
|
12
|
+
*/
|
|
13
|
+
export class OutputAccumulator {
|
|
14
|
+
maxLines;
|
|
15
|
+
maxBytes;
|
|
16
|
+
spillDir;
|
|
17
|
+
chunks = [];
|
|
18
|
+
tailBytes = 0;
|
|
19
|
+
totalBytes = 0;
|
|
20
|
+
totalNewlines = 0;
|
|
21
|
+
spill = null;
|
|
22
|
+
spillPath = null;
|
|
23
|
+
spillError = null;
|
|
24
|
+
constructor(maxLines = DEFAULT_MAX_OUTPUT_LINES, maxBytes = DEFAULT_MAX_OUTPUT_BYTES, spillDir = os.tmpdir()) {
|
|
25
|
+
this.maxLines = maxLines;
|
|
26
|
+
this.maxBytes = maxBytes;
|
|
27
|
+
this.spillDir = spillDir;
|
|
28
|
+
}
|
|
29
|
+
append(chunk) {
|
|
30
|
+
this.totalBytes += chunk.length;
|
|
31
|
+
for (const b of chunk)
|
|
32
|
+
if (b === 0x0a)
|
|
33
|
+
this.totalNewlines += 1;
|
|
34
|
+
if (!this.spill && this.overDisplayLimit()) {
|
|
35
|
+
this.openSpill();
|
|
36
|
+
}
|
|
37
|
+
if (this.spill) {
|
|
38
|
+
this.spill.write(chunk);
|
|
39
|
+
}
|
|
40
|
+
this.chunks.push(chunk);
|
|
41
|
+
this.tailBytes += chunk.length;
|
|
42
|
+
// Roll the in-memory tail: drop whole chunks from the head while over 2x cap.
|
|
43
|
+
while (this.tailBytes > this.maxBytes * 2 && this.chunks.length > 1) {
|
|
44
|
+
const dropped = this.chunks.shift();
|
|
45
|
+
this.tailBytes -= dropped.length;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
overDisplayLimit() {
|
|
49
|
+
// totalNewlines + 1 approximates the line count; both caps checked like the truncator.
|
|
50
|
+
return this.totalBytes > this.maxBytes || this.totalNewlines + 1 > this.maxLines;
|
|
51
|
+
}
|
|
52
|
+
openSpill() {
|
|
53
|
+
this.spillPath = path.join(this.spillDir, `glbgpt-bash-${randomBytes(8).toString('hex')}.log`);
|
|
54
|
+
try {
|
|
55
|
+
this.spill = createWriteStream(this.spillPath, { flags: 'w' });
|
|
56
|
+
this.spill.on('error', (err) => {
|
|
57
|
+
this.spillError = err;
|
|
58
|
+
});
|
|
59
|
+
// Replay everything captured so far — the rolling tail still holds ALL
|
|
60
|
+
// chunks at this point (spill opens the moment the cap is first crossed,
|
|
61
|
+
// and rolling only starts beyond 2x the cap).
|
|
62
|
+
for (const c of this.chunks)
|
|
63
|
+
this.spill.write(c);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
this.spill = null;
|
|
67
|
+
this.spillPath = null;
|
|
68
|
+
this.spillError = err instanceof Error ? err : new Error(String(err));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Close the spill (if any) and produce the model-facing text: the truncated
|
|
73
|
+
* tail plus a footnote naming the full-output path. The decoded tail drops
|
|
74
|
+
* its first (possibly chunk-severed) partial line when rolling occurred.
|
|
75
|
+
*/
|
|
76
|
+
async finalize() {
|
|
77
|
+
if (this.spill) {
|
|
78
|
+
await new Promise((resolve) => this.spill.end(() => resolve()));
|
|
79
|
+
}
|
|
80
|
+
let decoded = Buffer.concat(this.chunks).toString('utf8');
|
|
81
|
+
if (this.totalBytes > this.tailBytes) {
|
|
82
|
+
// Rolling dropped the true start; also drop the first (possibly partial) decoded line
|
|
83
|
+
// so the snapshot never begins mid-line or mid-character.
|
|
84
|
+
const nl = decoded.indexOf('\n');
|
|
85
|
+
decoded = nl >= 0 ? decoded.slice(nl + 1) : decoded;
|
|
86
|
+
}
|
|
87
|
+
const truncation = truncateTail(decoded, this.maxLines, this.maxBytes);
|
|
88
|
+
// Report the REAL total when rolling lost lines the truncator cannot see.
|
|
89
|
+
const rolledAwayLines = this.totalNewlines + 1 - splitCount(decoded);
|
|
90
|
+
if (rolledAwayLines > 0) {
|
|
91
|
+
truncation.totalLines += rolledAwayLines;
|
|
92
|
+
truncation.startLine += rolledAwayLines;
|
|
93
|
+
truncation.truncated = true;
|
|
94
|
+
if (!truncation.truncatedBy)
|
|
95
|
+
truncation.truncatedBy = 'lines';
|
|
96
|
+
}
|
|
97
|
+
const fullOutputPath = this.spillError ? null : this.spillPath;
|
|
98
|
+
const text = truncation.truncated && fullOutputPath
|
|
99
|
+
? `${truncation.text}\n${truncationFootnote(truncation, fullOutputPath)}`
|
|
100
|
+
: truncation.text;
|
|
101
|
+
return { text, truncation, fullOutputPath };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function splitCount(s) {
|
|
105
|
+
let n = 1;
|
|
106
|
+
for (let i = 0; i < s.length; i++)
|
|
107
|
+
if (s.charCodeAt(i) === 10)
|
|
108
|
+
n += 1;
|
|
109
|
+
return n;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=outputAccumulator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outputAccumulator.js","sourceRoot":"","sources":["../../../src/coding/outputAccumulator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CodingAction, GateVerdict, PermissionGate } from './permissions.js';
|
|
2
|
+
import type { PersistedRules, PersistRule } from './permissionStore.js';
|
|
3
|
+
/**
|
|
4
|
+
* Permission modes (product decision 2026-08-21):
|
|
5
|
+
* - default: every bash/write/edit asks, with "allow for session" building a session allowlist;
|
|
6
|
+
* - accept-edits: file writes auto-approved (workspace-jailed by the tools), bash still asks;
|
|
7
|
+
* - yolo: everything auto-approved (explicit opt-in flag only).
|
|
8
|
+
*/
|
|
9
|
+
export type PermissionMode = 'default' | 'accept-edits' | 'yolo';
|
|
10
|
+
export type ApprovalDecision = 'allow-once' | 'allow-session' | 'allow-project' | 'deny';
|
|
11
|
+
export interface ApprovalRequest {
|
|
12
|
+
action: CodingAction;
|
|
13
|
+
/** One-line headline for the dialog, e.g. `Run command` / `Edit file`. */
|
|
14
|
+
title: string;
|
|
15
|
+
/** The command text or workspace-relative path. */
|
|
16
|
+
detail: string;
|
|
17
|
+
/** Diff / content preview (edit/write), when available. */
|
|
18
|
+
preview?: string;
|
|
19
|
+
/** Whether "Allow for session"/"Always allow (project)" may be offered (never for dangerous commands). */
|
|
20
|
+
allowSession: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Interactive approver (TUI bridge). Absent approver = headless surface (exec). */
|
|
23
|
+
export interface Approver {
|
|
24
|
+
approve(req: ApprovalRequest): Promise<ApprovalDecision>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The coding-tools permission gate. Single choke point (spec 2026-08-21): tools call
|
|
28
|
+
* check() after validating arguments and before any side effect.
|
|
29
|
+
*
|
|
30
|
+
* Guardrails that survive every mode except yolo, and that session allowlists never
|
|
31
|
+
* override: `.git/` writes always ask; dangerous bash (sudo, forced rm, ...) always
|
|
32
|
+
* asks and is never allowlistable. Headless (no approver) fails CLOSED: anything
|
|
33
|
+
* that would ask is denied with a message naming the enable flag.
|
|
34
|
+
*/
|
|
35
|
+
export declare class CodingPermissionGate implements PermissionGate {
|
|
36
|
+
private mode;
|
|
37
|
+
private readonly approver;
|
|
38
|
+
private readonly root;
|
|
39
|
+
/** Explicit per-category headless enables (exec --allow-bash / --allow-write). */
|
|
40
|
+
private readonly headlessAllow;
|
|
41
|
+
private readonly bashAllowlist;
|
|
42
|
+
private readonly bashExactAllowlist;
|
|
43
|
+
private readonly writeDirAllowlist;
|
|
44
|
+
/** Persist callback for 'allow-project' decisions (run.tsx wires the PermissionStore). */
|
|
45
|
+
private readonly onPersist;
|
|
46
|
+
constructor(opts: {
|
|
47
|
+
root: string;
|
|
48
|
+
approver?: Approver | null;
|
|
49
|
+
mode?: PermissionMode;
|
|
50
|
+
headlessAllow?: {
|
|
51
|
+
bash?: boolean;
|
|
52
|
+
write?: boolean;
|
|
53
|
+
};
|
|
54
|
+
/** Rules loaded from the per-workspace permission store (TUI only). */
|
|
55
|
+
persisted?: PersistedRules;
|
|
56
|
+
onPersist?: (rule: PersistRule) => void;
|
|
57
|
+
});
|
|
58
|
+
getMode(): PermissionMode;
|
|
59
|
+
setMode(mode: PermissionMode): void;
|
|
60
|
+
check(action: CodingAction): Promise<GateVerdict>;
|
|
61
|
+
private checkBash;
|
|
62
|
+
private allowedByBashList;
|
|
63
|
+
private checkWrite;
|
|
64
|
+
private allowedByDirList;
|
|
65
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { allowlistPrefixFor, isCompoundCommand, isDangerousCommand, matchesAllowlistPrefix, } from './dangerousCommands.js';
|
|
3
|
+
const DENIED_BY_USER = 'The user denied permission for this action. Adjust your approach or ask the user.';
|
|
4
|
+
/**
|
|
5
|
+
* The coding-tools permission gate. Single choke point (spec 2026-08-21): tools call
|
|
6
|
+
* check() after validating arguments and before any side effect.
|
|
7
|
+
*
|
|
8
|
+
* Guardrails that survive every mode except yolo, and that session allowlists never
|
|
9
|
+
* override: `.git/` writes always ask; dangerous bash (sudo, forced rm, ...) always
|
|
10
|
+
* asks and is never allowlistable. Headless (no approver) fails CLOSED: anything
|
|
11
|
+
* that would ask is denied with a message naming the enable flag.
|
|
12
|
+
*/
|
|
13
|
+
export class CodingPermissionGate {
|
|
14
|
+
mode;
|
|
15
|
+
approver;
|
|
16
|
+
root;
|
|
17
|
+
/** Explicit per-category headless enables (exec --allow-bash / --allow-write). */
|
|
18
|
+
headlessAllow;
|
|
19
|
+
bashAllowlist = new Set(); // two-word prefixes
|
|
20
|
+
bashExactAllowlist = new Set(); // full commands (compound/banned-prefix)
|
|
21
|
+
writeDirAllowlist = new Set(); // directory subtrees
|
|
22
|
+
/** Persist callback for 'allow-project' decisions (run.tsx wires the PermissionStore). */
|
|
23
|
+
onPersist;
|
|
24
|
+
constructor(opts) {
|
|
25
|
+
this.root = opts.root;
|
|
26
|
+
this.approver = opts.approver ?? null;
|
|
27
|
+
this.mode = opts.mode ?? 'default';
|
|
28
|
+
this.headlessAllow = {
|
|
29
|
+
bash: opts.headlessAllow?.bash === true,
|
|
30
|
+
write: opts.headlessAllow?.write === true,
|
|
31
|
+
};
|
|
32
|
+
this.onPersist = opts.onPersist;
|
|
33
|
+
for (const p of opts.persisted?.bashPrefixes ?? [])
|
|
34
|
+
this.bashAllowlist.add(p);
|
|
35
|
+
for (const c of opts.persisted?.bashExact ?? [])
|
|
36
|
+
this.bashExactAllowlist.add(c);
|
|
37
|
+
for (const d of opts.persisted?.writeDirs ?? [])
|
|
38
|
+
this.writeDirAllowlist.add(d);
|
|
39
|
+
}
|
|
40
|
+
getMode() {
|
|
41
|
+
return this.mode;
|
|
42
|
+
}
|
|
43
|
+
setMode(mode) {
|
|
44
|
+
this.mode = mode;
|
|
45
|
+
}
|
|
46
|
+
async check(action) {
|
|
47
|
+
if (this.mode === 'yolo')
|
|
48
|
+
return { ok: true };
|
|
49
|
+
return action.kind === 'bash' ? this.checkBash(action.command) : this.checkWrite(action);
|
|
50
|
+
}
|
|
51
|
+
async checkBash(command) {
|
|
52
|
+
if (this.headlessAllow.bash)
|
|
53
|
+
return { ok: true };
|
|
54
|
+
const dangerous = isDangerousCommand(command);
|
|
55
|
+
if (!dangerous && this.allowedByBashList(command))
|
|
56
|
+
return { ok: true };
|
|
57
|
+
if (!this.approver) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
message: 'Shell commands are disabled here. The user must re-run with --allow-bash (or --yolo) to enable them.',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const decision = await this.approver.approve({
|
|
64
|
+
action: { kind: 'bash', command },
|
|
65
|
+
title: dangerous ? 'Run command (DANGEROUS)' : 'Run command',
|
|
66
|
+
detail: command,
|
|
67
|
+
allowSession: !dangerous,
|
|
68
|
+
});
|
|
69
|
+
if (decision === 'deny')
|
|
70
|
+
return { ok: false, message: DENIED_BY_USER };
|
|
71
|
+
if ((decision === 'allow-session' || decision === 'allow-project') && !dangerous) {
|
|
72
|
+
const prefix = allowlistPrefixFor(command);
|
|
73
|
+
if (prefix)
|
|
74
|
+
this.bashAllowlist.add(prefix);
|
|
75
|
+
else
|
|
76
|
+
this.bashExactAllowlist.add(command);
|
|
77
|
+
if (decision === 'allow-project') {
|
|
78
|
+
this.onPersist?.(prefix ? { type: 'bash-prefix', value: prefix } : { type: 'bash-exact', value: command });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { ok: true };
|
|
82
|
+
}
|
|
83
|
+
allowedByBashList(command) {
|
|
84
|
+
if (this.bashExactAllowlist.has(command))
|
|
85
|
+
return true;
|
|
86
|
+
if (isCompoundCommand(command))
|
|
87
|
+
return false; // prefixes NEVER match compound commands
|
|
88
|
+
for (const prefix of this.bashAllowlist) {
|
|
89
|
+
if (matchesAllowlistPrefix(command, prefix))
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
async checkWrite(action) {
|
|
95
|
+
const rel = path.relative(this.root, action.path);
|
|
96
|
+
const touchesGit = rel.split(path.sep).includes('.git');
|
|
97
|
+
if (!touchesGit) {
|
|
98
|
+
if (this.headlessAllow.write)
|
|
99
|
+
return { ok: true };
|
|
100
|
+
if (this.mode === 'accept-edits')
|
|
101
|
+
return { ok: true };
|
|
102
|
+
if (this.allowedByDirList(action.path))
|
|
103
|
+
return { ok: true };
|
|
104
|
+
}
|
|
105
|
+
if (!this.approver) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
message: touchesGit
|
|
109
|
+
? 'Writing inside .git/ is not allowed here.'
|
|
110
|
+
: 'File writes are disabled here. The user must re-run with --allow-write (or --yolo) to enable them.',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const decision = await this.approver.approve({
|
|
114
|
+
action,
|
|
115
|
+
title: action.kind === 'edit' ? 'Edit file' : 'Write file',
|
|
116
|
+
detail: rel || action.path,
|
|
117
|
+
preview: action.preview,
|
|
118
|
+
allowSession: !touchesGit,
|
|
119
|
+
});
|
|
120
|
+
if (decision === 'deny')
|
|
121
|
+
return { ok: false, message: DENIED_BY_USER };
|
|
122
|
+
if ((decision === 'allow-session' || decision === 'allow-project') && !touchesGit) {
|
|
123
|
+
const dir = path.dirname(action.path);
|
|
124
|
+
this.writeDirAllowlist.add(dir);
|
|
125
|
+
if (decision === 'allow-project')
|
|
126
|
+
this.onPersist?.({ type: 'write-dir', value: dir });
|
|
127
|
+
}
|
|
128
|
+
return { ok: true };
|
|
129
|
+
}
|
|
130
|
+
allowedByDirList(file) {
|
|
131
|
+
for (const dir of this.writeDirAllowlist) {
|
|
132
|
+
if (file === dir || file.startsWith(dir + path.sep))
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=permissionGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionGate.js","sourceRoot":"","sources":["../../../src/coding/permissionGate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent per-workspace permission rules ("Always allow for this project",
|
|
3
|
+
* P1 2026-08-21). One JSON file under the CLI config dir, keyed by workspace
|
|
4
|
+
* root realpath, so rules never leak across projects.
|
|
5
|
+
*
|
|
6
|
+
* Consumed by the TUI only: exec stays fail-closed on explicit flags — a
|
|
7
|
+
* headless run must never gain shell access from an approval granted in some
|
|
8
|
+
* earlier interactive session.
|
|
9
|
+
*
|
|
10
|
+
* Dangerous commands can never reach here (the gate refuses to remember them),
|
|
11
|
+
* and every write is best-effort: a corrupt or unwritable store degrades to
|
|
12
|
+
* session-only behavior, never to an error the user sees mid-approval.
|
|
13
|
+
*/
|
|
14
|
+
export interface PersistedRules {
|
|
15
|
+
bashPrefixes: string[];
|
|
16
|
+
bashExact: string[];
|
|
17
|
+
writeDirs: string[];
|
|
18
|
+
}
|
|
19
|
+
export type PersistRule = {
|
|
20
|
+
type: 'bash-prefix';
|
|
21
|
+
value: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'bash-exact';
|
|
24
|
+
value: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'write-dir';
|
|
27
|
+
value: string;
|
|
28
|
+
};
|
|
29
|
+
export declare class PermissionStore {
|
|
30
|
+
private readonly filePath;
|
|
31
|
+
constructor(filePath?: string);
|
|
32
|
+
load(workspaceRoot: string): Promise<PersistedRules>;
|
|
33
|
+
/** Append one rule (read-modify-write; best-effort, never throws). */
|
|
34
|
+
add(workspaceRoot: string, rule: PersistRule): Promise<void>;
|
|
35
|
+
private read;
|
|
36
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { configDir } from '../config/paths.js';
|
|
4
|
+
const EMPTY = { bashPrefixes: [], bashExact: [], writeDirs: [] };
|
|
5
|
+
export class PermissionStore {
|
|
6
|
+
filePath;
|
|
7
|
+
constructor(filePath = path.join(configDir(), 'permissions.json')) {
|
|
8
|
+
this.filePath = filePath;
|
|
9
|
+
}
|
|
10
|
+
async load(workspaceRoot) {
|
|
11
|
+
const store = await this.read();
|
|
12
|
+
const ws = store.workspaces[workspaceRoot];
|
|
13
|
+
if (!ws)
|
|
14
|
+
return { ...EMPTY };
|
|
15
|
+
return {
|
|
16
|
+
bashPrefixes: dedupeStrings(ws.bashPrefixes),
|
|
17
|
+
bashExact: dedupeStrings(ws.bashExact),
|
|
18
|
+
writeDirs: dedupeStrings(ws.writeDirs),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Append one rule (read-modify-write; best-effort, never throws). */
|
|
22
|
+
async add(workspaceRoot, rule) {
|
|
23
|
+
try {
|
|
24
|
+
const store = await this.read();
|
|
25
|
+
const ws = (store.workspaces[workspaceRoot] ??= {});
|
|
26
|
+
const key = rule.type === 'bash-prefix' ? 'bashPrefixes' : rule.type === 'bash-exact' ? 'bashExact' : 'writeDirs';
|
|
27
|
+
const list = (ws[key] ??= []);
|
|
28
|
+
if (!list.includes(rule.value))
|
|
29
|
+
list.push(rule.value);
|
|
30
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
31
|
+
await fs.writeFile(this.filePath, JSON.stringify(store, null, 2) + '\n', 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Best-effort: the in-memory session rule still applies.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async read() {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(await fs.readFile(this.filePath, 'utf8'));
|
|
40
|
+
if (typeof parsed === 'object' &&
|
|
41
|
+
parsed !== null &&
|
|
42
|
+
parsed.version === 1 &&
|
|
43
|
+
typeof parsed.workspaces === 'object') {
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Missing or corrupt → fresh store.
|
|
49
|
+
}
|
|
50
|
+
return { version: 1, workspaces: {} };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function dedupeStrings(v) {
|
|
54
|
+
return Array.isArray(v) ? [...new Set(v.filter((x) => typeof x === 'string'))] : [];
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=permissionStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionStore.js","sourceRoot":"","sources":["../../../src/coding/permissionStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission gate contract for the coding tools (bash / write_file / edit_file).
|
|
3
|
+
*
|
|
4
|
+
* Single choke point (pi's beforeToolCall shape): tools call {@link PermissionGate.check}
|
|
5
|
+
* AFTER parsing/validating arguments and BEFORE any side effect. A denial is returned to
|
|
6
|
+
* the model as an error tool result — the turn continues and the model can adapt.
|
|
7
|
+
*
|
|
8
|
+
* The full implementation (modes / session allowlist / dangerous-command detection /
|
|
9
|
+
* interactive approver) lives in permissionGate.ts; this file is the dependency surface
|
|
10
|
+
* the tools import, so they stay decoupled from UI concerns.
|
|
11
|
+
*/
|
|
12
|
+
export type CodingAction = {
|
|
13
|
+
kind: 'bash';
|
|
14
|
+
command: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'write';
|
|
17
|
+
path: string;
|
|
18
|
+
preview?: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'edit';
|
|
21
|
+
path: string;
|
|
22
|
+
preview?: string;
|
|
23
|
+
};
|
|
24
|
+
export type GateVerdict = {
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
export interface PermissionGate {
|
|
31
|
+
check(action: CodingAction): Promise<GateVerdict>;
|
|
32
|
+
}
|
|
33
|
+
/** Gate that allows everything — unit tests and yolo mode. */
|
|
34
|
+
export declare const allowAllGate: PermissionGate;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission gate contract for the coding tools (bash / write_file / edit_file).
|
|
3
|
+
*
|
|
4
|
+
* Single choke point (pi's beforeToolCall shape): tools call {@link PermissionGate.check}
|
|
5
|
+
* AFTER parsing/validating arguments and BEFORE any side effect. A denial is returned to
|
|
6
|
+
* the model as an error tool result — the turn continues and the model can adapt.
|
|
7
|
+
*
|
|
8
|
+
* The full implementation (modes / session allowlist / dangerous-command detection /
|
|
9
|
+
* interactive approver) lives in permissionGate.ts; this file is the dependency surface
|
|
10
|
+
* the tools import, so they stay decoupled from UI concerns.
|
|
11
|
+
*/
|
|
12
|
+
/** Gate that allows everything — unit tests and yolo mode. */
|
|
13
|
+
export const allowAllGate = {
|
|
14
|
+
async check() {
|
|
15
|
+
return { ok: true };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=permissions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../../src/coding/permissions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAaH,8DAA8D;AAC9D,MAAM,CAAC,MAAM,YAAY,GAAmB;IAC1C,KAAK,CAAC,KAAK;QACT,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const PROJECT_CONTEXT_MAX_BYTES: number;
|
|
2
|
+
export interface ProjectContextFile {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Collect context files from the repo root down to `workspaceRoot` (root-first order),
|
|
8
|
+
* within one shared byte budget. Files that would blow the budget are skipped whole
|
|
9
|
+
* (never truncated mid-file — a half instruction is worse than none).
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadProjectContext(workspaceRoot: string, maxBytes?: number): Promise<ProjectContextFile[]>;
|
|
12
|
+
/**
|
|
13
|
+
* The coding-guidance system message. Kept deliberately short (pi style): tool-choice
|
|
14
|
+
* strategy the tool descriptions cannot express, the safety posture, and the project
|
|
15
|
+
* instructions. Written to degrade gracefully for models without tool_use — every
|
|
16
|
+
* rule is scoped to "when the tools are available".
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildCodingSystemMessage(opts: {
|
|
19
|
+
workspaceRoot: string;
|
|
20
|
+
contextFiles: ProjectContextFile[];
|
|
21
|
+
}): string;
|