codiedev 0.9.30 → 0.9.31
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/assets/git-refs.mjs +208 -0
- package/assets/opencode-plugin.mjs +6 -201
- package/package.json +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Git-ref resolution and header construction for the Codiedev OpenCode
|
|
2
|
+
// plugin. These live OUTSIDE opencode-plugin.mjs on purpose: opencode
|
|
3
|
+
// treats EVERY export of a plugin module as a plugin factory, so exporting
|
|
4
|
+
// helpers there breaks harness startup. See openCodePluginGitRefs.test.ts.
|
|
5
|
+
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { dirname, isAbsolute } from "node:path";
|
|
9
|
+
|
|
10
|
+
const skillUseBySession = new Map();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The directory the agent last WROTE to. Reading refs from the plugin's cwd is
|
|
14
|
+
* only right when `codie` was launched where the work lands. A dev who launches
|
|
15
|
+
* in their primary checkout while the agent edits a sibling git worktree would
|
|
16
|
+
* otherwise report the launch checkout's branch forever — the shape seen on
|
|
17
|
+
* `secscan`, whose merged PRs came from `feat/live-security-operations` and
|
|
18
|
+
* `worktree-landing-page-login` while the session recorded only `main`.
|
|
19
|
+
*
|
|
20
|
+
* Writes are the signal, not reads: an agent reads all over the tree, but the
|
|
21
|
+
* code it writes is what becomes the PR.
|
|
22
|
+
*
|
|
23
|
+
* Keyed by session, like skillUseBySession: one OpenCode process hosts many
|
|
24
|
+
* sessions (TUI `/new`, `task` subagents, `opencode serve`). A single global
|
|
25
|
+
* slot would let a write in session B retarget session A's headers, and both
|
|
26
|
+
* dirs pass the trust check when they share an origin, so the guard would not
|
|
27
|
+
* catch it.
|
|
28
|
+
*/
|
|
29
|
+
export const workDirBySession = new Map();
|
|
30
|
+
const WRITE_TOOLS = new Set(["write", "edit", "patch"]);
|
|
31
|
+
|
|
32
|
+
export function trackWorkPath(tool, args, sessionID, state = workDirBySession) {
|
|
33
|
+
if (!WRITE_TOOLS.has(tool)) return;
|
|
34
|
+
const filePath = args?.filePath ?? args?.file_path;
|
|
35
|
+
// Relative paths are resolved against a cwd we may not share, so they are
|
|
36
|
+
// not usable anchors.
|
|
37
|
+
if (typeof filePath !== "string" || !isAbsolute(filePath)) return;
|
|
38
|
+
state.set(sessionID, dirname(filePath));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `tool.execute.before` fires BEFORE the write lands, so a file created in a
|
|
43
|
+
* new subdirectory names a directory that does not exist yet. Running git
|
|
44
|
+
* there fails, and caching that failure would revert to the launch cwd for the
|
|
45
|
+
* whole TTL — most of a task, for an agent scaffolding several directories.
|
|
46
|
+
* Walk up to the nearest directory that does exist.
|
|
47
|
+
*/
|
|
48
|
+
function nearestExistingDir(dir, exists = existsSync) {
|
|
49
|
+
let current = dir;
|
|
50
|
+
for (let hops = 0; current && hops < 40; hops += 1) {
|
|
51
|
+
if (exists(current)) return current;
|
|
52
|
+
const parent = dirname(current);
|
|
53
|
+
if (parent === current) return null;
|
|
54
|
+
current = parent;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Canonicalize a git remote to bare `host/path` so the two sides of the trust
|
|
61
|
+
* check compare equal.
|
|
62
|
+
*
|
|
63
|
+
* They arrive from DIFFERENT inputs: the live side is the raw output of
|
|
64
|
+
* `git remote get-url origin`, while CODIEDEV_REPO_URL has already been through
|
|
65
|
+
* `safeRepoUrl()` in codie.ts, which strips userinfo. Canonicalizing to one
|
|
66
|
+
* shape — no scheme, no userinfo, scp-colon folded to a slash — is what makes
|
|
67
|
+
* `git@host:org/repo.git` and `host:org/repo.git` land on the same string.
|
|
68
|
+
*
|
|
69
|
+
* An earlier version rewrote ssh→https instead, which matched only when BOTH
|
|
70
|
+
* sides were raw. It silently failed for every ssh-cloned checkout, i.e. most
|
|
71
|
+
* enterprise ones.
|
|
72
|
+
*/
|
|
73
|
+
function normalizeRemote(url) {
|
|
74
|
+
let value = String(url || "").trim().toLowerCase();
|
|
75
|
+
if (!value) return "";
|
|
76
|
+
value = value.replace(/^[a-z][a-z0-9+.-]*:\/\//, ""); // scheme://
|
|
77
|
+
value = value.replace(/^[^@/]+@/, ""); // git@ , user:token@
|
|
78
|
+
value = value.replace(/^([^/:]+):(?!\d)/, "$1/"); // scp-style host:path, keep :port
|
|
79
|
+
value = value.replace(/\/+$/, ""); // trailing slash BEFORE .git, or "repo.git/" survives
|
|
80
|
+
value = value.replace(/\.git$/, "");
|
|
81
|
+
return value.replace(/\/+$/, "");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Git refs are re-read live per request, not taken from the launch-time
|
|
86
|
+
* environment. bin/codie.js samples CODIEDEV_BRANCH / CODIEDEV_HEAD_SHA once
|
|
87
|
+
* and freezes them for the whole session, so a dev who starts on main and
|
|
88
|
+
* moves to a feature branch reported `main` until they quit. Gateway sessions
|
|
89
|
+
* then branch-matched nothing, because a merged PR's sourceBranch is always
|
|
90
|
+
* the feature branch. The env vars survive only as a fallback for when git
|
|
91
|
+
* cannot be read at all.
|
|
92
|
+
*/
|
|
93
|
+
const GIT_TTL_MS = 5_000;
|
|
94
|
+
const gitCache = {};
|
|
95
|
+
|
|
96
|
+
function clean(value, max) {
|
|
97
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
98
|
+
return text ? text.slice(0, max) : "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Returns trimmed stdout, or null when git could not be read. */
|
|
102
|
+
function runGit(args, cwd) {
|
|
103
|
+
try {
|
|
104
|
+
return execFileSync("git", args, {
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
107
|
+
timeout: 1_000,
|
|
108
|
+
...(cwd ? { cwd } : {}),
|
|
109
|
+
}).trim();
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A write directory is only trusted when it belongs to the SAME repo the
|
|
117
|
+
* session is attributed to. Without that check, one stray edit into an
|
|
118
|
+
* unrelated checkout would publish that repo's branch name to the gateway.
|
|
119
|
+
* With no configured repo url there is nothing to check against, so we stay on
|
|
120
|
+
* cwd rather than trust a path.
|
|
121
|
+
*/
|
|
122
|
+
function trustedDir(dir, env, run, cache, at, ttlMs, exists = existsSync) {
|
|
123
|
+
if (!dir) return null;
|
|
124
|
+
const want = normalizeRemote(env.CODIEDEV_REPO_URL);
|
|
125
|
+
if (!want) return null;
|
|
126
|
+
const probe = nearestExistingDir(dir, exists);
|
|
127
|
+
if (!probe) return null;
|
|
128
|
+
const key = `trust:${probe}`;
|
|
129
|
+
const slot = cache[key];
|
|
130
|
+
if (slot && at - slot.at < ttlMs) return slot.ok ? probe : null;
|
|
131
|
+
let ok = false;
|
|
132
|
+
try {
|
|
133
|
+
ok = normalizeRemote(run(["remote", "get-url", "origin"], probe)) === want;
|
|
134
|
+
} catch {
|
|
135
|
+
ok = false;
|
|
136
|
+
}
|
|
137
|
+
cache[key] = { at, ok };
|
|
138
|
+
return ok ? probe : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resolveGitRefs({
|
|
142
|
+
env = process.env,
|
|
143
|
+
run = runGit,
|
|
144
|
+
cache = gitCache,
|
|
145
|
+
now = Date.now,
|
|
146
|
+
ttlMs = GIT_TTL_MS,
|
|
147
|
+
dir = null,
|
|
148
|
+
exists = existsSync,
|
|
149
|
+
} = {}) {
|
|
150
|
+
const at = now();
|
|
151
|
+
const target = trustedDir(dir, env, run, cache, at, ttlMs, exists);
|
|
152
|
+
// Keyed per directory: moving between worktrees must be visible at once, not
|
|
153
|
+
// after the TTL of whichever directory was resolved first.
|
|
154
|
+
const key = `refs:${target ?? ""}`;
|
|
155
|
+
const slot = cache[key];
|
|
156
|
+
// One git pair per directory per TTL keeps this flat however fast the agent
|
|
157
|
+
// loops. Failures are cached too, so a pathological git costs one attempt
|
|
158
|
+
// per TTL rather than one per request.
|
|
159
|
+
if (slot && at - slot.at < ttlMs) return slot.refs;
|
|
160
|
+
|
|
161
|
+
let branch = null;
|
|
162
|
+
let sha = null;
|
|
163
|
+
try {
|
|
164
|
+
branch = run(["branch", "--show-current"], target ?? undefined);
|
|
165
|
+
sha = run(["rev-parse", "HEAD"], target ?? undefined);
|
|
166
|
+
} catch {
|
|
167
|
+
branch = null;
|
|
168
|
+
sha = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// A successful read wins even when it is empty: on a detached HEAD "no
|
|
172
|
+
// branch" is the true answer, and falling back to env would put the stale
|
|
173
|
+
// launch-time branch straight back.
|
|
174
|
+
//
|
|
175
|
+
// The fallback is ALL-OR-NOTHING. Mixing a live branch with an env sha (or
|
|
176
|
+
// the reverse) describes a commit that never existed on that branch — if
|
|
177
|
+
// `rev-parse` alone times out on a lock, the request would claim
|
|
178
|
+
// `feat/x` + `<launch sha of main>` and the server would attribute the
|
|
179
|
+
// session to a commit it never touched. That is worse than no sha at all.
|
|
180
|
+
const branchOk = typeof branch === "string";
|
|
181
|
+
const shaOk = typeof sha === "string" && sha !== "";
|
|
182
|
+
const refs = branchOk && shaOk
|
|
183
|
+
? { branch: clean(branch, 200), sha: clean(sha, 40) }
|
|
184
|
+
: { branch: clean(env.CODIEDEV_BRANCH, 200), sha: clean(env.CODIEDEV_HEAD_SHA, 40) };
|
|
185
|
+
cache[key] = { at, refs };
|
|
186
|
+
return refs;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The complete set of headers this plugin may add — a closed allowlist. No
|
|
191
|
+
* prompt, message, code, diff, or tool output ever appears here.
|
|
192
|
+
*/
|
|
193
|
+
export function buildAttributionHeaders({ sessionID, env = process.env, refs, skills = [] }) {
|
|
194
|
+
const headers = {};
|
|
195
|
+
const session = clean(sessionID, 200);
|
|
196
|
+
if (session) headers["X-Codiedev-Session-Id"] = session;
|
|
197
|
+
const repo = clean(env.CODIEDEV_REPO_URL, 2048);
|
|
198
|
+
if (repo) headers["X-Codiedev-Repo-Url"] = repo;
|
|
199
|
+
const branch = clean(refs?.branch, 200);
|
|
200
|
+
if (branch) headers["X-Codiedev-Branch"] = branch;
|
|
201
|
+
const sha = clean(refs?.sha, 40);
|
|
202
|
+
if (/^[0-9a-f]{7,40}$/i.test(sha)) headers["X-Codiedev-Head-Sha"] = sha;
|
|
203
|
+
const user = clean(env.CODIEDEV_USER, 200);
|
|
204
|
+
if (user) headers["X-Codiedev-User"] = user;
|
|
205
|
+
const skillList = clean(skills.join(","), 4096);
|
|
206
|
+
if (skillList) headers["X-Codiedev-Skills-Used"] = skillList;
|
|
207
|
+
return headers;
|
|
208
|
+
}
|
|
@@ -3,210 +3,15 @@
|
|
|
3
3
|
// output. It only attaches stable session/git identifiers and explicit skill
|
|
4
4
|
// names to inference requests handled by the company gateway.
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
buildAttributionHeaders,
|
|
8
|
+
resolveGitRefs,
|
|
9
|
+
trackWorkPath,
|
|
10
|
+
workDirBySession,
|
|
11
|
+
} from "./git-refs.mjs";
|
|
9
12
|
|
|
10
13
|
const skillUseBySession = new Map();
|
|
11
14
|
|
|
12
|
-
/**
|
|
13
|
-
* The directory the agent last WROTE to. Reading refs from the plugin's cwd is
|
|
14
|
-
* only right when `codie` was launched where the work lands. A dev who launches
|
|
15
|
-
* in their primary checkout while the agent edits a sibling git worktree would
|
|
16
|
-
* otherwise report the launch checkout's branch forever — the shape seen on
|
|
17
|
-
* `secscan`, whose merged PRs came from `feat/live-security-operations` and
|
|
18
|
-
* `worktree-landing-page-login` while the session recorded only `main`.
|
|
19
|
-
*
|
|
20
|
-
* Writes are the signal, not reads: an agent reads all over the tree, but the
|
|
21
|
-
* code it writes is what becomes the PR.
|
|
22
|
-
*
|
|
23
|
-
* Keyed by session, like skillUseBySession: one OpenCode process hosts many
|
|
24
|
-
* sessions (TUI `/new`, `task` subagents, `opencode serve`). A single global
|
|
25
|
-
* slot would let a write in session B retarget session A's headers, and both
|
|
26
|
-
* dirs pass the trust check when they share an origin, so the guard would not
|
|
27
|
-
* catch it.
|
|
28
|
-
*/
|
|
29
|
-
const workDirBySession = new Map();
|
|
30
|
-
const WRITE_TOOLS = new Set(["write", "edit", "patch"]);
|
|
31
|
-
|
|
32
|
-
export function trackWorkPath(tool, args, sessionID, state = workDirBySession) {
|
|
33
|
-
if (!WRITE_TOOLS.has(tool)) return;
|
|
34
|
-
const filePath = args?.filePath ?? args?.file_path;
|
|
35
|
-
// Relative paths are resolved against a cwd we may not share, so they are
|
|
36
|
-
// not usable anchors.
|
|
37
|
-
if (typeof filePath !== "string" || !isAbsolute(filePath)) return;
|
|
38
|
-
state.set(sessionID, dirname(filePath));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* `tool.execute.before` fires BEFORE the write lands, so a file created in a
|
|
43
|
-
* new subdirectory names a directory that does not exist yet. Running git
|
|
44
|
-
* there fails, and caching that failure would revert to the launch cwd for the
|
|
45
|
-
* whole TTL — most of a task, for an agent scaffolding several directories.
|
|
46
|
-
* Walk up to the nearest directory that does exist.
|
|
47
|
-
*/
|
|
48
|
-
function nearestExistingDir(dir, exists = existsSync) {
|
|
49
|
-
let current = dir;
|
|
50
|
-
for (let hops = 0; current && hops < 40; hops += 1) {
|
|
51
|
-
if (exists(current)) return current;
|
|
52
|
-
const parent = dirname(current);
|
|
53
|
-
if (parent === current) return null;
|
|
54
|
-
current = parent;
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Canonicalize a git remote to bare `host/path` so the two sides of the trust
|
|
61
|
-
* check compare equal.
|
|
62
|
-
*
|
|
63
|
-
* They arrive from DIFFERENT inputs: the live side is the raw output of
|
|
64
|
-
* `git remote get-url origin`, while CODIEDEV_REPO_URL has already been through
|
|
65
|
-
* `safeRepoUrl()` in codie.ts, which strips userinfo. Canonicalizing to one
|
|
66
|
-
* shape — no scheme, no userinfo, scp-colon folded to a slash — is what makes
|
|
67
|
-
* `git@host:org/repo.git` and `host:org/repo.git` land on the same string.
|
|
68
|
-
*
|
|
69
|
-
* An earlier version rewrote ssh→https instead, which matched only when BOTH
|
|
70
|
-
* sides were raw. It silently failed for every ssh-cloned checkout, i.e. most
|
|
71
|
-
* enterprise ones.
|
|
72
|
-
*/
|
|
73
|
-
function normalizeRemote(url) {
|
|
74
|
-
let value = String(url || "").trim().toLowerCase();
|
|
75
|
-
if (!value) return "";
|
|
76
|
-
value = value.replace(/^[a-z][a-z0-9+.-]*:\/\//, ""); // scheme://
|
|
77
|
-
value = value.replace(/^[^@/]+@/, ""); // git@ , user:token@
|
|
78
|
-
value = value.replace(/^([^/:]+):(?!\d)/, "$1/"); // scp-style host:path, keep :port
|
|
79
|
-
value = value.replace(/\/+$/, ""); // trailing slash BEFORE .git, or "repo.git/" survives
|
|
80
|
-
value = value.replace(/\.git$/, "");
|
|
81
|
-
return value.replace(/\/+$/, "");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Git refs are re-read live per request, not taken from the launch-time
|
|
86
|
-
* environment. bin/codie.js samples CODIEDEV_BRANCH / CODIEDEV_HEAD_SHA once
|
|
87
|
-
* and freezes them for the whole session, so a dev who starts on main and
|
|
88
|
-
* moves to a feature branch reported `main` until they quit. Gateway sessions
|
|
89
|
-
* then branch-matched nothing, because a merged PR's sourceBranch is always
|
|
90
|
-
* the feature branch. The env vars survive only as a fallback for when git
|
|
91
|
-
* cannot be read at all.
|
|
92
|
-
*/
|
|
93
|
-
const GIT_TTL_MS = 5_000;
|
|
94
|
-
const gitCache = {};
|
|
95
|
-
|
|
96
|
-
function clean(value, max) {
|
|
97
|
-
const text = typeof value === "string" ? value.trim() : "";
|
|
98
|
-
return text ? text.slice(0, max) : "";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Returns trimmed stdout, or null when git could not be read. */
|
|
102
|
-
function runGit(args, cwd) {
|
|
103
|
-
try {
|
|
104
|
-
return execFileSync("git", args, {
|
|
105
|
-
encoding: "utf8",
|
|
106
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
107
|
-
timeout: 1_000,
|
|
108
|
-
...(cwd ? { cwd } : {}),
|
|
109
|
-
}).trim();
|
|
110
|
-
} catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* A write directory is only trusted when it belongs to the SAME repo the
|
|
117
|
-
* session is attributed to. Without that check, one stray edit into an
|
|
118
|
-
* unrelated checkout would publish that repo's branch name to the gateway.
|
|
119
|
-
* With no configured repo url there is nothing to check against, so we stay on
|
|
120
|
-
* cwd rather than trust a path.
|
|
121
|
-
*/
|
|
122
|
-
function trustedDir(dir, env, run, cache, at, ttlMs, exists = existsSync) {
|
|
123
|
-
if (!dir) return null;
|
|
124
|
-
const want = normalizeRemote(env.CODIEDEV_REPO_URL);
|
|
125
|
-
if (!want) return null;
|
|
126
|
-
const probe = nearestExistingDir(dir, exists);
|
|
127
|
-
if (!probe) return null;
|
|
128
|
-
const key = `trust:${probe}`;
|
|
129
|
-
const slot = cache[key];
|
|
130
|
-
if (slot && at - slot.at < ttlMs) return slot.ok ? probe : null;
|
|
131
|
-
let ok = false;
|
|
132
|
-
try {
|
|
133
|
-
ok = normalizeRemote(run(["remote", "get-url", "origin"], probe)) === want;
|
|
134
|
-
} catch {
|
|
135
|
-
ok = false;
|
|
136
|
-
}
|
|
137
|
-
cache[key] = { at, ok };
|
|
138
|
-
return ok ? probe : null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function resolveGitRefs({
|
|
142
|
-
env = process.env,
|
|
143
|
-
run = runGit,
|
|
144
|
-
cache = gitCache,
|
|
145
|
-
now = Date.now,
|
|
146
|
-
ttlMs = GIT_TTL_MS,
|
|
147
|
-
dir = null,
|
|
148
|
-
exists = existsSync,
|
|
149
|
-
} = {}) {
|
|
150
|
-
const at = now();
|
|
151
|
-
const target = trustedDir(dir, env, run, cache, at, ttlMs, exists);
|
|
152
|
-
// Keyed per directory: moving between worktrees must be visible at once, not
|
|
153
|
-
// after the TTL of whichever directory was resolved first.
|
|
154
|
-
const key = `refs:${target ?? ""}`;
|
|
155
|
-
const slot = cache[key];
|
|
156
|
-
// One git pair per directory per TTL keeps this flat however fast the agent
|
|
157
|
-
// loops. Failures are cached too, so a pathological git costs one attempt
|
|
158
|
-
// per TTL rather than one per request.
|
|
159
|
-
if (slot && at - slot.at < ttlMs) return slot.refs;
|
|
160
|
-
|
|
161
|
-
let branch = null;
|
|
162
|
-
let sha = null;
|
|
163
|
-
try {
|
|
164
|
-
branch = run(["branch", "--show-current"], target ?? undefined);
|
|
165
|
-
sha = run(["rev-parse", "HEAD"], target ?? undefined);
|
|
166
|
-
} catch {
|
|
167
|
-
branch = null;
|
|
168
|
-
sha = null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// A successful read wins even when it is empty: on a detached HEAD "no
|
|
172
|
-
// branch" is the true answer, and falling back to env would put the stale
|
|
173
|
-
// launch-time branch straight back.
|
|
174
|
-
//
|
|
175
|
-
// The fallback is ALL-OR-NOTHING. Mixing a live branch with an env sha (or
|
|
176
|
-
// the reverse) describes a commit that never existed on that branch — if
|
|
177
|
-
// `rev-parse` alone times out on a lock, the request would claim
|
|
178
|
-
// `feat/x` + `<launch sha of main>` and the server would attribute the
|
|
179
|
-
// session to a commit it never touched. That is worse than no sha at all.
|
|
180
|
-
const branchOk = typeof branch === "string";
|
|
181
|
-
const shaOk = typeof sha === "string" && sha !== "";
|
|
182
|
-
const refs = branchOk && shaOk
|
|
183
|
-
? { branch: clean(branch, 200), sha: clean(sha, 40) }
|
|
184
|
-
: { branch: clean(env.CODIEDEV_BRANCH, 200), sha: clean(env.CODIEDEV_HEAD_SHA, 40) };
|
|
185
|
-
cache[key] = { at, refs };
|
|
186
|
-
return refs;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* The complete set of headers this plugin may add — a closed allowlist. No
|
|
191
|
-
* prompt, message, code, diff, or tool output ever appears here.
|
|
192
|
-
*/
|
|
193
|
-
export function buildAttributionHeaders({ sessionID, env = process.env, refs, skills = [] }) {
|
|
194
|
-
const headers = {};
|
|
195
|
-
const session = clean(sessionID, 200);
|
|
196
|
-
if (session) headers["X-Codiedev-Session-Id"] = session;
|
|
197
|
-
const repo = clean(env.CODIEDEV_REPO_URL, 2048);
|
|
198
|
-
if (repo) headers["X-Codiedev-Repo-Url"] = repo;
|
|
199
|
-
const branch = clean(refs?.branch, 200);
|
|
200
|
-
if (branch) headers["X-Codiedev-Branch"] = branch;
|
|
201
|
-
const sha = clean(refs?.sha, 40);
|
|
202
|
-
if (/^[0-9a-f]{7,40}$/i.test(sha)) headers["X-Codiedev-Head-Sha"] = sha;
|
|
203
|
-
const user = clean(env.CODIEDEV_USER, 200);
|
|
204
|
-
if (user) headers["X-Codiedev-User"] = user;
|
|
205
|
-
const skillList = clean(skills.join(","), 4096);
|
|
206
|
-
if (skillList) headers["X-Codiedev-Skills-Used"] = skillList;
|
|
207
|
-
return headers;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
15
|
function addSkill(sessionID, value) {
|
|
211
16
|
const skill = clean(value, 160).replace(/^\//, "").replace(/\.md$/i, "");
|
|
212
17
|
if (!skill) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.31",
|
|
4
4
|
"description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codiedev": "dist/cli.js",
|