@cleocode/git-shim 2026.4.117 → 2026.4.124

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.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Tests for the git-shim denylist (T1118 L2).
3
+ *
4
+ * Verifies:
5
+ * - Blocked operations are correctly identified for restricted roles.
6
+ * - Allowed operations pass through.
7
+ * - Flag-based matching works for subcommands like `branch` and `reset`.
8
+ *
9
+ * @task T1118
10
+ * @task T1121
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=denylist.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"denylist.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/denylist.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Tests for the git-shim denylist (T1118 L2).
3
+ *
4
+ * Verifies:
5
+ * - Blocked operations are correctly identified for restricted roles.
6
+ * - Allowed operations pass through.
7
+ * - Flag-based matching works for subcommands like `branch` and `reset`.
8
+ *
9
+ * @task T1118
10
+ * @task T1121
11
+ */
12
+ import { describe, expect, it } from 'vitest';
13
+ import { findDeniedOp, RESTRICTED_ROLES } from '../denylist.js';
14
+ describe('RESTRICTED_ROLES', () => {
15
+ it('includes worker, lead, subagent', () => {
16
+ expect(RESTRICTED_ROLES.has('worker')).toBe(true);
17
+ expect(RESTRICTED_ROLES.has('lead')).toBe(true);
18
+ expect(RESTRICTED_ROLES.has('subagent')).toBe(true);
19
+ });
20
+ it('does not include orchestrator', () => {
21
+ expect(RESTRICTED_ROLES.has('orchestrator')).toBe(false);
22
+ });
23
+ });
24
+ describe('findDeniedOp', () => {
25
+ describe('unconditionally blocked subcommands', () => {
26
+ it('blocks git checkout', () => {
27
+ const result = findDeniedOp('checkout', []);
28
+ expect(result).not.toBeNull();
29
+ expect(result?.subcommand).toBe('checkout');
30
+ });
31
+ it('blocks git checkout with args', () => {
32
+ const result = findDeniedOp('checkout', ['main']);
33
+ expect(result).not.toBeNull();
34
+ });
35
+ it('blocks git switch', () => {
36
+ expect(findDeniedOp('switch', [])).not.toBeNull();
37
+ });
38
+ it('blocks git rebase', () => {
39
+ expect(findDeniedOp('rebase', [])).not.toBeNull();
40
+ });
41
+ it('blocks git rebase -i', () => {
42
+ expect(findDeniedOp('rebase', ['-i', 'HEAD~3'])).not.toBeNull();
43
+ });
44
+ it('blocks git update-ref', () => {
45
+ expect(findDeniedOp('update-ref', ['HEAD', 'abc123'])).not.toBeNull();
46
+ });
47
+ });
48
+ describe('flag-gated blocked subcommands', () => {
49
+ it('blocks git branch -b', () => {
50
+ expect(findDeniedOp('branch', ['-b', 'my-branch'])).not.toBeNull();
51
+ });
52
+ it('blocks git branch -D', () => {
53
+ expect(findDeniedOp('branch', ['-D', 'my-branch'])).not.toBeNull();
54
+ });
55
+ it('blocks git branch -d', () => {
56
+ expect(findDeniedOp('branch', ['-d', 'my-branch'])).not.toBeNull();
57
+ });
58
+ it('allows git branch (list)', () => {
59
+ expect(findDeniedOp('branch', [])).toBeNull();
60
+ });
61
+ it('allows git branch -a (list all)', () => {
62
+ expect(findDeniedOp('branch', ['-a'])).toBeNull();
63
+ });
64
+ it('blocks git reset --hard', () => {
65
+ expect(findDeniedOp('reset', ['--hard'])).not.toBeNull();
66
+ });
67
+ it('allows git reset --soft', () => {
68
+ expect(findDeniedOp('reset', ['--soft', 'HEAD~1'])).toBeNull();
69
+ });
70
+ it('allows git reset (mixed)', () => {
71
+ expect(findDeniedOp('reset', ['HEAD~1'])).toBeNull();
72
+ });
73
+ it('blocks git clean -f', () => {
74
+ expect(findDeniedOp('clean', ['-f'])).not.toBeNull();
75
+ });
76
+ it('blocks git clean -fdx', () => {
77
+ expect(findDeniedOp('clean', ['-fdx'])).not.toBeNull();
78
+ });
79
+ it('blocks git stash pop', () => {
80
+ expect(findDeniedOp('stash', ['pop'])).not.toBeNull();
81
+ });
82
+ it('blocks git stash apply', () => {
83
+ expect(findDeniedOp('stash', ['apply'])).not.toBeNull();
84
+ });
85
+ it('allows git stash push', () => {
86
+ expect(findDeniedOp('stash', ['push'])).toBeNull();
87
+ });
88
+ it('allows git stash list', () => {
89
+ expect(findDeniedOp('stash', ['list'])).toBeNull();
90
+ });
91
+ it('blocks git push --force', () => {
92
+ expect(findDeniedOp('push', ['--force'])).not.toBeNull();
93
+ });
94
+ it('blocks git push -f', () => {
95
+ expect(findDeniedOp('push', ['-f', 'origin', 'main'])).not.toBeNull();
96
+ });
97
+ it('allows git push (normal)', () => {
98
+ expect(findDeniedOp('push', ['origin', 'task/T1118'])).toBeNull();
99
+ });
100
+ it('blocks git worktree add', () => {
101
+ expect(findDeniedOp('worktree', ['add', '/some/path'])).not.toBeNull();
102
+ });
103
+ it('blocks git worktree remove', () => {
104
+ expect(findDeniedOp('worktree', ['remove', '/some/path'])).not.toBeNull();
105
+ });
106
+ it('allows git worktree list', () => {
107
+ expect(findDeniedOp('worktree', ['list'])).toBeNull();
108
+ });
109
+ });
110
+ describe('always-allowed subcommands', () => {
111
+ const allowed = [
112
+ ['status', []],
113
+ ['log', ['--oneline']],
114
+ ['diff', ['HEAD~1']],
115
+ ['show', ['HEAD']],
116
+ ['add', ['.']],
117
+ ['commit', ['-m', 'message']],
118
+ ['fetch', ['origin']],
119
+ ['ls-files', []],
120
+ ['rev-parse', ['HEAD']],
121
+ ['cat-file', ['-p', 'HEAD:file.ts']],
122
+ ];
123
+ for (const [sub, args] of allowed) {
124
+ it(`allows git ${sub}`, () => {
125
+ expect(findDeniedOp(sub, [...args])).toBeNull();
126
+ });
127
+ }
128
+ });
129
+ });
130
+ //# sourceMappingURL=denylist.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"denylist.test.js","sourceRoot":"","sources":["../../src/__tests__/denylist.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Branch-mutation denylist for the git-shim binary (T1118 L2).
3
+ *
4
+ * When CLEO_AGENT_ROLE is set to a restricted role (worker|lead|subagent),
5
+ * any git invocation matching an entry in this list is rejected with exit 1
6
+ * and a structured error on stderr.
7
+ *
8
+ * The allowlist is implicit: any subcommand NOT in the denylist passes through
9
+ * to real git transparently.
10
+ *
11
+ * @task T1118
12
+ * @task T1121
13
+ */
14
+ import type { DeniedGitOp } from '@cleocode/contracts';
15
+ /**
16
+ * Roles that are subject to the branch-mutation denylist.
17
+ *
18
+ * Orchestrators bypass the shim entirely.
19
+ *
20
+ * @task T1118
21
+ * @task T1121
22
+ */
23
+ export declare const RESTRICTED_ROLES: Set<string>;
24
+ /**
25
+ * Branch-mutating git operations that are denied for restricted roles.
26
+ *
27
+ * Entries are matched against argv[1] (subcommand) and optional flags.
28
+ * When a flag is present, BOTH the subcommand AND the flag must appear.
29
+ *
30
+ * @remarks
31
+ * - `flag: undefined` means any invocation of the subcommand is denied.
32
+ * - `flag: "--hard"` means only `git reset --hard` is denied; `git reset
33
+ * --soft` passes through.
34
+ *
35
+ * @task T1118
36
+ * @task T1121
37
+ */
38
+ export declare const GIT_OP_DENYLIST: ReadonlyArray<DeniedGitOp>;
39
+ /**
40
+ * Determine whether a git invocation should be denied for a restricted role.
41
+ *
42
+ * @param subcommand - The git subcommand (argv[1]).
43
+ * @param args - The remaining arguments (argv[2..]).
44
+ * @returns The matching {@link DeniedGitOp} if denied, or null if allowed.
45
+ */
46
+ export declare function findDeniedOp(subcommand: string, args: string[]): DeniedGitOp | null;
47
+ //# sourceMappingURL=denylist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"denylist.d.ts","sourceRoot":"","sources":["../src/denylist.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,aAA0C,CAAC;AAExE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,EAAE,aAAa,CAAC,WAAW,CA+JtD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,WAAW,GAAG,IAAI,CAenF"}
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Branch-mutation denylist for the git-shim binary (T1118 L2).
3
+ *
4
+ * When CLEO_AGENT_ROLE is set to a restricted role (worker|lead|subagent),
5
+ * any git invocation matching an entry in this list is rejected with exit 1
6
+ * and a structured error on stderr.
7
+ *
8
+ * The allowlist is implicit: any subcommand NOT in the denylist passes through
9
+ * to real git transparently.
10
+ *
11
+ * @task T1118
12
+ * @task T1121
13
+ */
14
+ /**
15
+ * Roles that are subject to the branch-mutation denylist.
16
+ *
17
+ * Orchestrators bypass the shim entirely.
18
+ *
19
+ * @task T1118
20
+ * @task T1121
21
+ */
22
+ export const RESTRICTED_ROLES = new Set(['worker', 'lead', 'subagent']);
23
+ /**
24
+ * Branch-mutating git operations that are denied for restricted roles.
25
+ *
26
+ * Entries are matched against argv[1] (subcommand) and optional flags.
27
+ * When a flag is present, BOTH the subcommand AND the flag must appear.
28
+ *
29
+ * @remarks
30
+ * - `flag: undefined` means any invocation of the subcommand is denied.
31
+ * - `flag: "--hard"` means only `git reset --hard` is denied; `git reset
32
+ * --soft` passes through.
33
+ *
34
+ * @task T1118
35
+ * @task T1121
36
+ */
37
+ export const GIT_OP_DENYLIST = [
38
+ // Branch-switching operations
39
+ {
40
+ subcommand: 'checkout',
41
+ reason: 'agents MUST NOT switch branches — use the assigned worktree branch',
42
+ },
43
+ {
44
+ subcommand: 'switch',
45
+ reason: 'agents MUST NOT switch branches — use the assigned worktree branch',
46
+ },
47
+ // Branch mutation
48
+ {
49
+ subcommand: 'branch',
50
+ flag: '-b',
51
+ reason: 'agents MUST NOT create branches — the orchestrator manages branch lifecycle',
52
+ },
53
+ {
54
+ subcommand: 'branch',
55
+ flag: '-B',
56
+ reason: 'agents MUST NOT create/reset branches — the orchestrator manages branch lifecycle',
57
+ },
58
+ {
59
+ subcommand: 'branch',
60
+ flag: '-D',
61
+ reason: 'agents MUST NOT delete branches — the orchestrator manages branch lifecycle',
62
+ },
63
+ {
64
+ subcommand: 'branch',
65
+ flag: '-d',
66
+ reason: 'agents MUST NOT delete branches — the orchestrator manages branch lifecycle',
67
+ },
68
+ {
69
+ subcommand: 'branch',
70
+ flag: '--delete',
71
+ reason: 'agents MUST NOT delete branches — the orchestrator manages branch lifecycle',
72
+ },
73
+ {
74
+ subcommand: 'branch',
75
+ flag: '-m',
76
+ reason: 'agents MUST NOT rename branches — the orchestrator manages branch lifecycle',
77
+ },
78
+ {
79
+ subcommand: 'branch',
80
+ flag: '-M',
81
+ reason: 'agents MUST NOT rename branches — the orchestrator manages branch lifecycle',
82
+ },
83
+ {
84
+ subcommand: 'branch',
85
+ flag: '--move',
86
+ reason: 'agents MUST NOT rename branches — the orchestrator manages branch lifecycle',
87
+ },
88
+ // Worktree operations (workers must not create/remove worktrees)
89
+ {
90
+ subcommand: 'worktree',
91
+ flag: 'add',
92
+ reason: 'agents MUST NOT create worktrees — the orchestrator manages worktree lifecycle',
93
+ },
94
+ {
95
+ subcommand: 'worktree',
96
+ flag: 'remove',
97
+ reason: 'agents MUST NOT remove worktrees — the orchestrator manages worktree lifecycle',
98
+ },
99
+ {
100
+ subcommand: 'worktree',
101
+ flag: 'move',
102
+ reason: 'agents MUST NOT move worktrees — the orchestrator manages worktree lifecycle',
103
+ },
104
+ {
105
+ subcommand: 'worktree',
106
+ flag: 'lock',
107
+ reason: 'agents MUST NOT lock/unlock worktrees — the orchestrator manages worktree lifecycle',
108
+ },
109
+ {
110
+ subcommand: 'worktree',
111
+ flag: 'unlock',
112
+ reason: 'agents MUST NOT lock/unlock worktrees — the orchestrator manages worktree lifecycle',
113
+ },
114
+ {
115
+ subcommand: 'worktree',
116
+ flag: 'prune',
117
+ reason: 'agents MUST NOT prune worktrees — the orchestrator manages worktree lifecycle',
118
+ },
119
+ // Destructive reset
120
+ {
121
+ subcommand: 'reset',
122
+ flag: '--hard',
123
+ reason: 'agents MUST NOT hard-reset — create a new commit or use `git restore` instead',
124
+ },
125
+ {
126
+ subcommand: 'reset',
127
+ flag: '--merge',
128
+ reason: 'agents MUST NOT merge-reset — create a new commit or use `git restore` instead',
129
+ },
130
+ {
131
+ subcommand: 'reset',
132
+ flag: '--keep',
133
+ reason: 'agents MUST NOT keep-reset — create a new commit or use `git restore` instead',
134
+ },
135
+ // Clean operations
136
+ {
137
+ subcommand: 'clean',
138
+ flag: '-f',
139
+ reason: 'agents MUST NOT force-clean — use `git restore` for individual files',
140
+ },
141
+ {
142
+ subcommand: 'clean',
143
+ flag: '-fd',
144
+ reason: 'agents MUST NOT force-clean directories — use `git restore` for individual files',
145
+ },
146
+ {
147
+ subcommand: 'clean',
148
+ flag: '-fdx',
149
+ reason: 'agents MUST NOT force-clean directories including ignored files — unsafe for agents',
150
+ },
151
+ // Rebase (rewrites history)
152
+ {
153
+ subcommand: 'rebase',
154
+ reason: "agents MUST NOT rebase — history rewriting is the orchestrator's responsibility",
155
+ },
156
+ // Stash pop/apply (applies stashed changes to working tree)
157
+ {
158
+ subcommand: 'stash',
159
+ flag: 'pop',
160
+ reason: 'agents MUST NOT pop stashes — use explicit git apply or cherry-pick instead',
161
+ },
162
+ {
163
+ subcommand: 'stash',
164
+ flag: 'apply',
165
+ reason: 'agents MUST NOT apply stashes — use explicit git apply or cherry-pick instead',
166
+ },
167
+ // Direct ref manipulation
168
+ {
169
+ subcommand: 'update-ref',
170
+ reason: 'agents MUST NOT directly manipulate refs — use commits instead',
171
+ },
172
+ // Force push
173
+ {
174
+ subcommand: 'push',
175
+ flag: '-f',
176
+ reason: 'agents MUST NOT force-push — the orchestrator manages push lifecycle',
177
+ },
178
+ {
179
+ subcommand: 'push',
180
+ flag: '--force',
181
+ reason: 'agents MUST NOT force-push — the orchestrator manages push lifecycle',
182
+ },
183
+ {
184
+ subcommand: 'push',
185
+ flag: '--force-with-lease',
186
+ reason: 'agents MUST NOT force-push — the orchestrator manages push lifecycle',
187
+ },
188
+ ];
189
+ /**
190
+ * Determine whether a git invocation should be denied for a restricted role.
191
+ *
192
+ * @param subcommand - The git subcommand (argv[1]).
193
+ * @param args - The remaining arguments (argv[2..]).
194
+ * @returns The matching {@link DeniedGitOp} if denied, or null if allowed.
195
+ */
196
+ export function findDeniedOp(subcommand, args) {
197
+ for (const entry of GIT_OP_DENYLIST) {
198
+ if (entry.subcommand !== subcommand)
199
+ continue;
200
+ if (entry.flag === undefined) {
201
+ // Any invocation of this subcommand is denied.
202
+ return entry;
203
+ }
204
+ // Check if the flag appears anywhere in the argument list.
205
+ if (args.includes(entry.flag)) {
206
+ return entry;
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ //# sourceMappingURL=denylist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"denylist.js","sourceRoot":"","sources":["../src/denylist.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @cleocode/git-shim — Harness-agnostic git branch-mutation fence.
3
+ *
4
+ * Exports the denylist utilities so other packages can inspect the
5
+ * blocked operation table without executing the shim binary.
6
+ *
7
+ * The shim binary itself lives at `dist/shim.js` and is registered
8
+ * as the `git` bin entry in package.json.
9
+ *
10
+ * @task T1118
11
+ * @task T1121
12
+ * @packageDocumentation
13
+ */
14
+ export type {} from './denylist.js';
15
+ export { findDeniedOp, GIT_OP_DENYLIST, RESTRICTED_ROLES } from './denylist.js';
16
+ /**
17
+ * Install the shim symlink so that `git` resolves to this shim when
18
+ * the shim directory is on PATH.
19
+ *
20
+ * Creates `<shimDir>/git` as a symlink pointing at the shim binary.
21
+ * The shim binary itself is the `dist/shim.js` file emitted by tsc.
22
+ *
23
+ * @param shimDir - Directory to place the `git` symlink in.
24
+ * @param shimBinPath - Absolute path to the compiled shim binary (dist/shim.js).
25
+ * @returns Whether the symlink was created or already existed.
26
+ * @task T1118
27
+ * @task T1121
28
+ */
29
+ export declare function installShimSymlink(shimDir: string, shimBinPath: string): Promise<boolean>;
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,YAAY,EAAE,MAAM,eAAe,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEhF;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiC/F"}
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @cleocode/git-shim — Harness-agnostic git branch-mutation fence.
3
+ *
4
+ * Exports the denylist utilities so other packages can inspect the
5
+ * blocked operation table without executing the shim binary.
6
+ *
7
+ * The shim binary itself lives at `dist/shim.js` and is registered
8
+ * as the `git` bin entry in package.json.
9
+ *
10
+ * @task T1118
11
+ * @task T1121
12
+ * @packageDocumentation
13
+ */
14
+ export { findDeniedOp, GIT_OP_DENYLIST, RESTRICTED_ROLES } from './denylist.js';
15
+ /**
16
+ * Install the shim symlink so that `git` resolves to this shim when
17
+ * the shim directory is on PATH.
18
+ *
19
+ * Creates `<shimDir>/git` as a symlink pointing at the shim binary.
20
+ * The shim binary itself is the `dist/shim.js` file emitted by tsc.
21
+ *
22
+ * @param shimDir - Directory to place the `git` symlink in.
23
+ * @param shimBinPath - Absolute path to the compiled shim binary (dist/shim.js).
24
+ * @returns Whether the symlink was created or already existed.
25
+ * @task T1118
26
+ * @task T1121
27
+ */
28
+ export async function installShimSymlink(shimDir, shimBinPath) {
29
+ const { existsSync, mkdirSync, symlinkSync, unlinkSync } = await import('node:fs');
30
+ const { join } = await import('node:path');
31
+ mkdirSync(shimDir, { recursive: true });
32
+ const linkPath = join(shimDir, 'git');
33
+ // Remove stale symlink if it points somewhere wrong.
34
+ if (existsSync(linkPath)) {
35
+ try {
36
+ const { readlinkSync } = await import('node:fs');
37
+ const target = readlinkSync(linkPath);
38
+ if (target === shimBinPath)
39
+ return true; // already correct
40
+ unlinkSync(linkPath);
41
+ }
42
+ catch {
43
+ try {
44
+ unlinkSync(linkPath);
45
+ }
46
+ catch {
47
+ // ignore
48
+ }
49
+ }
50
+ }
51
+ try {
52
+ symlinkSync(shimBinPath, linkPath);
53
+ // Ensure the shim binary is executable.
54
+ const { chmodSync } = await import('node:fs');
55
+ chmodSync(shimBinPath, 0o755);
56
+ return true;
57
+ }
58
+ catch (err) {
59
+ const message = err instanceof Error ? err.message : String(err);
60
+ throw new Error(`Failed to install git shim symlink at ${linkPath}: ${message}`);
61
+ }
62
+ }
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEhF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAAe,EAAE,WAAmB;IAC3E,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;IACnF,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;IAE3C,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAEtC,qDAAqD;IACrD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;YACtC,IAAI,MAAM,KAAK,WAAW;gBAAE,OAAO,IAAI,CAAC,CAAC,kBAAkB;YAC3D,UAAU,CAAC,QAAQ,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC;gBACH,UAAU,CAAC,QAAQ,CAAC,CAAC;YACvB,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,CAAC;QACH,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QACnC,wCAAwC;QACxC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;QAC9C,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,yCAAyC,QAAQ,KAAK,OAAO,EAAE,CAAC,CAAC;IACnF,CAAC;AACH,CAAC"}
package/dist/shim.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * git-shim — harness-agnostic git branch-mutation fence (T1118 L2).
4
+ *
5
+ * Usage: place this binary on PATH BEFORE real git, and export:
6
+ * CLEO_AGENT_ROLE=worker (or lead|subagent)
7
+ * CLEO_WORKTREE_ROOT=<path>
8
+ *
9
+ * Behaviour:
10
+ * - If CLEO_AGENT_ROLE is a restricted role AND the git subcommand is in
11
+ * the denylist AND CLEO_ALLOW_BRANCH_OPS is not set: print structured
12
+ * error to stderr, exit 1.
13
+ * - If CLEO_ALLOW_BRANCH_OPS=1: log a warning and pass through (escape hatch).
14
+ * - Otherwise: exec real git with all original args (zero overhead path).
15
+ *
16
+ * The real git binary is resolved from CLEO_REAL_GIT_PATH env (override) or
17
+ * discovered by walking PATH and skipping any entry that resolves to this
18
+ * shim itself (detected via CLEO_SHIM_MARKER env set by the shim installer).
19
+ *
20
+ * Exit codes:
21
+ * 0 — passthrough succeeded (real git exited 0)
22
+ * 1 — git op blocked by shim
23
+ * 77 — well-known CLEO_GIT_BLOCKED sentinel (same as exit 1 but distinguishable)
24
+ * N — real git exit code propagated
25
+ *
26
+ * @task T1118
27
+ * @task T1121
28
+ */
29
+ export {};
30
+ //# sourceMappingURL=shim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shim.d.ts","sourceRoot":"","sources":["../src/shim.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG"}
package/dist/shim.js ADDED
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * git-shim — harness-agnostic git branch-mutation fence (T1118 L2).
4
+ *
5
+ * Usage: place this binary on PATH BEFORE real git, and export:
6
+ * CLEO_AGENT_ROLE=worker (or lead|subagent)
7
+ * CLEO_WORKTREE_ROOT=<path>
8
+ *
9
+ * Behaviour:
10
+ * - If CLEO_AGENT_ROLE is a restricted role AND the git subcommand is in
11
+ * the denylist AND CLEO_ALLOW_BRANCH_OPS is not set: print structured
12
+ * error to stderr, exit 1.
13
+ * - If CLEO_ALLOW_BRANCH_OPS=1: log a warning and pass through (escape hatch).
14
+ * - Otherwise: exec real git with all original args (zero overhead path).
15
+ *
16
+ * The real git binary is resolved from CLEO_REAL_GIT_PATH env (override) or
17
+ * discovered by walking PATH and skipping any entry that resolves to this
18
+ * shim itself (detected via CLEO_SHIM_MARKER env set by the shim installer).
19
+ *
20
+ * Exit codes:
21
+ * 0 — passthrough succeeded (real git exited 0)
22
+ * 1 — git op blocked by shim
23
+ * 77 — well-known CLEO_GIT_BLOCKED sentinel (same as exit 1 but distinguishable)
24
+ * N — real git exit code propagated
25
+ *
26
+ * @task T1118
27
+ * @task T1121
28
+ */
29
+ import { spawnSync } from 'node:child_process';
30
+ import { existsSync } from 'node:fs';
31
+ import { join } from 'node:path';
32
+ import { findDeniedOp, RESTRICTED_ROLES } from './denylist.js';
33
+ /** Sentinel exit code for a shim-blocked git operation. */
34
+ const BLOCKED_EXIT_CODE = 77;
35
+ /**
36
+ * Read and validate the CLEO agent role from the environment.
37
+ *
38
+ * @returns The agent role string, or null if not set or not restricted.
39
+ */
40
+ function getAgentRole() {
41
+ const role = process.env['CLEO_AGENT_ROLE'];
42
+ if (!role)
43
+ return null;
44
+ return RESTRICTED_ROLES.has(role) ? role : null;
45
+ }
46
+ /**
47
+ * Resolve the path to the real git binary.
48
+ *
49
+ * Strategy:
50
+ * 1. If CLEO_REAL_GIT_PATH env is set, use it (override for testing).
51
+ * 2. Walk PATH entries, skip the first one that contains CLEO_SHIM_MARKER.
52
+ * 3. Fall back to /usr/bin/git, /usr/local/bin/git, /opt/homebrew/bin/git.
53
+ *
54
+ * @returns Absolute path to real git, or null if not found.
55
+ */
56
+ function resolveRealGit() {
57
+ // Explicit override wins — useful for tests.
58
+ const override = process.env['CLEO_REAL_GIT_PATH'];
59
+ if (override && existsSync(override))
60
+ return override;
61
+ const shimMarker = process.env['CLEO_SHIM_MARKER'] ?? '.cleo/bin/git-shim';
62
+ const pathDirs = (process.env['PATH'] ?? '').split(':');
63
+ for (const dir of pathDirs) {
64
+ // Skip directories that look like the shim directory.
65
+ if (dir.includes(shimMarker))
66
+ continue;
67
+ const candidate = join(dir, 'git');
68
+ if (existsSync(candidate))
69
+ return candidate;
70
+ }
71
+ // Hard fallbacks for common installations.
72
+ const fallbacks = ['/usr/bin/git', '/usr/local/bin/git', '/opt/homebrew/bin/git'];
73
+ for (const fb of fallbacks) {
74
+ if (existsSync(fb))
75
+ return fb;
76
+ }
77
+ return null;
78
+ }
79
+ /**
80
+ * Emit a structured JSON error line to stderr.
81
+ *
82
+ * Format mirrors LAFS error envelopes so parsers can detect it.
83
+ *
84
+ * @param code - CLEO error code string.
85
+ * @param message - Human-readable message.
86
+ * @param context - Additional key/value context.
87
+ */
88
+ function emitBlockedError(code, message, context) {
89
+ process.stderr.write(JSON.stringify({
90
+ cleo_error: true,
91
+ code,
92
+ message,
93
+ ...context,
94
+ }) + '\n');
95
+ // Also emit a plain-text line so human readers can understand without parsing.
96
+ process.stderr.write(`[git-shim] BLOCKED: ${message}\n` +
97
+ `[git-shim] Set CLEO_ALLOW_BRANCH_OPS=1 to bypass (audited).\n`);
98
+ }
99
+ /**
100
+ * Main shim entry point.
101
+ *
102
+ * Reads argv, env, applies the denylist, and either blocks or passes through
103
+ * to the real git binary.
104
+ */
105
+ function main() {
106
+ const argv = process.argv.slice(2); // strip "node" and shim path
107
+ const subcommand = argv[0];
108
+ // Fast path: no subcommand → pass through (git with no args shows usage).
109
+ if (!subcommand) {
110
+ const realGit = resolveRealGit();
111
+ if (!realGit) {
112
+ process.stderr.write('[git-shim] ERROR: real git binary not found\n');
113
+ process.exit(1);
114
+ }
115
+ const result = spawnSync(realGit, argv, { stdio: 'inherit' });
116
+ process.exit(result.status ?? 1);
117
+ return;
118
+ }
119
+ const role = getAgentRole();
120
+ // Fast path: no restricted role → pass through unconditionally.
121
+ if (!role) {
122
+ const realGit = resolveRealGit();
123
+ if (!realGit) {
124
+ process.stderr.write('[git-shim] ERROR: real git binary not found\n');
125
+ process.exit(1);
126
+ }
127
+ const result = spawnSync(realGit, argv, { stdio: 'inherit' });
128
+ process.exit(result.status ?? 1);
129
+ return;
130
+ }
131
+ // Check escape hatch — operator has explicitly permitted this one invocation.
132
+ const allowBranchOps = process.env['CLEO_ALLOW_BRANCH_OPS'] === '1';
133
+ const remainingArgs = argv.slice(1);
134
+ const denied = findDeniedOp(subcommand, remainingArgs);
135
+ if (denied && !allowBranchOps) {
136
+ const worktreeRoot = process.env['CLEO_WORKTREE_ROOT'] ?? '(unknown)';
137
+ emitBlockedError('E_GIT_OP_BLOCKED', `git ${subcommand}${denied.flag ? ` ${denied.flag}` : ''} is not allowed for role '${role}': ${denied.reason}`, {
138
+ subcommand,
139
+ flag: denied.flag ?? '',
140
+ role,
141
+ worktree_root: worktreeRoot,
142
+ });
143
+ process.exit(BLOCKED_EXIT_CODE);
144
+ return;
145
+ }
146
+ if (denied && allowBranchOps) {
147
+ // Escape hatch used — log an audit warning.
148
+ process.stderr.write(`[git-shim] WARNING: CLEO_ALLOW_BRANCH_OPS=1 bypassed block for ` +
149
+ `'git ${subcommand}' (role=${role}). This bypass is audited.\n`);
150
+ }
151
+ // Pass through to real git.
152
+ const realGit = resolveRealGit();
153
+ if (!realGit) {
154
+ process.stderr.write('[git-shim] ERROR: real git binary not found on PATH\n');
155
+ process.exit(1);
156
+ }
157
+ const result = spawnSync(realGit, argv, { stdio: 'inherit' });
158
+ process.exit(result.status ?? 0);
159
+ }
160
+ main();
161
+ //# sourceMappingURL=shim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shim.js","sourceRoot":"","sources":["../src/shim.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/git-shim",
3
- "version": "2026.4.117",
3
+ "version": "2026.4.124",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Harness-agnostic git shim for CLEO agent branch-protection (T1118)",
@@ -32,15 +32,17 @@
32
32
  "author": "CLEO Code <hello@cleocode.dev>",
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
- "@cleocode/contracts": "2026.4.117"
35
+ "@cleocode/contracts": "2026.4.124"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^24.3.0",
39
39
  "typescript": "^6.0.2",
40
40
  "vitest": "^4.1.4"
41
41
  },
42
- "publishConfig": {
43
- "access": "public"
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/kryptobaseddev/cleo",
45
+ "directory": "packages/git-shim"
44
46
  },
45
47
  "scripts": {
46
48
  "build": "tsc -b",