@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/post.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ingest request — the ONE place the credential's plaintext is used.
|
|
3
|
+
*
|
|
4
|
+
* `send()` still makes exactly one attempt and decides nothing; `deliver()`, at
|
|
5
|
+
* the bottom of this file, is `CR-018`'s application of the failure policy around
|
|
6
|
+
* it. The classification itself is data in `policy.ts` and the ledger is in
|
|
7
|
+
* `state.ts`, so what lives here is only the wiring between them and the wire.
|
|
8
|
+
*
|
|
9
|
+
* `X-Repo-Slug` is derived in `git.ts` (`CR-019d`) and arrives here already
|
|
10
|
+
* resolved. It is REQUIRED on the wire: the merged server 400s a request without
|
|
11
|
+
* it, deliberately, because a NULL `repository_id` is fail-closed-INVISIBLE
|
|
12
|
+
* rather than merely wrong (D57 §OV1).
|
|
13
|
+
*
|
|
14
|
+
* Wire shape retyped from the plan and from reading the closed-source server. No
|
|
15
|
+
* code is imported or copied from `vibecommit-mcp` (D60 §D1a): this package is
|
|
16
|
+
* MIT, and lifting closed code into it relicenses that code by accident.
|
|
17
|
+
*
|
|
18
|
+
* @provenance vibecommit-mcp src/conversation/ingest_session.ts — wire shape, retyped
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { agentForTranscript } from "./agents/registry.js";
|
|
22
|
+
import { UNKNOWN_AGENT_ID } from "./agents/types.js";
|
|
23
|
+
import { classify, markDelivered, markHeld, markSkipped, nextSpan, resolveCaps, } from "./policy.js";
|
|
24
|
+
import { fileState, isStopped, loadSessionState, saveSessionState, withFileState, } from "./state.js";
|
|
25
|
+
import { dropRewrites, dropSpooled } from "./spool.js";
|
|
26
|
+
import { CLIENT_VERSION, CLIENT_VERSION_HEADER } from "./version.js";
|
|
27
|
+
/** Production data plane. Overridable for tests and for a self-hosted server. */
|
|
28
|
+
export const DEFAULT_INGEST_URL = "https://api.vibecommit.ai/ingest/v1/session";
|
|
29
|
+
/**
|
|
30
|
+
* Loopback hosts, the only place a plaintext ingest URL is tolerated.
|
|
31
|
+
* The contract test needs `http://127.0.0.1:<port>`; nothing else does.
|
|
32
|
+
*/
|
|
33
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
34
|
+
/**
|
|
35
|
+
* `/cso` finding 2 (2026-08-09), MEDIUM. The override used to be passed to
|
|
36
|
+
* `fetch` unexamined, so `http://` put `Authorization: Bearer vcik_…` on the
|
|
37
|
+
* wire in cleartext — and the ingest credential is long-lived with no rotation
|
|
38
|
+
* by design, so one interception is a durable compromise rather than a
|
|
39
|
+
* 15-minute one.
|
|
40
|
+
*/
|
|
41
|
+
export function isAllowedIngestUrl(raw) {
|
|
42
|
+
let url;
|
|
43
|
+
try {
|
|
44
|
+
url = new URL(raw);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (url.protocol === "https:")
|
|
50
|
+
return true;
|
|
51
|
+
return url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
|
|
52
|
+
}
|
|
53
|
+
/** The ingest URL, or null when the override is one we refuse to send to. */
|
|
54
|
+
export function ingestUrl(env) {
|
|
55
|
+
const override = env.VIBECOMMIT_INGEST_URL;
|
|
56
|
+
if (override === undefined || override.trim() === "")
|
|
57
|
+
return DEFAULT_INGEST_URL;
|
|
58
|
+
const candidate = override.trim();
|
|
59
|
+
return isAllowedIngestUrl(candidate) ? candidate : null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build the request headers.
|
|
63
|
+
*
|
|
64
|
+
* `expose()` is called here and in no other module. That is the point of the
|
|
65
|
+
* opaque wrapper: the single call site is greppable, reviewable, and sits at the
|
|
66
|
+
* boundary where the secret has to be plaintext anyway.
|
|
67
|
+
*
|
|
68
|
+
* ## ⛔ SHAs ON THE WIRE, NEVER COMMIT METADATA (`CR-170` §4)
|
|
69
|
+
*
|
|
70
|
+
* No message, no author, no dates. MEASURED over the last 20 commits per repo:
|
|
71
|
+
* **9/20 schema, 8/20 web and 5/20 capture exceed 4096 bytes, largest 12,112** —
|
|
72
|
+
* so a quarter of real commits would land PERMANENTLY TRUNCATED in a header, and
|
|
73
|
+
* `capture_commits` rows cannot be retracted. Metadata is backfillable from a
|
|
74
|
+
* sha by anyone holding the repository; the observation that a commit happened
|
|
75
|
+
* at all is not. So the wire carries the irreplaceable half.
|
|
76
|
+
*/
|
|
77
|
+
export function buildIngestHeaders(credential, delta) {
|
|
78
|
+
const headers = {
|
|
79
|
+
authorization: `Bearer ${credential.expose()}`,
|
|
80
|
+
"content-type": "application/x-ndjson",
|
|
81
|
+
"content-encoding": "zstd",
|
|
82
|
+
"x-session-id": delta.sessionId,
|
|
83
|
+
"x-seq": String(delta.seq),
|
|
84
|
+
"x-byte-offset": String(delta.byteOffset),
|
|
85
|
+
"x-file": delta.fileKey,
|
|
86
|
+
"x-repo-slug": delta.repoSlug,
|
|
87
|
+
[CLIENT_VERSION_HEADER.toLowerCase()]: CLIENT_VERSION,
|
|
88
|
+
};
|
|
89
|
+
// ⛔ OMITTED ON `unknown`, AND THE OMISSION IS THE POINT — not a tidier way of
|
|
90
|
+
// saying the same thing.
|
|
91
|
+
//
|
|
92
|
+
// The server keeps ABSENT and `unknown` apart deliberately, and its own
|
|
93
|
+
// comment says the header's parsed type is nullable *because* of this guard:
|
|
94
|
+
// an absent header leaves a stream's pinned agent alone, while a PRESENT
|
|
95
|
+
// header resolves and is compared — and `unknown` resolves to `unknown`, which
|
|
96
|
+
// disagrees with any non-`unknown` pin. That disagreement is not a rejection;
|
|
97
|
+
// it drops the stream's buffered `tail_records` and re-pins, which SHEDS THE
|
|
98
|
+
// IN-FLIGHT TURN (D164 §4's mismatch-as-rewind).
|
|
99
|
+
//
|
|
100
|
+
// ⛔ So sending `unknown` would manufacture exactly the disagreement the
|
|
101
|
+
// nullable type exists to prevent. A stream that resolves cleanly on delta 1
|
|
102
|
+
// and becomes ambiguous later — a second root appearing, an env var set in one
|
|
103
|
+
// shell and not the next, a symlink flipped — would shed a turn on every flip.
|
|
104
|
+
// Omitting cannot: it says nothing, and nothing cannot disagree.
|
|
105
|
+
//
|
|
106
|
+
// ⚠ **NOT "silently", and the correction is worth its line**: the server
|
|
107
|
+
// ECHOES `agent_rewind` in the ingest 200 body (D169 §1), so the RE-PIN is
|
|
108
|
+
// reported. What is NOT reported is the shed itself. ⚠ And nothing on this
|
|
109
|
+
// side reads either — `send()` parses a body only on a non-2xx. So the case
|
|
110
|
+
// against sending rests on the turn being LOST, not on the loss being
|
|
111
|
+
// invisible; a designed, reported rewind (D164 §4) is survivable, and this
|
|
112
|
+
// client would simply be causing them for no gain.
|
|
113
|
+
//
|
|
114
|
+
// ⚠ On a genuinely ambiguous FIRST delta the two are indistinguishable — the
|
|
115
|
+
// bind writes `unknown` either way. There is no case where sending wins, and
|
|
116
|
+
// one class where it loses, so the header is conditional.
|
|
117
|
+
//
|
|
118
|
+
// This is also this function's existing idiom, four lines below: OMITTED,
|
|
119
|
+
// never sent empty; an absent header is unambiguously "nothing to say".
|
|
120
|
+
if (delta.agent !== UNKNOWN_AGENT_ID)
|
|
121
|
+
headers["x-agent"] = delta.agent;
|
|
122
|
+
// ⚠ OMITTED, never sent empty. A blank header is a value the server has to
|
|
123
|
+
// have an opinion about; an absent one is unambiguously "nothing to say", and
|
|
124
|
+
// an empty repository genuinely has nothing to say here.
|
|
125
|
+
if (delta.head != null) {
|
|
126
|
+
headers["x-head-sha"] = delta.head.sha;
|
|
127
|
+
if (delta.head.branch !== null)
|
|
128
|
+
headers["x-head-branch"] = delta.head.branch;
|
|
129
|
+
}
|
|
130
|
+
// ⛔⛔ THE WIRE CONTRACT, AND EVERY WAY OF GETTING IT WRONG IS SILENT IN BOTH
|
|
131
|
+
// DIRECTIONS (`D190`). Three header names and two separators, agreed between
|
|
132
|
+
// two repositories with NO COMPILER, NO SCHEMA AND NO SHARED TYPE spanning the
|
|
133
|
+
// seam. Name it `x-commit-attribution` (singular), join with `;`, or emit the
|
|
134
|
+
// pairs as `ancestor,successor`, and you get a green client suite, a green
|
|
135
|
+
// server suite, a 200 response, and ZERO EDGES FOREVER. Nothing goes red
|
|
136
|
+
// anywhere. `test/wire-attribution.test.ts` types these literals a second time
|
|
137
|
+
// rather than importing them, because a cell that round-trips this code's own
|
|
138
|
+
// constant proves only that it equals itself.
|
|
139
|
+
//
|
|
140
|
+
// ⛔ THE TWO LISTS ARE SENT TOGETHER OR NOT AT ALL, AND THEY ARE THE SAME
|
|
141
|
+
// LENGTH. `parseObservedCommits` returns `[]` — the ENTIRE BATCH, not the odd
|
|
142
|
+
// entry — when `rungs.length !== shas.length`. `capSpool` builds them in one
|
|
143
|
+
// loop so they cannot disagree; this line refuses to send them if they somehow
|
|
144
|
+
// do, because a batch dropped server-side is invisible and a batch not sent is
|
|
145
|
+
// retried by the next hook.
|
|
146
|
+
//
|
|
147
|
+
// ⚠ NO RUNG, NO EDGE (`D190 §2`) is why the guard is a REFUSAL rather than a
|
|
148
|
+
// fallback to `x-commits` alone: the server treats a commit with no attribution
|
|
149
|
+
// as a PRE-LADDER client's mtime guess and writes nothing, so sending the shas
|
|
150
|
+
// bare would be a slower road to the same zero, with a spool truncated on the
|
|
151
|
+
// 2xx as though it had worked.
|
|
152
|
+
const observed = delta.commits;
|
|
153
|
+
if (observed !== undefined && observed.shas.length > 0) {
|
|
154
|
+
if (observed.shas.length === observed.attributions.length) {
|
|
155
|
+
headers["x-commits"] = observed.shas.join(",");
|
|
156
|
+
headers["x-commit-attributions"] = observed.attributions.join(",");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ⚠ Same idiom as every other conditional header here: OMITTED, never sent
|
|
160
|
+
// empty. `parseRewritePairs` drops silently on a wrong separator, a third
|
|
161
|
+
// colon-field, a bad sha or a self-pair, so the spelling is fixed by
|
|
162
|
+
// `capSuccessors` and asserted literally by the wire cell.
|
|
163
|
+
if (delta.rewrites !== undefined && delta.rewrites.length > 0) {
|
|
164
|
+
headers["x-rewrites"] = delta.rewrites.join(",");
|
|
165
|
+
}
|
|
166
|
+
return headers;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The wire's error code for org-approval-pending — `CR-128`, D81.
|
|
170
|
+
*
|
|
171
|
+
* ⚠ A CROSS-REPO CONTRACT, not a local string. The server emits it as
|
|
172
|
+
* `403 { error: "capture_not_approved", owners_notified }` and pins the body
|
|
173
|
+
* shape in its own suite. **`repository_forbidden` is ALSO a 403** and carries
|
|
174
|
+
* no `owners_notified` at all — deliberately, because it is a different
|
|
175
|
+
* condition where no owner was or could have been notified. That is why every
|
|
176
|
+
* consumer here branches on THIS CODE and never on the status.
|
|
177
|
+
*/
|
|
178
|
+
export const CAPTURE_NOT_APPROVED = "capture_not_approved";
|
|
179
|
+
/**
|
|
180
|
+
* An error body is a handful of bytes by contract. Anything larger is not one
|
|
181
|
+
* this client parses — a cap so a misconfigured or hostile endpoint cannot make
|
|
182
|
+
* a hook buffer an arbitrary response into memory.
|
|
183
|
+
*/
|
|
184
|
+
const MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
185
|
+
/**
|
|
186
|
+
* Parse an error body, or return null. **NEVER THROWS, on any input.**
|
|
187
|
+
*
|
|
188
|
+
* That is a hard requirement rather than defensive habit: `deliver()` is the
|
|
189
|
+
* HOOK's path, `post.ts` states that an exception there is the contract
|
|
190
|
+
* breaking, and `JSON.parse` on a response body is an exception waiting for a
|
|
191
|
+
* bad day. Every failure — a non-JSON body, an empty body, an array, a body
|
|
192
|
+
* whose `error` is not a string — resolves to `null`, and `null` sends the
|
|
193
|
+
* caller to the branch that asserts nothing. That is the honest default,
|
|
194
|
+
* because `null` already means *not determinable*.
|
|
195
|
+
*
|
|
196
|
+
* ⚠ Read ONLY on a non-2xx. A 2xx body is not read, not awaited, and not
|
|
197
|
+
* parsed, so the hook's hot path is untouched.
|
|
198
|
+
*/
|
|
199
|
+
async function readErrorDetail(res) {
|
|
200
|
+
if (res.ok)
|
|
201
|
+
return null;
|
|
202
|
+
// The declared length, when there is one. The abort signal already bounds the
|
|
203
|
+
// read in TIME; this bounds it in BYTES for a fast, huge response.
|
|
204
|
+
const declared = Number(res.headers.get("content-length"));
|
|
205
|
+
if (Number.isFinite(declared) && declared > MAX_ERROR_BODY_BYTES)
|
|
206
|
+
return null;
|
|
207
|
+
let body;
|
|
208
|
+
try {
|
|
209
|
+
const text = await res.text();
|
|
210
|
+
if (text.length > MAX_ERROR_BODY_BYTES)
|
|
211
|
+
return null;
|
|
212
|
+
body = JSON.parse(text);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
218
|
+
return null;
|
|
219
|
+
const { error, owners_notified: owners } = body;
|
|
220
|
+
if (typeof error !== "string" || error === "")
|
|
221
|
+
return null;
|
|
222
|
+
// The key is absent (`undefined`) on bodies that do not carry it, `null` when
|
|
223
|
+
// it is present and not determinable. Both are preserved; neither is coerced
|
|
224
|
+
// into a number, because a count invented here is a claim invented here.
|
|
225
|
+
const ownersNotified = owners === null ? null : typeof owners === "number" && Number.isFinite(owners) ? owners : undefined;
|
|
226
|
+
return { code: error, ownersNotified };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* One attempt. No retry, no backoff, no classification — `CR-018` owns all three.
|
|
230
|
+
*
|
|
231
|
+
* The timeout is mandatory rather than optional: a hook has a wall-clock budget
|
|
232
|
+
* it must not exceed (DESIGN.md §13.7), and `fetch` without a signal waits on the
|
|
233
|
+
* platform default, which is far longer than any hook timeout.
|
|
234
|
+
*
|
|
235
|
+
* Never throws. A hook that takes an exception from the network has already
|
|
236
|
+
* broken the contract; returning `unreachable` keeps the decision with the
|
|
237
|
+
* caller.
|
|
238
|
+
*/
|
|
239
|
+
export async function send(url, headers, body, timeoutMs) {
|
|
240
|
+
const controller = new AbortController();
|
|
241
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
242
|
+
try {
|
|
243
|
+
const res = await fetch(url, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers,
|
|
246
|
+
body,
|
|
247
|
+
signal: controller.signal,
|
|
248
|
+
// `/cso` finding 2. undici strips Authorization on a cross-origin
|
|
249
|
+
// redirect, so following one is not a live hole — but a POST carrying a
|
|
250
|
+
// bearer token has no business chasing a hop the operator did not
|
|
251
|
+
// configure, and an ingest endpoint has no reason to issue one.
|
|
252
|
+
redirect: "error",
|
|
253
|
+
});
|
|
254
|
+
// The body read happens INSIDE the try and BEFORE the timer is cleared, so
|
|
255
|
+
// the same `AbortController` that bounds the request also bounds the read.
|
|
256
|
+
// A slow body cannot outlive the hook's wall-clock budget.
|
|
257
|
+
return { kind: "response", status: res.status, detail: await readErrorDetail(res) };
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return { kind: "unreachable" };
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// CR-018 — one attempt, classified, with the matching action applied.
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
/**
|
|
270
|
+
* A one-way digest of the credential, for the `fatal` stop's key.
|
|
271
|
+
*
|
|
272
|
+
* The second and last call to `expose()` in the package, kept in this module for
|
|
273
|
+
* the same reason as the first: the plaintext's exits are greppable in one file.
|
|
274
|
+
* What is stored is 16 hex characters of a SHA-256 over a high-entropy opaque
|
|
275
|
+
* token — enough to tell one credential from another, and not a preimage of
|
|
276
|
+
* anything. No copy path and no header ever carries it.
|
|
277
|
+
*/
|
|
278
|
+
export function credentialFingerprint(credential) {
|
|
279
|
+
return createHash("sha256").update(credential.expose()).digest("hex").slice(0, 16);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Send the undelivered span of one transcript file and apply the failure policy.
|
|
283
|
+
*
|
|
284
|
+
* **Exactly one attempt, ever.** `later` does not retry here; it leaves the
|
|
285
|
+
* offset where it is so the hook that fires on the NEXT turn picks the same
|
|
286
|
+
* bytes up. A loop in this function would reopen D56 plan §D8, the founder
|
|
287
|
+
* override that says a hook must never block or slow a developer's turn.
|
|
288
|
+
*
|
|
289
|
+
* Never throws: the caller is a hook, and an exception on this path is the
|
|
290
|
+
* contract breaking. Every failure resolves to a `Delivery`.
|
|
291
|
+
*
|
|
292
|
+
* `readBody` is supplied by the caller rather than read here, because the caller
|
|
293
|
+
* is the one that has confined `transcript_path` to the Claude Code root
|
|
294
|
+
* (`/cso` finding 1). This module never opens a file.
|
|
295
|
+
*/
|
|
296
|
+
export async function deliver(ctx, eof, readBody) {
|
|
297
|
+
// `CR-017d`. The consent gate already refuses a `cwd` outside a work tree, so
|
|
298
|
+
// the hook cannot reach this — but the ledger's isolation must not rest on a
|
|
299
|
+
// check in another module, the same discipline `consent.ts` states about its
|
|
300
|
+
// own error paths. No repo, no send: a delta whose progress cannot be recorded
|
|
301
|
+
// against a repo is one whose progress would be recorded against all of them.
|
|
302
|
+
const key = { repoKey: ctx.repoKey, sessionId: ctx.sessionId };
|
|
303
|
+
if (key.repoKey === "")
|
|
304
|
+
return { kind: "no-repo" };
|
|
305
|
+
const session = loadSessionState(ctx.home, key);
|
|
306
|
+
if (isStopped(session, credentialFingerprint(ctx.credential)))
|
|
307
|
+
return { kind: "stopped" };
|
|
308
|
+
const current = fileState(session, ctx.fileKey);
|
|
309
|
+
const span = nextSpan(current, eof);
|
|
310
|
+
if (span === null)
|
|
311
|
+
return { kind: "nothing-to-send" };
|
|
312
|
+
const body = readBody(span.from, span.to);
|
|
313
|
+
if (body === null)
|
|
314
|
+
return { kind: "nothing-to-send" };
|
|
315
|
+
// `seq` is claimed BEFORE the attempt and persisted regardless of outcome, so
|
|
316
|
+
// it is monotonic per session across retries. A seq reused after a failure
|
|
317
|
+
// would read server-side as a replay of a delta that is not the same bytes.
|
|
318
|
+
const seq = session.seq + 1;
|
|
319
|
+
const outcome = await send(ctx.url, buildIngestHeaders(ctx.credential, {
|
|
320
|
+
sessionId: ctx.sessionId,
|
|
321
|
+
seq,
|
|
322
|
+
byteOffset: span.from,
|
|
323
|
+
fileKey: ctx.fileKey,
|
|
324
|
+
repoSlug: ctx.repoSlug,
|
|
325
|
+
// ⛔ THE ONE PLACE THE WIRE AGENT IS DECIDED — from the containing
|
|
326
|
+
// registry root, never from the flag (D177 §7). Per delivery rather than
|
|
327
|
+
// per hook: a delegated stream is a file in its own right, and the root
|
|
328
|
+
// that contains it is what names its producer.
|
|
329
|
+
agent: agentForTranscript(ctx.home, ctx.env, ctx.transcriptPath),
|
|
330
|
+
head: ctx.head,
|
|
331
|
+
commits: ctx.commits,
|
|
332
|
+
rewrites: ctx.rewrites,
|
|
333
|
+
}), body, ctx.timeoutMs);
|
|
334
|
+
const disposition = classify(outcome);
|
|
335
|
+
const caps = resolveCaps(ctx.env);
|
|
336
|
+
let next = { ...session, seq };
|
|
337
|
+
switch (disposition) {
|
|
338
|
+
case "ok":
|
|
339
|
+
next = withFileState(next, ctx.fileKey, markDelivered(current, span.to, ctx.nowMs));
|
|
340
|
+
// ⛔ TRUNCATE HERE AND NOWHERE ELSE. The spool is append-then-drop-on-2xx,
|
|
341
|
+
// so a commit survives a 500, a timeout and a `later` and is retried by
|
|
342
|
+
// the next hook. Dropping on READ would lose it permanently to one bad
|
|
343
|
+
// response, and `capture_commits` rows can never be retracted (D105/D108).
|
|
344
|
+
//
|
|
345
|
+
// ⛔ `count`, NEVER `shas.length`. `dropSpooled` drops the FIRST N LINES,
|
|
346
|
+
// and a HELD line is consumed without being sent — so truncating by the
|
|
347
|
+
// number of shas on the wire would delete the held line at the head and
|
|
348
|
+
// leave the delivered commit to be re-sent on every hook, forever.
|
|
349
|
+
if (ctx.commits !== undefined && ctx.commits.count > 0) {
|
|
350
|
+
dropSpooled(ctx.home, key, ctx.commits.count);
|
|
351
|
+
}
|
|
352
|
+
// The rewrite spool is its own file with its own cap, so its own drop.
|
|
353
|
+
if (ctx.rewrites !== undefined && ctx.rewrites.length > 0) {
|
|
354
|
+
dropRewrites(ctx.home, key, ctx.rewrites.length);
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
case "later":
|
|
358
|
+
next = withFileState(next, ctx.fileKey, markHeld(current, span, ctx.nowMs, caps));
|
|
359
|
+
break;
|
|
360
|
+
case "never":
|
|
361
|
+
next = withFileState(next, ctx.fileKey, markSkipped(current, span.from, span.to));
|
|
362
|
+
break;
|
|
363
|
+
case "fatal":
|
|
364
|
+
// Credential-level, not payload-level: the offset does NOT advance, because
|
|
365
|
+
// these bytes are fine and a reconnected user should still get them.
|
|
366
|
+
next = {
|
|
367
|
+
...withFileState(next, ctx.fileKey, markHeld(current, span, ctx.nowMs, caps)),
|
|
368
|
+
stop: {
|
|
369
|
+
at: new Date(ctx.nowMs).toISOString(),
|
|
370
|
+
fingerprint: credentialFingerprint(ctx.credential),
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
saveSessionState(ctx.home, key, next);
|
|
376
|
+
// `outcome.detail` is null on every path except a non-2xx with a parseable
|
|
377
|
+
// error body, so this is a pass-through and not a second decision.
|
|
378
|
+
return {
|
|
379
|
+
kind: "attempted",
|
|
380
|
+
disposition,
|
|
381
|
+
detail: outcome.kind === "response" ? outcome.detail : null,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
//# sourceMappingURL=post.js.map
|
package/dist/post.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post.js","sourceRoot":"","sources":["../src/post.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
/**
|
|
39
|
+
* The git toplevel for `dir`, or null when it is not inside a work tree.
|
|
40
|
+
*
|
|
41
|
+
* ⚠ **The timeout DERIVES from the hook's remaining budget** (`TODOS[87]`).
|
|
42
|
+
* `execFileSync` blocks the event loop, so the watchdog cannot fire while this
|
|
43
|
+
* call is outstanding — a fixed 5s ceiling here is 5s the watchdog provably
|
|
44
|
+
* cannot cover, and it is one of four such ceilings that summed to 20s against a
|
|
45
|
+
* 5s budget. `null` means the budget is spent, and the answer is the one this
|
|
46
|
+
* function already gives when git cannot tell us: no toplevel, so no consent,
|
|
47
|
+
* so the hook does nothing. Silent by contract and correct by default.
|
|
48
|
+
*
|
|
49
|
+
* ⛔ **The answer is validated by CONTENT: `--show-toplevel` is absolute, always.**
|
|
50
|
+
* Without that gate a `git` on `PATH` that echoed a 40-hex string back would have
|
|
51
|
+
* it `resolve`d against `process.cwd()` and returned as a toplevel — accepted
|
|
52
|
+
* because it arrived where an answer was expected. Fail closed instead.
|
|
53
|
+
*/
|
|
54
|
+
export declare function gitToplevel(dir: string): string | null;
|
|
55
|
+
/** The two keys `D184` splits apart. Never one value doing both jobs. */
|
|
56
|
+
export interface ProjectKeys {
|
|
57
|
+
/**
|
|
58
|
+
* The worktree TOPLEVEL. ⛔ **UNCHANGED role** — the session-state bucket
|
|
59
|
+
* (`D58`/`CR-017d` keeps per-worktree isolation deliberately) and the
|
|
60
|
+
* `resolveRepoSlug` input.
|
|
61
|
+
*/
|
|
62
|
+
readonly worktree: string;
|
|
63
|
+
/**
|
|
64
|
+
* The canonicalised git COMMON DIR. ⛔ **The consent key, and nothing else.**
|
|
65
|
+
* Shared by a main clone and every worktree linked to it, which is the whole
|
|
66
|
+
* point; never a redaction confinement root (`D184 §3`).
|
|
67
|
+
*/
|
|
68
|
+
readonly consent: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Both keys for a directory, or **null** — ⛔ **fail closed, never a fallback.**
|
|
72
|
+
*
|
|
73
|
+
* If either spawn fails or either answer fails its content check, there is no
|
|
74
|
+
* partial answer: a guessed consent key grants capture on a repository the user
|
|
75
|
+
* never approved, and a guessed worktree key files a session under the wrong
|
|
76
|
+
* repository permanently.
|
|
77
|
+
*
|
|
78
|
+
* ⚠ **NO MIGRATION PATH FOR EXISTING `projects.json` ENTRIES, deliberately.**
|
|
79
|
+
* The allow list is keyed by string and the key changes shape, so a pre-existing
|
|
80
|
+
* grant at a toplevel stops matching. Pre-launch there are zero users and zero
|
|
81
|
+
* rows (`project_pre_launch_no_data`), so the migration is a no-op with nothing
|
|
82
|
+
* to migrate — writing one would be speculative code guarding a case that cannot
|
|
83
|
+
* occur. The observable cost if that assumption were ever wrong is one re-run of
|
|
84
|
+
* `vibecommit connect`, not a silent grant.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveProjectKeys(dir: string): ProjectKeys | null;
|
|
87
|
+
/**
|
|
88
|
+
* The worktree toplevel for a directory: the session bucket and slug input.
|
|
89
|
+
*
|
|
90
|
+
* ⛔ **NO LONGER THE CONSENT KEY — `D184` moved that to
|
|
91
|
+
* `resolveProjectKeys(dir).consent`.** The name is kept because the three roles
|
|
92
|
+
* it still serves are unchanged and every caller of it wants the toplevel; a
|
|
93
|
+
* rename would be churn across files this change has no other reason to touch.
|
|
94
|
+
*
|
|
95
|
+
* Falls back to NOTHING. A directory outside a work tree has no project identity,
|
|
96
|
+
* and inventing one from `cwd` would file a session under a key the user cannot
|
|
97
|
+
* see in `vibecommit status`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveProjectKey(dir: string): string | null;
|