@vibe-commit/capture 0.1.0
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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/agents/claude_code.d.ts +11 -0
- package/dist/agents/claude_code.js +162 -0
- package/dist/agents/claude_code.js.map +1 -0
- package/dist/agents/codex_cli.d.ts +15 -0
- package/dist/agents/codex_cli.js +67 -0
- package/dist/agents/codex_cli.js.map +1 -0
- package/dist/agents/cursor.d.ts +21 -0
- package/dist/agents/cursor.js +89 -0
- package/dist/agents/cursor.js.map +1 -0
- package/dist/agents/registry.d.ts +184 -0
- package/dist/agents/registry.js +212 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/agents/types.d.ts +279 -0
- package/dist/agents/types.js +67 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/commands/connect.d.ts +36 -0
- package/dist/commands/connect.js +499 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/context.d.ts +45 -0
- package/dist/commands/context.js +14 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/report.d.ts +105 -0
- package/dist/commands/report.js +430 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/status.d.ts +29 -0
- package/dist/commands/status.js +236 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/why.d.ts +349 -0
- package/dist/commands/why.js +937 -0
- package/dist/commands/why.js.map +1 -0
- package/dist/consent.d.ts +59 -0
- package/dist/consent.js +111 -0
- package/dist/consent.js.map +1 -0
- package/dist/copy/claims.d.ts +79 -0
- package/dist/copy/claims.js +343 -0
- package/dist/copy/claims.js.map +1 -0
- package/dist/copy/grades.d.ts +117 -0
- package/dist/copy/grades.js +96 -0
- package/dist/copy/grades.js.map +1 -0
- package/dist/copy/index.d.ts +12 -0
- package/dist/copy/index.js +13 -0
- package/dist/copy/index.js.map +1 -0
- package/dist/copy/strings.d.ts +1328 -0
- package/dist/copy/strings.js +1243 -0
- package/dist/copy/strings.js.map +1 -0
- package/dist/copy/time.d.ts +27 -0
- package/dist/copy/time.js +43 -0
- package/dist/copy/time.js.map +1 -0
- package/dist/credential.d.ts +211 -0
- package/dist/credential.js +298 -0
- package/dist/credential.js.map +1 -0
- package/dist/exit.d.ts +59 -0
- package/dist/exit.js +60 -0
- package/dist/exit.js.map +1 -0
- package/dist/git.d.ts +299 -0
- package/dist/git.js +653 -0
- package/dist/git.js.map +1 -0
- package/dist/hooks/entry.d.ts +277 -0
- package/dist/hooks/entry.js +1092 -0
- package/dist/hooks/entry.js.map +1 -0
- package/dist/hooks/post_commit.d.ts +59 -0
- package/dist/hooks/post_commit.js +160 -0
- package/dist/hooks/post_commit.js.map +1 -0
- package/dist/hooks/post_rewrite.d.ts +59 -0
- package/dist/hooks/post_rewrite.js +161 -0
- package/dist/hooks/post_rewrite.js.map +1 -0
- package/dist/hooks/session_end.d.ts +101 -0
- package/dist/hooks/session_end.js +120 -0
- package/dist/hooks/session_end.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +360 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +153 -0
- package/dist/install.js +284 -0
- package/dist/install.js.map +1 -0
- package/dist/json.d.ts +100 -0
- package/dist/json.js +94 -0
- package/dist/json.js.map +1 -0
- package/dist/oauth/discovery.d.ts +96 -0
- package/dist/oauth/discovery.js +163 -0
- package/dist/oauth/discovery.js.map +1 -0
- package/dist/oauth/lock.d.ts +100 -0
- package/dist/oauth/lock.js +287 -0
- package/dist/oauth/lock.js.map +1 -0
- package/dist/oauth/loopback.d.ts +85 -0
- package/dist/oauth/loopback.js +124 -0
- package/dist/oauth/loopback.js.map +1 -0
- package/dist/oauth/pkce.d.ts +58 -0
- package/dist/oauth/pkce.js +77 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/read.d.ts +120 -0
- package/dist/oauth/read.js +144 -0
- package/dist/oauth/read.js.map +1 -0
- package/dist/oauth/session.d.ts +80 -0
- package/dist/oauth/session.js +112 -0
- package/dist/oauth/session.js.map +1 -0
- package/dist/oauth/signin.d.ts +178 -0
- package/dist/oauth/signin.js +225 -0
- package/dist/oauth/signin.js.map +1 -0
- package/dist/oauth/token.d.ts +139 -0
- package/dist/oauth/token.js +183 -0
- package/dist/oauth/token.js.map +1 -0
- package/dist/paths.d.ts +366 -0
- package/dist/paths.js +460 -0
- package/dist/paths.js.map +1 -0
- package/dist/policy.d.ts +195 -0
- package/dist/policy.js +244 -0
- package/dist/policy.js.map +1 -0
- package/dist/post.d.ts +302 -0
- package/dist/post.js +384 -0
- package/dist/post.js.map +1 -0
- package/dist/project.d.ts +99 -0
- package/dist/project.js +192 -0
- package/dist/project.js.map +1 -0
- package/dist/redact.d.ts +175 -0
- package/dist/redact.js +700 -0
- package/dist/redact.js.map +1 -0
- package/dist/runtime.d.ts +30 -0
- package/dist/runtime.js +37 -0
- package/dist/runtime.js.map +1 -0
- package/dist/spawn_budget.d.ts +101 -0
- package/dist/spawn_budget.js +124 -0
- package/dist/spawn_budget.js.map +1 -0
- package/dist/spool.d.ts +390 -0
- package/dist/spool.js +556 -0
- package/dist/spool.js.map +1 -0
- package/dist/state.d.ts +109 -0
- package/dist/state.js +217 -0
- package/dist/state.js.map +1 -0
- package/dist/system_message.d.ts +101 -0
- package/dist/system_message.js +141 -0
- package/dist/system_message.js.map +1 -0
- package/dist/term.d.ts +112 -0
- package/dist/term.js +134 -0
- package/dist/term.js.map +1 -0
- package/dist/version.d.ts +16 -0
- package/dist/version.js +17 -0
- package/dist/version.js.map +1 -0
- package/package.json +50 -0
package/dist/project.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What "this repository" means — ⛔ **TWO answers, and D184 splits them.**
|
|
3
|
+
*
|
|
4
|
+
* One value served four roles until `D184`. It still serves three of them; the
|
|
5
|
+
* fourth — **the consent key** — moves, and `resolveProjectKeys` is where the two
|
|
6
|
+
* surviving answers are produced together so a caller cannot take the wrong one
|
|
7
|
+
* by accident:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* resolveProjectKeys(cwd) ──┬──> consent gate = canonicalised git COMMON DIR [MOVES]
|
|
11
|
+
* ├──> session bucket = worktree TOPLEVEL (D58/CR-017d)
|
|
12
|
+
* ├──> repoSlug input = worktree TOPLEVEL
|
|
13
|
+
* └──> redaction root = NOT FROM HERE. A SET, and the
|
|
14
|
+
* common dir is never in it (D184 §3).
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ⛔ **The redaction confinement root is NOT one of the values this module
|
|
18
|
+
* returns**, and that omission is the decision. `D184 §3`: if the confinement
|
|
19
|
+
* root follows the consent key to the common dir, every file of an external
|
|
20
|
+
* worktree is judged outside the main clone and `isInside` fail-closes each one
|
|
21
|
+
* into a redaction marker. Capture succeeds, turn counts look right, the content
|
|
22
|
+
* is gone. Widening it is `T2`'s work in `redact.ts`, against a root SET.
|
|
23
|
+
*
|
|
24
|
+
* ## ⛔ Why the consent key had to move at all
|
|
25
|
+
*
|
|
26
|
+
* Consent was keyed on `--show-toplevel`, which **differs per worktree**, with no
|
|
27
|
+
* wildcards and absence-means-no. So an agent session inside a `git worktree`
|
|
28
|
+
* captured **nothing** — no conversation, no commit, no notice — and `D19` makes
|
|
29
|
+
* worktree-per-builder the house convention. `M2`: `rev-parse --git-path hooks`
|
|
30
|
+
* in a linked worktree returns the MAIN clone's `.git/hooks`, so one install
|
|
31
|
+
* already covers every worktree. The common dir is the identity the install
|
|
32
|
+
* already had.
|
|
33
|
+
*
|
|
34
|
+
* ⚠ Deliberately NOT `src/git.ts`. `CR-019` owns that file (deriving the repo slug
|
|
35
|
+
* for `X-Repo-Slug`) and would collide. This module holds only what the consent
|
|
36
|
+
* gate needs; `CR-019` may fold it in when it lands.
|
|
37
|
+
*/
|
|
38
|
+
import { execFileSync } from "node:child_process";
|
|
39
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
40
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
41
|
+
import { spawnTimeoutMs } from "./spawn_budget.js";
|
|
42
|
+
/** This spawn's own ceiling, used when no hook budget is armed. */
|
|
43
|
+
const TOPLEVEL_TIMEOUT_MS = 5_000;
|
|
44
|
+
/**
|
|
45
|
+
* One `rev-parse` answer, or null.
|
|
46
|
+
*
|
|
47
|
+
* ⛔ **ONE ARGUMENT PER INVOCATION, and `resolveProjectKeys` therefore spawns
|
|
48
|
+
* TWICE rather than once.** Prior learning `git-rev-parse-combined-args-fail-together`,
|
|
49
|
+
* reproduced 10/10: *when the unresolvable arg is 40 hex, `rev-parse` exits 0 and
|
|
50
|
+
* echoes it back verbatim as if it were an answer.* Reproduced again here on
|
|
51
|
+
* 2026-08-21 — `rev-parse --show-toplevel <40×a>` prints the toplevel, then
|
|
52
|
+
* `aaaa…`, and exits 0. A combined invocation therefore cannot be validated by
|
|
53
|
+
* WHICH LINE the answer landed on, because a garbage answer occupies a line too.
|
|
54
|
+
*
|
|
55
|
+
* ⚠ **The cost is +1 spawn at hook entry, outside the 8-file loop**, and it is
|
|
56
|
+
* accepted on the record (`D184 §6`). `hooks/post_commit.ts` arms no spawn
|
|
57
|
+
* budget, deliberately, so this path is unaffected there.
|
|
58
|
+
*
|
|
59
|
+
* `execFileSync` with an argument array, never a shell string: a repository path
|
|
60
|
+
* containing a space or a `;` is ordinary on a developer's machine and would be
|
|
61
|
+
* a command-injection sink through `execSync`.
|
|
62
|
+
*/
|
|
63
|
+
function revParse(dir, arg) {
|
|
64
|
+
const timeout = spawnTimeoutMs(TOPLEVEL_TIMEOUT_MS);
|
|
65
|
+
if (timeout === null)
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
const out = execFileSync("git", ["-C", dir, "rev-parse", arg], {
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
71
|
+
timeout,
|
|
72
|
+
});
|
|
73
|
+
const value = out.trim();
|
|
74
|
+
return value === "" ? null : value;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The git toplevel for `dir`, or null when it is not inside a work tree.
|
|
82
|
+
*
|
|
83
|
+
* ⚠ **The timeout DERIVES from the hook's remaining budget** (`TODOS[87]`).
|
|
84
|
+
* `execFileSync` blocks the event loop, so the watchdog cannot fire while this
|
|
85
|
+
* call is outstanding — a fixed 5s ceiling here is 5s the watchdog provably
|
|
86
|
+
* cannot cover, and it is one of four such ceilings that summed to 20s against a
|
|
87
|
+
* 5s budget. `null` means the budget is spent, and the answer is the one this
|
|
88
|
+
* function already gives when git cannot tell us: no toplevel, so no consent,
|
|
89
|
+
* so the hook does nothing. Silent by contract and correct by default.
|
|
90
|
+
*
|
|
91
|
+
* ⛔ **The answer is validated by CONTENT: `--show-toplevel` is absolute, always.**
|
|
92
|
+
* Without that gate a `git` on `PATH` that echoed a 40-hex string back would have
|
|
93
|
+
* it `resolve`d against `process.cwd()` and returned as a toplevel — accepted
|
|
94
|
+
* because it arrived where an answer was expected. Fail closed instead.
|
|
95
|
+
*/
|
|
96
|
+
export function gitToplevel(dir) {
|
|
97
|
+
const top = revParse(dir, "--show-toplevel");
|
|
98
|
+
if (top === null || !isAbsolute(top))
|
|
99
|
+
return null;
|
|
100
|
+
return resolve(top);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* ⛔ **THE CONSENT KEY — the git common dir, canonicalised before it is ever a key.**
|
|
104
|
+
*
|
|
105
|
+
* ## `M3`: keying this naively is a SECURITY REGRESSION, not a formatting bug
|
|
106
|
+
*
|
|
107
|
+
* `--git-common-dir` is **`.git`, RELATIVE**, in a main clone, and an absolute
|
|
108
|
+
* path in a linked worktree. Measured 2026-08-21, four shapes:
|
|
109
|
+
*
|
|
110
|
+
* | shape | `--git-common-dir` |
|
|
111
|
+
* |---|---|
|
|
112
|
+
* | main clone, `-C` the toplevel | `.git` |
|
|
113
|
+
* | main clone, `-C` a subdirectory two deep | `../../.git` |
|
|
114
|
+
* | linked worktree | `<main>/.git`, absolute |
|
|
115
|
+
* | worktree of a BARE repo | `<repos>/project.git`, absolute |
|
|
116
|
+
*
|
|
117
|
+
* ⛔ Keyed on the raw string, **every main clone on the machine gets the key
|
|
118
|
+
* `.git`** — consent to one repo would grant capture on all of them.
|
|
119
|
+
*
|
|
120
|
+
* ⛔ **And the base for that resolution is `dir`, NOT the toplevel.** Row 2 is
|
|
121
|
+
* why, and it is the same hazard one layer over: `resolve(toplevel, "../../.git")`
|
|
122
|
+
* for a repo at `…/scratch/repo` is `…/.git` — a key SHARED by every sibling repo
|
|
123
|
+
* under that parent. Git's answer is relative to git's cwd, which `-C dir` makes
|
|
124
|
+
* `dir`. `D184 §2` says "resolve against the toplevel"; measured, that is wrong
|
|
125
|
+
* for the subdirectory case and this resolves against `dir`.
|
|
126
|
+
*
|
|
127
|
+
* ⚠ **`realpath` before it is a key**, so `/tmp/x` and `/private/tmp/x` are not
|
|
128
|
+
* two grants for one repo, and so an unresolvable answer fails closed.
|
|
129
|
+
*
|
|
130
|
+
* ## The content check, which is what makes this not "trust the line"
|
|
131
|
+
*
|
|
132
|
+
* A 40-hex string is legal syntax for a RELATIVE common dir, so absoluteness
|
|
133
|
+
* cannot reject it here the way it does for the toplevel. The answer must be a
|
|
134
|
+
* directory that actually holds a `HEAD` — present in all four measured shapes
|
|
135
|
+
* above, and absent from any directory that merely happens to exist under `dir`.
|
|
136
|
+
* ⚠ Existence alone would be a check the fixture can satisfy by accident
|
|
137
|
+
* (`project_redaction_fixture_must_exist_on_disk` is the same shape of trap).
|
|
138
|
+
*/
|
|
139
|
+
function gitConsentKey(dir) {
|
|
140
|
+
const raw = revParse(dir, "--git-common-dir");
|
|
141
|
+
if (raw === null)
|
|
142
|
+
return null;
|
|
143
|
+
let canonical;
|
|
144
|
+
try {
|
|
145
|
+
canonical = realpathSync(resolve(dir, raw));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return existsSync(join(canonical, "HEAD")) ? canonical : null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Both keys for a directory, or **null** — ⛔ **fail closed, never a fallback.**
|
|
154
|
+
*
|
|
155
|
+
* If either spawn fails or either answer fails its content check, there is no
|
|
156
|
+
* partial answer: a guessed consent key grants capture on a repository the user
|
|
157
|
+
* never approved, and a guessed worktree key files a session under the wrong
|
|
158
|
+
* repository permanently.
|
|
159
|
+
*
|
|
160
|
+
* ⚠ **NO MIGRATION PATH FOR EXISTING `projects.json` ENTRIES, deliberately.**
|
|
161
|
+
* The allow list is keyed by string and the key changes shape, so a pre-existing
|
|
162
|
+
* grant at a toplevel stops matching. Pre-launch there are zero users and zero
|
|
163
|
+
* rows (`project_pre_launch_no_data`), so the migration is a no-op with nothing
|
|
164
|
+
* to migrate — writing one would be speculative code guarding a case that cannot
|
|
165
|
+
* occur. The observable cost if that assumption were ever wrong is one re-run of
|
|
166
|
+
* `vibecommit connect`, not a silent grant.
|
|
167
|
+
*/
|
|
168
|
+
export function resolveProjectKeys(dir) {
|
|
169
|
+
const worktree = gitToplevel(dir);
|
|
170
|
+
if (worktree === null)
|
|
171
|
+
return null;
|
|
172
|
+
const consent = gitConsentKey(dir);
|
|
173
|
+
if (consent === null)
|
|
174
|
+
return null;
|
|
175
|
+
return { worktree, consent };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The worktree toplevel for a directory: the session bucket and slug input.
|
|
179
|
+
*
|
|
180
|
+
* ⛔ **NO LONGER THE CONSENT KEY — `D184` moved that to
|
|
181
|
+
* `resolveProjectKeys(dir).consent`.** The name is kept because the three roles
|
|
182
|
+
* it still serves are unchanged and every caller of it wants the toplevel; a
|
|
183
|
+
* rename would be churn across files this change has no other reason to touch.
|
|
184
|
+
*
|
|
185
|
+
* Falls back to NOTHING. A directory outside a work tree has no project identity,
|
|
186
|
+
* and inventing one from `cwd` would file a session under a key the user cannot
|
|
187
|
+
* see in `vibecommit status`.
|
|
188
|
+
*/
|
|
189
|
+
export function resolveProjectKey(dir) {
|
|
190
|
+
return gitToplevel(dir);
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEtD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,mEAAmE;AACnE,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAElC;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAS,QAAQ,CAAC,GAAW,EAAE,GAAW;IACxC,MAAM,OAAO,GAAG,cAAc,CAAC,mBAAmB,CAAC,CAAC;IACpD,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE;YAC7D,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;YACnC,OAAO;SACR,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACzB,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IAC7C,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,SAAS,aAAa,CAAC,GAAW;IAChC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;IAC9C,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,SAAiB,CAAC;IACtB,IAAI,CAAC;QACH,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;AAChE,CAAC;AAkBD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC"}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Out-of-tree redaction — `CR-024d`, D57 §OV8d.
|
|
3
|
+
*
|
|
4
|
+
* A Claude Code transcript records structured file reads and writes VERBATIM:
|
|
5
|
+
* a `Read`, `Edit` or `Write` record carries a path and the file's content. When
|
|
6
|
+
* that path is outside the repository the user consented to, the content of a
|
|
7
|
+
* THIRD PARTY'S file is about to be uploaded under the user's credential and
|
|
8
|
+
* attached to a repo that has nothing to do with it. That is a confidentiality
|
|
9
|
+
* boundary, not a tidiness issue, and it is the whole reason this module exists.
|
|
10
|
+
*
|
|
11
|
+
* ## Why the client, permanently
|
|
12
|
+
*
|
|
13
|
+
* D57 §OV8d: *"Out-of-tree redaction is client-side. Server-side redaction would
|
|
14
|
+
* break `turn_hash == sha256(content)`, so it cannot live on the server at
|
|
15
|
+
* all."* A server that rewrote content after hashing would hold a hash chain
|
|
16
|
+
* over bytes it no longer has. There is no server-side variant to fall back on.
|
|
17
|
+
*
|
|
18
|
+
* ## The deviation this is allowed to make, and its limit
|
|
19
|
+
*
|
|
20
|
+
* D60 §D6 says the client holds no ANALYSIS, and OV8d is the ONE recorded
|
|
21
|
+
* deviation from it, because redacting requires parsing each line. The rule that
|
|
22
|
+
* survives is *"the client may render; it may not analyze, hash, or
|
|
23
|
+
* canonicalize."* So this module parses a line, finds a path, compares it to the
|
|
24
|
+
* project root, and replaces a span. It extracts no turns, computes no digest,
|
|
25
|
+
* canonicalises no record, and imports nothing from `vibecommit-mcp` — that last
|
|
26
|
+
* one is a LICENSING violation, not a style one (D60 §D1a, D64).
|
|
27
|
+
*
|
|
28
|
+
* ## ⚠ LENGTH PRESERVATION IS THE LOAD-BEARING PROPERTY
|
|
29
|
+
*
|
|
30
|
+
* The client sends a raw byte span with `X-Byte-Offset: from`, and the server
|
|
31
|
+
* advances its watermark by the DECOMPRESSED body's length
|
|
32
|
+
* (`vibecommit-mcp/src/conversation/ingest_session.ts`). The client's own
|
|
33
|
+
* `sentOffset` advances to `to`. If a redaction makes the body SHORTER, the two
|
|
34
|
+
* watermarks diverge and every subsequent delta arrives flagged `gap: true`,
|
|
35
|
+
* forever; longer, and every delta is `overlap: true`. A permanently
|
|
36
|
+
* gap-flagged stream is a capture-health surface reporting data loss that is
|
|
37
|
+
* not happening.
|
|
38
|
+
*
|
|
39
|
+
* So every replacement is padded to the exact byte length it replaced, and a
|
|
40
|
+
* span too short to hold even the shortest honest marker is LEFT ALONE. That
|
|
41
|
+
* trade — leak a sub-13-byte span rather than break the accounting — is the
|
|
42
|
+
* brief's, stated rather than smuggled, and `redactSpan` counts what it skipped
|
|
43
|
+
* so the number is reportable.
|
|
44
|
+
*
|
|
45
|
+
* ## Why re-serialising is safe here, measured rather than assumed
|
|
46
|
+
*
|
|
47
|
+
* `JSON.parse` → `JSON.stringify` was byte-identical on **39,910 of 39,910**
|
|
48
|
+
* real transcript lines across 25 sessions, because the transcript is written by
|
|
49
|
+
* `JSON.stringify` in the first place. That is what makes the padding
|
|
50
|
+
* arithmetic exact instead of approximate. It is not taken on faith: every line
|
|
51
|
+
* is round-tripped and compared BEFORE anything is changed, and a line that does
|
|
52
|
+
* not round-trip is returned untouched.
|
|
53
|
+
*
|
|
54
|
+
* @provenance vibecommit-mcp src/conversation/ingest_session.ts — offset accounting, read
|
|
55
|
+
*/
|
|
56
|
+
import { Buffer } from "node:buffer";
|
|
57
|
+
import type { RedactionProfile } from "./agents/types.js";
|
|
58
|
+
/**
|
|
59
|
+
* The DATA one profile supplies. **The algorithm above does not move.**
|
|
60
|
+
*
|
|
61
|
+
* `CR-185`, D164 §5. Everything below `walk` is the rule this module already
|
|
62
|
+
* had — *within one object, a path key naming a file outside the project root
|
|
63
|
+
* makes that object's content-bearing siblings third-party content, and the
|
|
64
|
+
* verdict inherits down the subtree* — and each agent supplies which keys play
|
|
65
|
+
* which part. A second agent is a second row in the table below, not a branch.
|
|
66
|
+
*/
|
|
67
|
+
export interface RedactionRules {
|
|
68
|
+
/** Keys whose STRING value is a filesystem path. The SELECTOR, never redacted. */
|
|
69
|
+
readonly pathKeys: ReadonlySet<string>;
|
|
70
|
+
/** Keys whose value is content, withheld when a path in scope is out of tree. */
|
|
71
|
+
readonly contentKeys: ReadonlySet<string>;
|
|
72
|
+
/**
|
|
73
|
+
* Keys whose STRING value is itself a JSON document to descend into.
|
|
74
|
+
*
|
|
75
|
+
* ⚠ **Empty for Claude Code, and that is not an oversight** — a Claude record
|
|
76
|
+
* carries its tool input as a real nested OBJECT, so `walk` already reaches
|
|
77
|
+
* it. Codex hands the whole argument list over as one escaped string.
|
|
78
|
+
*/
|
|
79
|
+
readonly nestedJsonKeys: ReadonlySet<string>;
|
|
80
|
+
/** Keys holding `[{ lines: [...] }]` hunk arrays whose lines are content. */
|
|
81
|
+
readonly patchKeys: ReadonlySet<string>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The rules a profile LABEL names.
|
|
85
|
+
*
|
|
86
|
+
* For a caller that already knows the dialect — `test/` does, and nothing in
|
|
87
|
+
* `src/` should, because ⛔ **the profile is selected by the containing root and
|
|
88
|
+
* not by anything the caller believes about the agent** (see
|
|
89
|
+
* `rulesForTranscript`).
|
|
90
|
+
*/
|
|
91
|
+
export declare function rulesFor(profile: RedactionProfile): RedactionRules;
|
|
92
|
+
/**
|
|
93
|
+
* ⛔ **THE PROFILE IS SELECTED BY WHICH REGISTRY ROOT CONTAINS THE RESOLVED
|
|
94
|
+
* TRANSCRIPT PATH — NEVER BY THE `--agent=` FLAG (D164 §5).**
|
|
95
|
+
*
|
|
96
|
+
* The flag stays the BEHAVIOUR selector (roots to search, events, budgets,
|
|
97
|
+
* delegated discovery). The scrubber stops depending on anyone remembering to
|
|
98
|
+
* set it — which is the whole defect: both no-flag install paths (the
|
|
99
|
+
* `.claude-plugin` bundle whose command carries no `--agent=`, and a
|
|
100
|
+
* hand-written `.codex/` config) resolved to `claude-code`, and a Codex record
|
|
101
|
+
* went through Claude's key set and was redacted NOT AT ALL.
|
|
102
|
+
*
|
|
103
|
+
* ## ⛔ This does NOT reopen `CR-183`'s `transcript_path`-as-selector rejection
|
|
104
|
+
*
|
|
105
|
+
* That rejection is about the **confinement boundary**, which is computed from
|
|
106
|
+
* the registry and is never derived from input — and it still is. The
|
|
107
|
+
* difference is the direction of the failure:
|
|
108
|
+
*
|
|
109
|
+
* - choosing a **stricter scrubber** because a path sits outside every known
|
|
110
|
+
* root is **fail-closed**;
|
|
111
|
+
* - choosing a **wider root** because a path *claims* to be there is not.
|
|
112
|
+
*
|
|
113
|
+
* A wrong answer here costs a suboptimal profile and can never make a file
|
|
114
|
+
* readable that was not readable before. `isInside` resolves both sides with
|
|
115
|
+
* `realpathSync`, so a symlink cannot claim a root it is not under.
|
|
116
|
+
*
|
|
117
|
+
* ⛔ **AMBIGUITY FAILS CLOSED TOO.** Two dialects can be pointed at one
|
|
118
|
+
* directory by environment (`CLAUDE_CONFIG_DIR` and `CODEX_HOME` are both
|
|
119
|
+
* operator-set, and both sides resolve through `realpath`), and a path inside
|
|
120
|
+
* two roots is a path whose producer is not known. Exactly one match selects a
|
|
121
|
+
* profile; zero or two select the strictest.
|
|
122
|
+
*
|
|
123
|
+
* ⚠ **Today no path reaching this function is unattributed**, because
|
|
124
|
+
* `isInsideAny(transcriptRoots(...))` refuses everything outside the union
|
|
125
|
+
* BEFORE `readSpan` opens it. So the fallback is a second, independent line
|
|
126
|
+
* rather than a live branch — stated plainly rather than left for a reader to
|
|
127
|
+
* assume it has a caller (`feedback_unexercisable_branch_not_verified`).
|
|
128
|
+
*/
|
|
129
|
+
export declare function rulesForTranscript(home: string, env: NodeJS.ProcessEnv, transcriptPath: string): RedactionRules;
|
|
130
|
+
export interface RedactionCounts {
|
|
131
|
+
/** String values replaced with a marker. */
|
|
132
|
+
readonly redacted: number;
|
|
133
|
+
/** Out-of-tree values left alone because no honest marker fits. */
|
|
134
|
+
readonly skipped: number;
|
|
135
|
+
}
|
|
136
|
+
export interface RedactedLine extends RedactionCounts {
|
|
137
|
+
readonly line: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Redact one NDJSON line. **Pure, total, and length-preserving.**
|
|
141
|
+
*
|
|
142
|
+
* Returns the line UNCHANGED on anything it cannot handle — a blank line, a
|
|
143
|
+
* parse failure, a line that does not round-trip. A hook cannot report an error
|
|
144
|
+
* (DESIGN.md §13.7), and a redactor that dropped a line it could not parse would
|
|
145
|
+
* be silent data loss on a path where D56 §D8 makes loss permanent.
|
|
146
|
+
*/
|
|
147
|
+
export declare function redactLine(line: string, projectRoots: readonly string[], rules: RedactionRules, resolver?: Containment): RedactedLine;
|
|
148
|
+
/**
|
|
149
|
+
* Redact every complete line in a raw transcript span.
|
|
150
|
+
*
|
|
151
|
+
* Operates on BYTES because a span is a byte range and its edges need not fall
|
|
152
|
+
* on a line — or even on a character. Segments are split on `\n` and a segment
|
|
153
|
+
* that does not decode to exactly its own bytes is passed through untouched,
|
|
154
|
+
* which is what keeps a span that bisects a multi-byte character from changing
|
|
155
|
+
* length when it is re-encoded.
|
|
156
|
+
*/
|
|
157
|
+
export declare function redactSpan(span: Buffer, projectRoots: readonly string[], rules: RedactionRules): {
|
|
158
|
+
bytes: Buffer;
|
|
159
|
+
} & RedactionCounts;
|
|
160
|
+
/**
|
|
161
|
+
* A marker whose JSON-ENCODED byte length is exactly the original's.
|
|
162
|
+
*
|
|
163
|
+
* Encoded, not raw: what has to match is the bytes on the wire, and
|
|
164
|
+
* `JSON.stringify` escapes newlines, quotes and control characters, so a
|
|
165
|
+
* 400-byte source file is rarely 400 bytes of JSON. The marker is pure ASCII
|
|
166
|
+
* with nothing `JSON.stringify` escapes, so its encoded length is simply its
|
|
167
|
+
* own length plus the two quotes — which is what makes the padding exact.
|
|
168
|
+
*
|
|
169
|
+
* Null when even `[out-of-tree]` does not fit. The caller counts that and leaves
|
|
170
|
+
* the value alone: a short span left intact is a small leak, and a mis-sized one
|
|
171
|
+
* is a permanently gap-flagged stream.
|
|
172
|
+
*/
|
|
173
|
+
export declare function markerFor(original: string): string | null;
|
|
174
|
+
type Containment = (roots: readonly string[], candidate: string) => boolean;
|
|
175
|
+
export {};
|