@cleocode/git-shim 2026.4.154 → 2026.4.158
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 +147 -0
- package/dist/__tests__/boundary-enforcement.test.d.ts +2 -0
- package/dist/__tests__/boundary-enforcement.test.d.ts.map +1 -0
- package/dist/__tests__/boundary-enforcement.test.js +282 -0
- package/dist/__tests__/boundary-enforcement.test.js.map +1 -0
- package/dist/audit-log.d.ts +59 -0
- package/dist/audit-log.d.ts.map +1 -0
- package/dist/audit-log.js +66 -0
- package/dist/audit-log.js.map +1 -0
- package/dist/boundary.d.ts +119 -0
- package/dist/boundary.d.ts.map +1 -0
- package/dist/boundary.js +242 -0
- package/dist/boundary.js.map +1 -0
- package/dist/index.d.ts +11 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/shim.d.ts +16 -14
- package/dist/shim.d.ts.map +1 -1
- package/dist/shim.js +138 -33
- package/dist/shim.js.map +1 -1
- package/dist/worktree-path.d.ts +69 -0
- package/dist/worktree-path.d.ts.map +1 -0
- package/dist/worktree-path.js +146 -0
- package/dist/worktree-path.js.map +1 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# @cleocode/git-shim
|
|
2
|
+
|
|
3
|
+
Harness-agnostic `git` PATH override that fences agent (worker | lead |
|
|
4
|
+
subagent) git invocations. Provides two layers of enforcement:
|
|
5
|
+
|
|
6
|
+
1. **T1118 denylist** — branch-mutation operations
|
|
7
|
+
(`checkout`, `switch`, `branch -D`, `worktree add`, `reset --hard`,
|
|
8
|
+
`rebase`, `push --force`, etc.).
|
|
9
|
+
2. **T1591 boundary fence** — contextual rules tied to the
|
|
10
|
+
worktree-by-default model (ADR-055) + worktree-merge integration
|
|
11
|
+
(ADR-062):
|
|
12
|
+
- **(a)** `git add` MUST stage paths inside the active worktree.
|
|
13
|
+
- **(b)** `git commit -m "<msg>"` MUST embed a CLEO task ID
|
|
14
|
+
(`T<NUM>`).
|
|
15
|
+
- **(c)** `git merge` MUST be invoked by `completeAgentWorktreeViaMerge`
|
|
16
|
+
(signalled via `CLEO_ORCHESTRATE_MERGE=1`).
|
|
17
|
+
- **(d)** `git cherry-pick <ref>` MUST NOT take a `task/T<NUM>` source
|
|
18
|
+
(cherry-pick from worktree branches is the deprecated integration
|
|
19
|
+
path superseded by ADR-062).
|
|
20
|
+
|
|
21
|
+
Every block and every bypass is recorded in
|
|
22
|
+
`<XDG_DATA_HOME ?? ~/.local/share>/cleo/audit/git-shim.jsonl`.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
The shim ships as the `git` bin entry of this package. The orchestrator
|
|
27
|
+
materialises a `git` symlink inside `<projectRoot>/.cleo/bin/` (or any
|
|
28
|
+
shim directory) via `installShimSymlink(...)` and prepends that
|
|
29
|
+
directory to the spawned agent's `PATH`.
|
|
30
|
+
|
|
31
|
+
## Activation gate
|
|
32
|
+
|
|
33
|
+
The shim only enforces when the agent role is restricted:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
CLEO_AGENT_ROLE=worker # or lead, subagent
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Orchestrators (the absence of `CLEO_AGENT_ROLE` or
|
|
40
|
+
`CLEO_AGENT_ROLE=orchestrator`) bypass the shim entirely — this keeps
|
|
41
|
+
orchestration code paths fast and unencumbered.
|
|
42
|
+
|
|
43
|
+
## Environment contract
|
|
44
|
+
|
|
45
|
+
| Variable | Set by | Purpose |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `CLEO_AGENT_ROLE` | orchestrator (spawn) | Activates the fence for restricted roles. |
|
|
48
|
+
| `CLEO_WORKTREE_ROOT` | orchestrator (spawn) | Explicit worktree path; auto-detected from cwd otherwise. |
|
|
49
|
+
| `CLEO_TASK_ID` | orchestrator (spawn) | Active task ID; auto-derived from worktree path otherwise. |
|
|
50
|
+
| `CLEO_ORCHESTRATE_MERGE` | `completeAgentWorktreeViaMerge` (T1587) | Single-purpose grant — allows `git merge`. See below. |
|
|
51
|
+
| `CLEO_ALLOW_BRANCH_OPS` | operator (manual) | Single-shot bypass for the legacy denylist. Audited. |
|
|
52
|
+
| `CLEO_ALLOW_GIT` | operator (emergency) | Universal bypass for any T1591 boundary block. Audited. |
|
|
53
|
+
| `CLEO_AUDIT_LOG_PATH` | tests | Override audit log location. |
|
|
54
|
+
| `CLEO_REAL_GIT_PATH` | tests | Skip PATH walk; use this binary as real git. |
|
|
55
|
+
| `CLEO_SHIM_MARKER` | installer (optional) | Path-fragment that identifies the shim dir. Default: `.cleo/bin/git-shim`. |
|
|
56
|
+
|
|
57
|
+
## `CLEO_ORCHESTRATE_MERGE=1` contract (T1587 / ADR-062 integration)
|
|
58
|
+
|
|
59
|
+
`packages/core/src/spawn/branch-lock.ts::completeAgentWorktreeViaMerge`
|
|
60
|
+
is the **only** sanctioned entry point for `git merge` on a CLEO task
|
|
61
|
+
branch. It MUST set `CLEO_ORCHESTRATE_MERGE=1` in the env passed to the
|
|
62
|
+
spawned `git merge --no-ff` process. The shim's boundary (c) refuses any
|
|
63
|
+
other `git merge` invocation from a restricted role.
|
|
64
|
+
|
|
65
|
+
Concretely (see `branch-lock.ts:830`):
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
execFileSync('git', ['merge', '--no-ff', branch, '-m', subject], {
|
|
69
|
+
cwd: gitRoot,
|
|
70
|
+
encoding: 'utf-8',
|
|
71
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
72
|
+
env: { ...process.env, CLEO_ORCHESTRATE_MERGE: '1' },
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If you discover a code path that needs to invoke `git merge` from
|
|
77
|
+
restricted-role context **other than** `completeAgentWorktreeViaMerge`,
|
|
78
|
+
file a follow-up task — do NOT silently set `CLEO_ORCHESTRATE_MERGE=1`
|
|
79
|
+
elsewhere. The narrow scope of this env var is what makes the boundary
|
|
80
|
+
auditable.
|
|
81
|
+
|
|
82
|
+
`--abort`, `--continue`, and `--quit` flag invocations are exempt —
|
|
83
|
+
they do not perform a merge.
|
|
84
|
+
|
|
85
|
+
## Bypass workflow
|
|
86
|
+
|
|
87
|
+
For a legitimate emergency (e.g. operator hotfix, incident triage):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# One-off bypass; audit entry written to git-shim.jsonl.
|
|
91
|
+
CLEO_ALLOW_GIT=1 git <subcommand> ...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The bypass is logged with full context (subcommand, args, cwd, role,
|
|
95
|
+
task_id, worktree_path). Reviewers can list bypasses via:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
jq 'select(.outcome | startswith("bypassed"))' \
|
|
99
|
+
~/.local/share/cleo/audit/git-shim.jsonl
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Audit record schema
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
interface AuditRecord {
|
|
106
|
+
ts: string; // ISO 8601
|
|
107
|
+
outcome: 'blocked' | 'bypassed-allow-git' | 'bypassed-orchestrate-merge';
|
|
108
|
+
boundary: 'a' | 'b' | 'c' | 'd' | 'denylist';
|
|
109
|
+
code: string; // E_GIT_OP_BLOCKED | E_GIT_BOUNDARY_*
|
|
110
|
+
subcommand: string;
|
|
111
|
+
args: string[];
|
|
112
|
+
cwd: string;
|
|
113
|
+
worktree_path: string | null;
|
|
114
|
+
task_id: string | null;
|
|
115
|
+
role: string | null;
|
|
116
|
+
context: Record<string, string>;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The schema is project-agnostic: the audit log under
|
|
121
|
+
`~/.local/share/cleo/audit/` is shared across every CLEO-managed
|
|
122
|
+
project on the host.
|
|
123
|
+
|
|
124
|
+
## Defense-in-depth pipeline
|
|
125
|
+
|
|
126
|
+
The git-binary fence (this package) is one layer in a multi-checkpoint
|
|
127
|
+
pipeline:
|
|
128
|
+
|
|
129
|
+
| Layer | Package | Catches |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| Git binary | `@cleocode/git-shim` (this) | Direct git invocations from agents |
|
|
132
|
+
| Commit-msg hook | `@cleocode/core` (T1588) | Editor-flow commits (no inline `-m`) |
|
|
133
|
+
| Drift watchdog | `@cleocode/core` (T1594) | Files modified outside worktree |
|
|
134
|
+
| Pre-push reconcile | `@cleocode/core` (T1595) | Branch state drift before push |
|
|
135
|
+
| Sync linter | CI (T1598) | Review-time policy mismatches |
|
|
136
|
+
|
|
137
|
+
Both T1588 (hook) and T1591 (shim, boundary b) enforce the
|
|
138
|
+
commit-subject T-ID rule. This is intentional defense in depth: a
|
|
139
|
+
developer who disables the local hook still trips the shim.
|
|
140
|
+
|
|
141
|
+
## References
|
|
142
|
+
|
|
143
|
+
- ADR-055 — worktree-by-default agent isolation
|
|
144
|
+
- ADR-062 — worktree integration via `git merge --no-ff` (not cherry-pick)
|
|
145
|
+
- T1118 — original branch-lock engine (denylist)
|
|
146
|
+
- T1587 — `completeAgentWorktreeViaMerge` (sets `CLEO_ORCHESTRATE_MERGE=1`)
|
|
147
|
+
- T1591 — this fence (boundary a/b/c/d)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary-enforcement.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/boundary-enforcement.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boundary-enforcement tests for the git-shim (T1591).
|
|
3
|
+
*
|
|
4
|
+
* Covers the four boundaries:
|
|
5
|
+
* (a) git add path inside agent worktree
|
|
6
|
+
* (b) git commit subject contains task ID
|
|
7
|
+
* (c) git merge requires CLEO_ORCHESTRATE_MERGE=1
|
|
8
|
+
* (d) git cherry-pick refuses task/T<NUM> sources
|
|
9
|
+
*
|
|
10
|
+
* Plus override (`CLEO_ALLOW_GIT=1`) + audit-log persistence.
|
|
11
|
+
*
|
|
12
|
+
* Tests are project-agnostic: fixture lives under a tmpdir using a fake
|
|
13
|
+
* project name (`fake-project-T1591`). No cleocode-specific paths.
|
|
14
|
+
*
|
|
15
|
+
* @task T1591
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
21
|
+
import { resolveAuditLogPath, writeAuditRecord } from '../audit-log.js';
|
|
22
|
+
import { validateAddPaths, validateCherryPickSource, validateCommitSubject, validateMergeAllowed, } from '../boundary.js';
|
|
23
|
+
import { extractTaskIdFromWorktreePath, isPathInsideWorktree, resolveActiveWorktree, resolveCleoWorktreesRoot, } from '../worktree-path.js';
|
|
24
|
+
let workspace;
|
|
25
|
+
let auditLogPath;
|
|
26
|
+
const ENV_KEYS = [
|
|
27
|
+
'CLEO_AGENT_ROLE',
|
|
28
|
+
'CLEO_WORKTREE_ROOT',
|
|
29
|
+
'CLEO_TASK_ID',
|
|
30
|
+
'CLEO_ALLOW_GIT',
|
|
31
|
+
'CLEO_ORCHESTRATE_MERGE',
|
|
32
|
+
'CLEO_AUDIT_LOG_PATH',
|
|
33
|
+
'XDG_DATA_HOME',
|
|
34
|
+
];
|
|
35
|
+
const savedEnv = {};
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
for (const key of ENV_KEYS)
|
|
38
|
+
savedEnv[key] = process.env[key];
|
|
39
|
+
workspace = mkdtempSync(join(tmpdir(), 'cleo-git-shim-T1591-'));
|
|
40
|
+
// Treat the workspace as a fake XDG_DATA_HOME so worktree paths land here.
|
|
41
|
+
process.env['XDG_DATA_HOME'] = workspace;
|
|
42
|
+
auditLogPath = join(workspace, 'audit', 'git-shim.jsonl');
|
|
43
|
+
process.env['CLEO_AUDIT_LOG_PATH'] = auditLogPath;
|
|
44
|
+
});
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
for (const key of ENV_KEYS) {
|
|
47
|
+
const v = savedEnv[key];
|
|
48
|
+
if (v === undefined)
|
|
49
|
+
delete process.env[key];
|
|
50
|
+
else
|
|
51
|
+
process.env[key] = v;
|
|
52
|
+
}
|
|
53
|
+
if (existsSync(workspace))
|
|
54
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// worktree-path resolution (project-agnostic)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
describe('worktree-path resolution', () => {
|
|
60
|
+
it('resolveCleoWorktreesRoot uses XDG_DATA_HOME', () => {
|
|
61
|
+
const root = resolveCleoWorktreesRoot();
|
|
62
|
+
expect(root).toBe(join(workspace, 'cleo', 'worktrees'));
|
|
63
|
+
});
|
|
64
|
+
it('extractTaskIdFromWorktreePath returns taskId from canonical layout', () => {
|
|
65
|
+
const wt = join(workspace, 'cleo', 'worktrees', 'abc123def4567890', 'T1591');
|
|
66
|
+
const taskId = extractTaskIdFromWorktreePath(wt);
|
|
67
|
+
expect(taskId).toBe('T1591');
|
|
68
|
+
});
|
|
69
|
+
it('extractTaskIdFromWorktreePath returns null outside worktree root', () => {
|
|
70
|
+
expect(extractTaskIdFromWorktreePath('/var/tmp/somewhere')).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
it('isPathInsideWorktree detects descendants', () => {
|
|
73
|
+
const wt = join(workspace, 'cleo', 'worktrees', 'p1', 'T1591');
|
|
74
|
+
expect(isPathInsideWorktree(join(wt, 'src/file.ts'), wt)).toBe(true);
|
|
75
|
+
expect(isPathInsideWorktree(wt, wt)).toBe(true);
|
|
76
|
+
expect(isPathInsideWorktree('/var/tmp/other', wt)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it('resolveActiveWorktree honours CLEO_WORKTREE_ROOT + CLEO_TASK_ID', () => {
|
|
79
|
+
const wt = join(workspace, 'cleo', 'worktrees', 'p1', 'T9999');
|
|
80
|
+
mkdirSync(wt, { recursive: true });
|
|
81
|
+
process.env['CLEO_WORKTREE_ROOT'] = wt;
|
|
82
|
+
process.env['CLEO_TASK_ID'] = 'T9999';
|
|
83
|
+
const active = resolveActiveWorktree('/some/unrelated/cwd');
|
|
84
|
+
expect(active).toEqual({ worktreePath: wt, taskId: 'T9999' });
|
|
85
|
+
});
|
|
86
|
+
it('resolveActiveWorktree walks up from cwd inside a worktree', () => {
|
|
87
|
+
const wt = join(workspace, 'cleo', 'worktrees', 'p1', 'T1591');
|
|
88
|
+
mkdirSync(join(wt, 'src'), { recursive: true });
|
|
89
|
+
const active = resolveActiveWorktree(join(wt, 'src'));
|
|
90
|
+
expect(active?.taskId).toBe('T1591');
|
|
91
|
+
expect(active?.worktreePath).toBe(wt);
|
|
92
|
+
});
|
|
93
|
+
it('resolveActiveWorktree returns null outside worktree tree', () => {
|
|
94
|
+
expect(resolveActiveWorktree('/tmp/random')).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Boundary (a) — git add path inside worktree
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
describe('boundary (a): worktree-path enforcement', () => {
|
|
101
|
+
const worktreePath = '/var/tmp/wt/T1591';
|
|
102
|
+
it('allows git add . (no explicit path)', () => {
|
|
103
|
+
expect(validateAddPaths(['.'], worktreePath, worktreePath)).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
it('allows git add inside worktree', () => {
|
|
106
|
+
expect(validateAddPaths(['src/file.ts'], worktreePath, worktreePath)).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
it('allows git add with -A flag', () => {
|
|
109
|
+
expect(validateAddPaths(['-A'], worktreePath, worktreePath)).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
it('refuses git add with absolute path outside worktree', () => {
|
|
112
|
+
const v = validateAddPaths(['/etc/passwd'], worktreePath, worktreePath);
|
|
113
|
+
expect(v).not.toBeNull();
|
|
114
|
+
expect(v?.code).toBe('E_GIT_BOUNDARY_WORKTREE_PATH');
|
|
115
|
+
expect(v?.boundary).toBe('a');
|
|
116
|
+
expect(v?.context['attempted_path']).toBe('/etc/passwd');
|
|
117
|
+
});
|
|
118
|
+
it('refuses git add with ../escape relative path', () => {
|
|
119
|
+
const v = validateAddPaths(['../../etc/passwd'], worktreePath, worktreePath);
|
|
120
|
+
expect(v).not.toBeNull();
|
|
121
|
+
expect(v?.code).toBe('E_GIT_BOUNDARY_WORKTREE_PATH');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Boundary (b) — commit subject must contain task ID
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
describe('boundary (b): commit T-ID gate', () => {
|
|
128
|
+
it('allows commit with T<NUM> in subject (no expected ID)', () => {
|
|
129
|
+
expect(validateCommitSubject(['-m', 'feat(T1591): add boundary'], null)).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
it('refuses commit without any T<NUM> token', () => {
|
|
132
|
+
const v = validateCommitSubject(['-m', 'wip: stuff'], null);
|
|
133
|
+
expect(v).not.toBeNull();
|
|
134
|
+
expect(v?.code).toBe('E_GIT_BOUNDARY_COMMIT_TASK_ID');
|
|
135
|
+
expect(v?.boundary).toBe('b');
|
|
136
|
+
});
|
|
137
|
+
it('refuses commit when expected ID is missing from subject', () => {
|
|
138
|
+
const v = validateCommitSubject(['-m', 'feat(T9999): wrong id'], 'T1591');
|
|
139
|
+
expect(v).not.toBeNull();
|
|
140
|
+
expect(v?.context['expected_task_id']).toBe('T1591');
|
|
141
|
+
});
|
|
142
|
+
it('allows commit with --message=… inline syntax', () => {
|
|
143
|
+
expect(validateCommitSubject(['--message=fix(T1591): boundary'], 'T1591')).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
it('passes through commits with no inline -m (editor flow)', () => {
|
|
146
|
+
expect(validateCommitSubject([], null)).toBeNull();
|
|
147
|
+
expect(validateCommitSubject(['--amend'], null)).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Boundary (c) — merge requires CLEO_ORCHESTRATE_MERGE
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
describe('boundary (c): merge restriction', () => {
|
|
154
|
+
it('refuses git merge without CLEO_ORCHESTRATE_MERGE', () => {
|
|
155
|
+
const v = validateMergeAllowed(['task/T1591'], {});
|
|
156
|
+
expect(v).not.toBeNull();
|
|
157
|
+
expect(v?.code).toBe('E_GIT_BOUNDARY_MERGE_FORBIDDEN');
|
|
158
|
+
expect(v?.boundary).toBe('c');
|
|
159
|
+
});
|
|
160
|
+
it('allows git merge when CLEO_ORCHESTRATE_MERGE=1', () => {
|
|
161
|
+
expect(validateMergeAllowed(['task/T1591'], { CLEO_ORCHESTRATE_MERGE: '1' })).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
it('allows control-flow flags --abort/--continue/--quit without env', () => {
|
|
164
|
+
expect(validateMergeAllowed(['--abort'], {})).toBeNull();
|
|
165
|
+
expect(validateMergeAllowed(['--continue'], {})).toBeNull();
|
|
166
|
+
expect(validateMergeAllowed(['--quit'], {})).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
it('refuses when env value is not exactly "1"', () => {
|
|
169
|
+
const v = validateMergeAllowed(['task/T1591'], { CLEO_ORCHESTRATE_MERGE: 'true' });
|
|
170
|
+
expect(v).not.toBeNull();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Boundary (d) — cherry-pick refuses task/T<NUM> sources
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
describe('boundary (d): cherry-pick from worktree branches', () => {
|
|
177
|
+
it('refuses cherry-pick of task/T1591', () => {
|
|
178
|
+
const v = validateCherryPickSource(['task/T1591']);
|
|
179
|
+
expect(v).not.toBeNull();
|
|
180
|
+
expect(v?.code).toBe('E_GIT_BOUNDARY_CHERRY_PICK_TASK_BRANCH');
|
|
181
|
+
expect(v?.boundary).toBe('d');
|
|
182
|
+
});
|
|
183
|
+
it('refuses cherry-pick of range task/T1591..HEAD', () => {
|
|
184
|
+
const v = validateCherryPickSource(['task/T1591..HEAD']);
|
|
185
|
+
expect(v).not.toBeNull();
|
|
186
|
+
expect(v?.context['source_ref']).toBe('task/T1591');
|
|
187
|
+
});
|
|
188
|
+
it('refuses cherry-pick of triple-dot range', () => {
|
|
189
|
+
const v = validateCherryPickSource(['main...task/T1591']);
|
|
190
|
+
expect(v).not.toBeNull();
|
|
191
|
+
});
|
|
192
|
+
it('allows cherry-pick of regular branch', () => {
|
|
193
|
+
expect(validateCherryPickSource(['feature/regular-branch'])).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
it('allows cherry-pick of explicit SHA', () => {
|
|
196
|
+
expect(validateCherryPickSource(['abc123def456'])).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
it('allows cherry-pick of HEAD~3', () => {
|
|
199
|
+
expect(validateCherryPickSource(['HEAD~3'])).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Audit log
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
describe('audit log', () => {
|
|
206
|
+
it('resolveAuditLogPath honours CLEO_AUDIT_LOG_PATH override', () => {
|
|
207
|
+
expect(resolveAuditLogPath()).toBe(auditLogPath);
|
|
208
|
+
});
|
|
209
|
+
it('resolveAuditLogPath defaults to XDG-conformant path', () => {
|
|
210
|
+
delete process.env['CLEO_AUDIT_LOG_PATH'];
|
|
211
|
+
expect(resolveAuditLogPath()).toBe(join(workspace, 'cleo', 'audit', 'git-shim.jsonl'));
|
|
212
|
+
});
|
|
213
|
+
it('writeAuditRecord persists a JSONL entry', () => {
|
|
214
|
+
const record = {
|
|
215
|
+
ts: '2026-04-29T00:00:00.000Z',
|
|
216
|
+
outcome: 'blocked',
|
|
217
|
+
boundary: 'a',
|
|
218
|
+
code: 'E_GIT_BOUNDARY_WORKTREE_PATH',
|
|
219
|
+
subcommand: 'add',
|
|
220
|
+
args: ['/etc/passwd'],
|
|
221
|
+
cwd: '/var/tmp/wt/T1591',
|
|
222
|
+
worktree_path: '/var/tmp/wt/T1591',
|
|
223
|
+
task_id: 'T1591',
|
|
224
|
+
role: 'worker',
|
|
225
|
+
context: { attempted_path: '/etc/passwd' },
|
|
226
|
+
};
|
|
227
|
+
writeAuditRecord(record);
|
|
228
|
+
expect(existsSync(auditLogPath)).toBe(true);
|
|
229
|
+
const content = readFileSync(auditLogPath, 'utf-8').trim();
|
|
230
|
+
const parsed = JSON.parse(content);
|
|
231
|
+
expect(parsed.code).toBe('E_GIT_BOUNDARY_WORKTREE_PATH');
|
|
232
|
+
expect(parsed.outcome).toBe('blocked');
|
|
233
|
+
expect(parsed.boundary).toBe('a');
|
|
234
|
+
});
|
|
235
|
+
it('writeAuditRecord appends rather than overwrites', () => {
|
|
236
|
+
const base = {
|
|
237
|
+
ts: '2026-04-29T00:00:00.000Z',
|
|
238
|
+
outcome: 'blocked',
|
|
239
|
+
boundary: 'b',
|
|
240
|
+
code: 'E_GIT_BOUNDARY_COMMIT_TASK_ID',
|
|
241
|
+
subcommand: 'commit',
|
|
242
|
+
args: ['-m', 'wip'],
|
|
243
|
+
cwd: '/var/tmp/wt/T1591',
|
|
244
|
+
worktree_path: null,
|
|
245
|
+
task_id: null,
|
|
246
|
+
role: 'worker',
|
|
247
|
+
context: {},
|
|
248
|
+
};
|
|
249
|
+
writeAuditRecord(base);
|
|
250
|
+
writeAuditRecord({ ...base, outcome: 'bypassed-allow-git' });
|
|
251
|
+
const lines = readFileSync(auditLogPath, 'utf-8').trim().split('\n');
|
|
252
|
+
expect(lines).toHaveLength(2);
|
|
253
|
+
expect(JSON.parse(lines[0] ?? '{}').outcome).toBe('blocked');
|
|
254
|
+
expect(JSON.parse(lines[1] ?? '{}').outcome).toBe('bypassed-allow-git');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// Project-agnostic verification (no cleocode-specific paths)
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
describe('project-agnostic verification', () => {
|
|
261
|
+
it('worktree path uses a fake project name without hardcoding "cleocode"', () => {
|
|
262
|
+
// Resolve a worktree path for a fake project at /home/user/projects/fake-app.
|
|
263
|
+
// The sha256-derived hash should never contain "cleocode".
|
|
264
|
+
const fakeProjectRoot = '/home/user/projects/fake-app';
|
|
265
|
+
const wt = join(workspace, 'cleo', 'worktrees', 'someProjectHash', 'T1591');
|
|
266
|
+
mkdirSync(wt, { recursive: true });
|
|
267
|
+
process.env['CLEO_WORKTREE_ROOT'] = wt;
|
|
268
|
+
process.env['CLEO_TASK_ID'] = 'T1591';
|
|
269
|
+
const active = resolveActiveWorktree('/totally/elsewhere');
|
|
270
|
+
expect(active?.worktreePath).toBe(wt);
|
|
271
|
+
expect(active?.worktreePath.includes('cleocode')).toBe(false);
|
|
272
|
+
// Reference to fakeProjectRoot to keep the test self-documenting.
|
|
273
|
+
expect(fakeProjectRoot.includes('cleocode')).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
it('audit log path uses XDG conventions, not project name', () => {
|
|
276
|
+
delete process.env['CLEO_AUDIT_LOG_PATH'];
|
|
277
|
+
const path = resolveAuditLogPath();
|
|
278
|
+
expect(path.includes('cleocode')).toBe(false);
|
|
279
|
+
expect(path.endsWith(join('cleo', 'audit', 'git-shim.jsonl'))).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
//# sourceMappingURL=boundary-enforcement.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary-enforcement.test.js","sourceRoot":"","sources":["../../src/__tests__/boundary-enforcement.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcome of a shim invocation that warrants an audit entry.
|
|
3
|
+
*
|
|
4
|
+
* @task T1591
|
|
5
|
+
*/
|
|
6
|
+
export type AuditOutcome = 'blocked' | 'bypassed-allow-git' | 'bypassed-orchestrate-merge';
|
|
7
|
+
/**
|
|
8
|
+
* Single JSONL record written to the audit log.
|
|
9
|
+
*
|
|
10
|
+
* @task T1591
|
|
11
|
+
*/
|
|
12
|
+
export interface AuditRecord {
|
|
13
|
+
/** ISO 8601 UTC timestamp. */
|
|
14
|
+
ts: string;
|
|
15
|
+
/** What happened. */
|
|
16
|
+
outcome: AuditOutcome;
|
|
17
|
+
/** Boundary letter (a-d) or "denylist" for legacy denylist hits. */
|
|
18
|
+
boundary: 'a' | 'b' | 'c' | 'd' | 'denylist';
|
|
19
|
+
/** CLEO error code, when blocked. */
|
|
20
|
+
code: string;
|
|
21
|
+
/** Git subcommand. */
|
|
22
|
+
subcommand: string;
|
|
23
|
+
/** Argv tail after subcommand. */
|
|
24
|
+
args: string[];
|
|
25
|
+
/** Working directory at invocation time. */
|
|
26
|
+
cwd: string;
|
|
27
|
+
/** Active worktree, when resolvable. */
|
|
28
|
+
worktree_path: string | null;
|
|
29
|
+
/** Task ID extracted from the worktree, when resolvable. */
|
|
30
|
+
task_id: string | null;
|
|
31
|
+
/** Agent role from CLEO_AGENT_ROLE. */
|
|
32
|
+
role: string | null;
|
|
33
|
+
/** Free-form context from the boundary predicate. */
|
|
34
|
+
context: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the audit log file path.
|
|
38
|
+
*
|
|
39
|
+
* Honours `CLEO_AUDIT_LOG_PATH` (test/owner override), otherwise defaults
|
|
40
|
+
* to the XDG-conformant location.
|
|
41
|
+
*
|
|
42
|
+
* @returns Absolute path to the jsonl file.
|
|
43
|
+
*
|
|
44
|
+
* @task T1591
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveAuditLogPath(): string;
|
|
47
|
+
/**
|
|
48
|
+
* Append a single record to the audit log.
|
|
49
|
+
*
|
|
50
|
+
* Best-effort: failures are swallowed so the shim can never wedge a git
|
|
51
|
+
* invocation. The record is also echoed to stderr so operators see it
|
|
52
|
+
* even if the file write fails.
|
|
53
|
+
*
|
|
54
|
+
* @param record - The audit record to persist.
|
|
55
|
+
*
|
|
56
|
+
* @task T1591
|
|
57
|
+
*/
|
|
58
|
+
export declare function writeAuditRecord(record: AuditRecord): void;
|
|
59
|
+
//# sourceMappingURL=audit-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-log.d.ts","sourceRoot":"","sources":["../src/audit-log.ts"],"names":[],"mappings":"AAoBA;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,oBAAoB,GAAG,4BAA4B,CAAC;AAE3F;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,qBAAqB;IACrB,OAAO,EAAE,YAAY,CAAC;IACtB,oEAAoE;IACpE,QAAQ,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,UAAU,CAAC;IAC7C,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,sBAAsB;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,4DAA4D;IAC5D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,uCAAuC;IACvC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAK5C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAmB1D"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only audit log for git-shim boundary events (T1591).
|
|
3
|
+
*
|
|
4
|
+
* Every block (refusal) and every bypass (`CLEO_ALLOW_GIT=1`) is recorded as
|
|
5
|
+
* one JSON line at:
|
|
6
|
+
*
|
|
7
|
+
* `<XDG_DATA_HOME ?? ~/.local/share>/cleo/audit/git-shim.jsonl`
|
|
8
|
+
*
|
|
9
|
+
* Tests override the path via `CLEO_AUDIT_LOG_PATH`.
|
|
10
|
+
*
|
|
11
|
+
* Records use a stable, project-agnostic shape so downstream tooling
|
|
12
|
+
* (`cleo audit show`, dashboards) can consume them without parsing knowledge
|
|
13
|
+
* of any particular project.
|
|
14
|
+
*
|
|
15
|
+
* @task T1591
|
|
16
|
+
*/
|
|
17
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the audit log file path.
|
|
22
|
+
*
|
|
23
|
+
* Honours `CLEO_AUDIT_LOG_PATH` (test/owner override), otherwise defaults
|
|
24
|
+
* to the XDG-conformant location.
|
|
25
|
+
*
|
|
26
|
+
* @returns Absolute path to the jsonl file.
|
|
27
|
+
*
|
|
28
|
+
* @task T1591
|
|
29
|
+
*/
|
|
30
|
+
export function resolveAuditLogPath() {
|
|
31
|
+
const override = process.env['CLEO_AUDIT_LOG_PATH'];
|
|
32
|
+
if (override)
|
|
33
|
+
return override;
|
|
34
|
+
const xdgData = process.env['XDG_DATA_HOME'] ?? join(homedir(), '.local', 'share');
|
|
35
|
+
return join(xdgData, 'cleo', 'audit', 'git-shim.jsonl');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Append a single record to the audit log.
|
|
39
|
+
*
|
|
40
|
+
* Best-effort: failures are swallowed so the shim can never wedge a git
|
|
41
|
+
* invocation. The record is also echoed to stderr so operators see it
|
|
42
|
+
* even if the file write fails.
|
|
43
|
+
*
|
|
44
|
+
* @param record - The audit record to persist.
|
|
45
|
+
*
|
|
46
|
+
* @task T1591
|
|
47
|
+
*/
|
|
48
|
+
export function writeAuditRecord(record) {
|
|
49
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
50
|
+
const path = resolveAuditLogPath();
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
53
|
+
appendFileSync(path, line, { encoding: 'utf-8' });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Best-effort — shim must never block on log failure.
|
|
57
|
+
}
|
|
58
|
+
// Always echo to stderr for human visibility.
|
|
59
|
+
if (record.outcome === 'blocked') {
|
|
60
|
+
process.stderr.write(`[git-shim] AUDIT block boundary=${record.boundary} code=${record.code}\n`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
process.stderr.write(`[git-shim] AUDIT bypass boundary=${record.boundary} via=${record.outcome}\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=audit-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-log.js","sourceRoot":"","sources":["../src/audit-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAuC1C;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IACpD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACnF,OAAO,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAmB;IAClD,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC;IAC3C,MAAM,IAAI,GAAG,mBAAmB,EAAE,CAAC;IACnC,IAAI,CAAC;QACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,sDAAsD;IACxD,CAAC;IACD,8CAA8C;IAC9C,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,mCAAmC,MAAM,CAAC,QAAQ,SAAS,MAAM,CAAC,IAAI,IAAI,CAC3E,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,oCAAoC,MAAM,CAAC,QAAQ,QAAQ,MAAM,CAAC,OAAO,IAAI,CAC9E,CAAC;IACJ,CAAC;AACH,CAAC"}
|