@blaxel/core 0.3.35-preview.344 → 0.3.36-preview.347
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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/common/coding-agent.js +75 -0
- package/dist/cjs/common/coding-agent.test.js +50 -0
- package/dist/cjs/common/sentry.js +20 -2
- package/dist/cjs/common/sentry.test.js +22 -2
- package/dist/cjs/common/settings.js +33 -4
- package/dist/cjs/common/settings.test.js +132 -0
- package/dist/cjs/types/common/coding-agent.d.ts +7 -0
- package/dist/cjs/types/common/coding-agent.test.d.ts +1 -0
- package/dist/cjs/types/common/settings.d.ts +6 -0
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/common/coding-agent.js +75 -0
- package/dist/cjs-browser/common/coding-agent.test.js +50 -0
- package/dist/cjs-browser/common/sentry.js +20 -2
- package/dist/cjs-browser/common/sentry.test.js +22 -2
- package/dist/cjs-browser/common/settings.js +33 -4
- package/dist/cjs-browser/common/settings.test.js +132 -0
- package/dist/cjs-browser/types/common/coding-agent.d.ts +7 -0
- package/dist/cjs-browser/types/common/coding-agent.test.d.ts +1 -0
- package/dist/cjs-browser/types/common/settings.d.ts +6 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/common/coding-agent.js +71 -0
- package/dist/esm/common/coding-agent.test.js +48 -0
- package/dist/esm/common/sentry.js +20 -2
- package/dist/esm/common/sentry.test.js +22 -2
- package/dist/esm/common/settings.js +33 -4
- package/dist/esm/common/settings.test.js +132 -0
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/common/coding-agent.js +71 -0
- package/dist/esm-browser/common/coding-agent.test.js +48 -0
- package/dist/esm-browser/common/sentry.js +20 -2
- package/dist/esm-browser/common/sentry.test.js +22 -2
- package/dist/esm-browser/common/settings.js +33 -4
- package/dist/esm-browser/common/settings.test.js +132 -0
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variables coding agents set for the commands they run, first
|
|
3
|
+
* match wins, with the agent id the SDK reports. Ids match the Blaxel CLI's.
|
|
4
|
+
*/
|
|
5
|
+
const CODING_AGENTS = [
|
|
6
|
+
["CLAUDECODE", "claude-code"],
|
|
7
|
+
["CODEX_THREAD_ID", "codex"],
|
|
8
|
+
["CODEX_SANDBOX", "codex"],
|
|
9
|
+
["CURSOR_AGENT", "cursor"],
|
|
10
|
+
["GEMINI_CLI", "gemini-cli"],
|
|
11
|
+
["QWEN_CODE", "qwen-code"],
|
|
12
|
+
["OPENCODE_TERMINAL", "opencode"],
|
|
13
|
+
["OPENCODE", "opencode"],
|
|
14
|
+
["GOOSE_TERMINAL", "goose"],
|
|
15
|
+
["AUGMENT_AGENT", "augment"],
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Agent names found in the generic `AI_AGENT` and `AGENT` variables, mapped to
|
|
19
|
+
* the same ids. Any other `AI_AGENT` name is reported as "other".
|
|
20
|
+
*/
|
|
21
|
+
const CODING_AGENT_NAMES = {
|
|
22
|
+
"claude-code": "claude-code",
|
|
23
|
+
claude: "claude-code",
|
|
24
|
+
codex: "codex",
|
|
25
|
+
cursor: "cursor",
|
|
26
|
+
"cursor-cli": "cursor",
|
|
27
|
+
"gemini-cli": "gemini-cli",
|
|
28
|
+
gemini: "gemini-cli",
|
|
29
|
+
"qwen-code": "qwen-code",
|
|
30
|
+
opencode: "opencode",
|
|
31
|
+
goose: "goose",
|
|
32
|
+
augment: "augment",
|
|
33
|
+
"augment-cli": "augment",
|
|
34
|
+
crush: "crush",
|
|
35
|
+
"github-copilot": "github-copilot",
|
|
36
|
+
"github-copilot-cli": "github-copilot",
|
|
37
|
+
};
|
|
38
|
+
/** Every environment variable `detectCodingAgent` reads. */
|
|
39
|
+
export const CODING_AGENT_ENV_VARS = [
|
|
40
|
+
...CODING_AGENTS.map(([name]) => name),
|
|
41
|
+
"AI_AGENT",
|
|
42
|
+
"AGENT",
|
|
43
|
+
];
|
|
44
|
+
function agentName(value) {
|
|
45
|
+
const name = value?.trim().toLowerCase() ?? "";
|
|
46
|
+
// Claude Code sets AI_AGENT=claude-code_<version>_agent: keep the name.
|
|
47
|
+
const end = name.search(/[_/@ ]/);
|
|
48
|
+
return end > 0 ? name.slice(0, end) : name;
|
|
49
|
+
}
|
|
50
|
+
function knownAgentId(name) {
|
|
51
|
+
return Object.prototype.hasOwnProperty.call(CODING_AGENT_NAMES, name)
|
|
52
|
+
? CODING_AGENT_NAMES[name]
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Id of the coding agent whose environment the SDK runs in, "other" for an
|
|
57
|
+
* agent without a known name, or "" for none.
|
|
58
|
+
*/
|
|
59
|
+
export function detectCodingAgent(getenv) {
|
|
60
|
+
for (const [name, id] of CODING_AGENTS) {
|
|
61
|
+
if (getenv(name)?.trim()) {
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const name = agentName(getenv("AI_AGENT"));
|
|
66
|
+
if (name) {
|
|
67
|
+
return knownAgentId(name) ?? "other";
|
|
68
|
+
}
|
|
69
|
+
// AGENT is a generic name, so only a known agent name counts.
|
|
70
|
+
return knownAgentId(agentName(getenv("AGENT"))) ?? "";
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { detectCodingAgent } from "./coding-agent.js";
|
|
3
|
+
function detect(vars) {
|
|
4
|
+
return detectCodingAgent((name) => vars[name]);
|
|
5
|
+
}
|
|
6
|
+
describe("detectCodingAgent", () => {
|
|
7
|
+
it("returns nothing outside a coding agent", () => {
|
|
8
|
+
expect(detect({})).toBe("");
|
|
9
|
+
expect(detect({ CLAUDECODE: "", AI_AGENT: " " })).toBe("");
|
|
10
|
+
});
|
|
11
|
+
it.each([
|
|
12
|
+
[{ CLAUDECODE: "1" }, "claude-code"],
|
|
13
|
+
[{ CODEX_THREAD_ID: "thread" }, "codex"],
|
|
14
|
+
[{ CODEX_SANDBOX: "seatbelt" }, "codex"],
|
|
15
|
+
[{ CURSOR_AGENT: "1" }, "cursor"],
|
|
16
|
+
[{ GEMINI_CLI: "1" }, "gemini-cli"],
|
|
17
|
+
[{ QWEN_CODE: "1" }, "qwen-code"],
|
|
18
|
+
[{ OPENCODE_TERMINAL: "1" }, "opencode"],
|
|
19
|
+
[{ OPENCODE: "1", AGENT: "1" }, "opencode"],
|
|
20
|
+
[{ GOOSE_TERMINAL: "1" }, "goose"],
|
|
21
|
+
[{ AUGMENT_AGENT: "1" }, "augment"],
|
|
22
|
+
])("detects the marker %j", (vars, id) => {
|
|
23
|
+
expect(detect(vars)).toBe(id);
|
|
24
|
+
});
|
|
25
|
+
it("prefers an agent-specific marker over the generic variables", () => {
|
|
26
|
+
expect(detect({ AI_AGENT: "something-else", CODEX_SANDBOX: "seatbelt" })).toBe("codex");
|
|
27
|
+
});
|
|
28
|
+
it.each([
|
|
29
|
+
["claude-code_2-1-306_agent", "claude-code"],
|
|
30
|
+
["Cursor", "cursor"],
|
|
31
|
+
["crush", "crush"],
|
|
32
|
+
["github-copilot-cli", "github-copilot"],
|
|
33
|
+
["some-new-agent", "other"],
|
|
34
|
+
])("maps AI_AGENT=%j to %j", (value, id) => {
|
|
35
|
+
expect(detect({ AI_AGENT: value })).toBe(id);
|
|
36
|
+
});
|
|
37
|
+
it("reads AGENT only for a known agent name", () => {
|
|
38
|
+
expect(detect({ AGENT: "goose" })).toBe("goose");
|
|
39
|
+
expect(detect({ AGENT: "1" })).toBe("");
|
|
40
|
+
expect(detect({ AGENT: "build-runner" })).toBe("");
|
|
41
|
+
expect(detect({ AGENT: "goose", AI_AGENT: "claude" })).toBe("claude-code");
|
|
42
|
+
});
|
|
43
|
+
it("never reports a free-form name", () => {
|
|
44
|
+
expect(detect({ AI_AGENT: "Jane Doe's laptop" })).toBe("other");
|
|
45
|
+
expect(detect({ AI_AGENT: "toString" })).toBe("other");
|
|
46
|
+
expect(detect({ AGENT: "toString" })).toBe("");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -181,9 +181,25 @@ function generateEventId() {
|
|
|
181
181
|
return value.toString(16);
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
|
+
const SAFE_WORKSPACE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
184
185
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
186
|
+
* The workspace name is the only user identifier, so an issue shows how many
|
|
187
|
+
* workspaces it affects. Omitted when unknown or not a workspace name.
|
|
188
|
+
*/
|
|
189
|
+
function sentryUser() {
|
|
190
|
+
try {
|
|
191
|
+
const workspace = settings.workspace;
|
|
192
|
+
return typeof workspace === "string" && SAFE_WORKSPACE_NAME.test(workspace)
|
|
193
|
+
? { id: workspace }
|
|
194
|
+
: undefined;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Build an allowlisted event. Raw messages, response bodies, resource context,
|
|
202
|
+
* and absolute host paths are deliberately excluded.
|
|
187
203
|
*/
|
|
188
204
|
function errorToSentryEvent(error, ownedFrames) {
|
|
189
205
|
return {
|
|
@@ -193,6 +209,7 @@ function errorToSentryEvent(error, ownedFrames) {
|
|
|
193
209
|
level: "error",
|
|
194
210
|
environment: settings.env,
|
|
195
211
|
release: `sdk-typescript@${settings.version}`,
|
|
212
|
+
user: sentryUser(),
|
|
196
213
|
tags: {
|
|
197
214
|
"blaxel.version": settings.version,
|
|
198
215
|
"blaxel.commit": settings.commit,
|
|
@@ -286,6 +303,7 @@ function h2DegradationToSentryEvent(reason, domainTag, count) {
|
|
|
286
303
|
release: `sdk-typescript@${settings.version}`,
|
|
287
304
|
message: `h2 transport degradation: ${reason}`,
|
|
288
305
|
fingerprint: ["h2-degradation", reason, domainTag],
|
|
306
|
+
user: sentryUser(),
|
|
289
307
|
tags: {
|
|
290
308
|
"blaxel.version": settings.version,
|
|
291
309
|
"blaxel.commit": settings.commit,
|
|
@@ -5,7 +5,7 @@ const mockSettings = vi.hoisted(() => ({
|
|
|
5
5
|
env: "prod",
|
|
6
6
|
version: "9.9.9",
|
|
7
7
|
commit: "abcdef0",
|
|
8
|
-
workspace: "
|
|
8
|
+
workspace: "acme-staging",
|
|
9
9
|
}));
|
|
10
10
|
vi.mock("./settings.js", () => ({ settings: mockSettings }));
|
|
11
11
|
const appFrameUrl = "file:///Users/customer/private-project/app.ts";
|
|
@@ -129,6 +129,8 @@ describe("SDK Sentry boundary", () => {
|
|
|
129
129
|
"blaxel.error_source": "unhandled-sdk-exception",
|
|
130
130
|
});
|
|
131
131
|
expect(event.tags).not.toHaveProperty("blaxel.workspace");
|
|
132
|
+
// The workspace is the only user identifier, so issues count affected workspaces.
|
|
133
|
+
expect(event.user).toEqual({ id: "acme-staging" });
|
|
132
134
|
const frames = event.exception?.values[0]?.stacktrace.frames ?? [];
|
|
133
135
|
expect(frames.length).toBeGreaterThan(0);
|
|
134
136
|
for (const frame of frames) {
|
|
@@ -137,7 +139,6 @@ describe("SDK Sentry boundary", () => {
|
|
|
137
139
|
const serialized = JSON.stringify(event);
|
|
138
140
|
expect(serialized).not.toContain("secret response body");
|
|
139
141
|
expect(serialized).not.toContain("customer-123");
|
|
140
|
-
expect(serialized).not.toContain("private-workspace");
|
|
141
142
|
expect(serialized).not.toContain("/Users/customer");
|
|
142
143
|
}
|
|
143
144
|
finally {
|
|
@@ -234,8 +235,27 @@ describe("SDK Sentry boundary", () => {
|
|
|
234
235
|
domainTag,
|
|
235
236
|
},
|
|
236
237
|
extra: { count: 100 },
|
|
238
|
+
user: { id: "acme-staging" },
|
|
237
239
|
});
|
|
238
240
|
});
|
|
241
|
+
it("leaves the user out when the workspace is not a workspace name", async () => {
|
|
242
|
+
const fetchMock = createFetchMock();
|
|
243
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
244
|
+
const workspace = mockSettings.workspace;
|
|
245
|
+
mockSettings.workspace = "/Users/customer/private project";
|
|
246
|
+
try {
|
|
247
|
+
const { flushSentry, initSentry } = await import("./sentry.js");
|
|
248
|
+
initSentry();
|
|
249
|
+
emitUncaughtExceptionMonitor(makeSdkError());
|
|
250
|
+
await flushSentry();
|
|
251
|
+
const event = eventFromFetch(fetchMock);
|
|
252
|
+
expect(event.user).toBeUndefined();
|
|
253
|
+
expect(JSON.stringify(event)).not.toContain("/Users/customer");
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
mockSettings.workspace = workspace;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
239
259
|
it("groups keys independently and hashes external domains", async () => {
|
|
240
260
|
const fetchMock = createFetchMock();
|
|
241
261
|
vi.stubGlobal("fetch", fetchMock);
|
|
@@ -3,6 +3,7 @@ import { ApiKey } from "../authentication/apikey.js";
|
|
|
3
3
|
import { ClientCredentials } from "../authentication/clientcredentials.js";
|
|
4
4
|
import { MissingCredentials } from "../authentication/credentials.js";
|
|
5
5
|
import { authentication } from "../authentication/index.js";
|
|
6
|
+
import { detectCodingAgent } from "../common/coding-agent.js";
|
|
6
7
|
import { env } from "../common/env.js";
|
|
7
8
|
import { CredentialsError } from "../common/errors.js";
|
|
8
9
|
import { isBrokenBunH2Runtime } from "../common/h2-runtime.js";
|
|
@@ -24,8 +25,8 @@ function missingCredentialsMessage() {
|
|
|
24
25
|
return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
|
|
25
26
|
}
|
|
26
27
|
// Build info - these placeholders are replaced at build time by build:replace-imports
|
|
27
|
-
const BUILD_VERSION = "0.3.
|
|
28
|
-
const BUILD_COMMIT = "
|
|
28
|
+
const BUILD_VERSION = "0.3.36-preview.347";
|
|
29
|
+
const BUILD_COMMIT = "bb3cd5806f537daac42970497c263d4ccb4b2455";
|
|
29
30
|
const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
|
|
30
31
|
const BLAXEL_API_VERSION = "2026-09-22";
|
|
31
32
|
// Bun < 1.3.11 never sends connection-level WINDOW_UPDATE: the pooled h2
|
|
@@ -62,6 +63,18 @@ function getConfigTracking() {
|
|
|
62
63
|
}
|
|
63
64
|
return null;
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether `DO_NOT_TRACK` (`1` or `true`) opts out of the coding agent token.
|
|
68
|
+
* The process environment is checked on its own as well, so a project `.env`
|
|
69
|
+
* or `blaxel.toml` can't undo an opt-out set for the process. The saved
|
|
70
|
+
* `tracking` setting is not read: it is an opt-in for error reports, and
|
|
71
|
+
* `bl login` saves it as false until the user turns it on.
|
|
72
|
+
*/
|
|
73
|
+
function trackingOptedOut() {
|
|
74
|
+
const optsOut = (value) => value === "true" || value === "1";
|
|
75
|
+
const processValue = typeof process !== "undefined" ? process.env?.DO_NOT_TRACK : undefined;
|
|
76
|
+
return optsOut(processValue) || optsOut(env.DO_NOT_TRACK);
|
|
77
|
+
}
|
|
65
78
|
// Function to get OS and architecture
|
|
66
79
|
function getOsArch() {
|
|
67
80
|
try {
|
|
@@ -218,16 +231,32 @@ class Settings {
|
|
|
218
231
|
set integration(value) {
|
|
219
232
|
this.config.integration = value;
|
|
220
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Id of the coding agent (Claude Code, Codex, Cursor, ...) whose environment
|
|
236
|
+
* runs the SDK, `"other"` for an agent without a known name, or `""` for none
|
|
237
|
+
* or when telemetry is opted out. Sent as an `agent/<id>` User-Agent token.
|
|
238
|
+
*/
|
|
239
|
+
get codingAgent() {
|
|
240
|
+
const agent = detectCodingAgent((name) => env[name]);
|
|
241
|
+
return agent && !trackingOptedOut() ? agent : "";
|
|
242
|
+
}
|
|
221
243
|
get headers() {
|
|
222
244
|
this.assertCredentials();
|
|
223
245
|
const osArch = getOsArch();
|
|
224
|
-
|
|
246
|
+
let userAgent = `blaxel/sdk/typescript/${this.version} (${osArch}) blaxel/${this.commit}`;
|
|
225
247
|
const integration = this.integration;
|
|
248
|
+
if (integration) {
|
|
249
|
+
userAgent += ` ${integration}`;
|
|
250
|
+
}
|
|
251
|
+
const codingAgent = this.codingAgent;
|
|
252
|
+
if (codingAgent) {
|
|
253
|
+
userAgent += ` agent/${codingAgent}`;
|
|
254
|
+
}
|
|
226
255
|
return {
|
|
227
256
|
"x-blaxel-authorization": this.authorization,
|
|
228
257
|
"x-blaxel-workspace": this.workspace || "",
|
|
229
258
|
"Blaxel-Version": this.apiVersion,
|
|
230
|
-
"User-Agent":
|
|
259
|
+
"User-Agent": userAgent,
|
|
231
260
|
};
|
|
232
261
|
}
|
|
233
262
|
get name() {
|
|
@@ -1,7 +1,39 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
1
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
5
|
import { ApiKey } from '../authentication/apikey.js';
|
|
6
|
+
import { CODING_AGENT_ENV_VARS } from './coding-agent.js';
|
|
3
7
|
import { env } from './env.js';
|
|
4
8
|
import { isBrokenBunH2Runtime } from './h2-runtime.js';
|
|
9
|
+
/**
|
|
10
|
+
* These tests may run inside a coding agent, which sets the variables the SDK
|
|
11
|
+
* reads to tag its User-Agent. Clear them (and DO_NOT_TRACK) for each test.
|
|
12
|
+
*/
|
|
13
|
+
function withoutCodingAgentEnv() {
|
|
14
|
+
const names = [...CODING_AGENT_ENV_VARS, 'DO_NOT_TRACK', 'HOME', 'USERPROFILE'];
|
|
15
|
+
const saved = {};
|
|
16
|
+
return {
|
|
17
|
+
clear() {
|
|
18
|
+
for (const name of names) {
|
|
19
|
+
saved[name] = process.env[name];
|
|
20
|
+
}
|
|
21
|
+
for (const name of [...CODING_AGENT_ENV_VARS, 'DO_NOT_TRACK']) {
|
|
22
|
+
delete process.env[name];
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
restore() {
|
|
26
|
+
for (const name of names) {
|
|
27
|
+
if (saved[name] === undefined) {
|
|
28
|
+
delete process.env[name];
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
process.env[name] = saved[name];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
5
37
|
describe('Settings.apiVersion', () => {
|
|
6
38
|
beforeEach(() => {
|
|
7
39
|
// Reset the module-level settings singleton between tests if needed
|
|
@@ -218,6 +250,7 @@ describe('Settings H2 tuning knobs (config > env > default)', () => {
|
|
|
218
250
|
});
|
|
219
251
|
describe('Settings.integration', () => {
|
|
220
252
|
const sdkUserAgent = /^blaxel\/sdk\/typescript\/\S+ \([^)]+\) blaxel\/\S+/;
|
|
253
|
+
const agentEnv = withoutCodingAgentEnv();
|
|
221
254
|
async function authenticated() {
|
|
222
255
|
const { settings } = await import('./settings.js');
|
|
223
256
|
settings.credentials = new ApiKey({ apiKey: 'test-key', workspace: 'test-ws' });
|
|
@@ -225,7 +258,9 @@ describe('Settings.integration', () => {
|
|
|
225
258
|
delete process.env.BL_INTEGRATION;
|
|
226
259
|
return settings;
|
|
227
260
|
}
|
|
261
|
+
beforeEach(() => agentEnv.clear());
|
|
228
262
|
afterEach(async () => {
|
|
263
|
+
agentEnv.restore();
|
|
229
264
|
const { settings } = await import('./settings.js');
|
|
230
265
|
delete settings.config.integration;
|
|
231
266
|
delete process.env.BL_INTEGRATION;
|
|
@@ -286,3 +321,100 @@ describe('Settings.integration warnings', () => {
|
|
|
286
321
|
}
|
|
287
322
|
});
|
|
288
323
|
});
|
|
324
|
+
describe('Settings.codingAgent', () => {
|
|
325
|
+
const sdkUserAgent = /^blaxel\/sdk\/typescript\/\S+ \([^)]+\) blaxel\/\S+/;
|
|
326
|
+
const agentEnv = withoutCodingAgentEnv();
|
|
327
|
+
let home = '';
|
|
328
|
+
/** A fresh settings module, so it reads the empty test home directory. */
|
|
329
|
+
async function authenticated() {
|
|
330
|
+
const { settings } = await import('./settings.js');
|
|
331
|
+
settings.credentials = new ApiKey({ apiKey: 'test-key', workspace: 'test-ws' });
|
|
332
|
+
delete settings.config.integration;
|
|
333
|
+
delete process.env.BL_INTEGRATION;
|
|
334
|
+
return settings;
|
|
335
|
+
}
|
|
336
|
+
/** A fresh settings module whose home directory holds this config.yaml. */
|
|
337
|
+
async function withConfig(config) {
|
|
338
|
+
mkdirSync(join(home, '.blaxel'));
|
|
339
|
+
writeFileSync(join(home, '.blaxel', 'config.yaml'), config);
|
|
340
|
+
return authenticated();
|
|
341
|
+
}
|
|
342
|
+
// Keep the tracking choice saved on the machine running the tests out.
|
|
343
|
+
beforeEach(() => {
|
|
344
|
+
agentEnv.clear();
|
|
345
|
+
home = mkdtempSync(join(tmpdir(), 'blaxel-home-'));
|
|
346
|
+
process.env.HOME = home;
|
|
347
|
+
process.env.USERPROFILE = home;
|
|
348
|
+
vi.resetModules();
|
|
349
|
+
});
|
|
350
|
+
afterEach(() => {
|
|
351
|
+
agentEnv.restore();
|
|
352
|
+
delete process.env.BL_INTEGRATION;
|
|
353
|
+
rmSync(home, { recursive: true, force: true });
|
|
354
|
+
vi.resetModules();
|
|
355
|
+
});
|
|
356
|
+
it('sends no agent token outside a coding agent', async () => {
|
|
357
|
+
const settings = await authenticated();
|
|
358
|
+
expect(settings.codingAgent).toBe('');
|
|
359
|
+
expect(settings.headers['User-Agent']).toMatch(new RegExp(sdkUserAgent.source + '$'));
|
|
360
|
+
});
|
|
361
|
+
it('appends the agent token detected from the environment', async () => {
|
|
362
|
+
const settings = await authenticated();
|
|
363
|
+
process.env.CLAUDECODE = '1';
|
|
364
|
+
expect(settings.codingAgent).toBe('claude-code');
|
|
365
|
+
const userAgent = settings.headers['User-Agent'];
|
|
366
|
+
expect(userAgent).toMatch(sdkUserAgent);
|
|
367
|
+
expect(userAgent.endsWith(' agent/claude-code')).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
it('puts the agent token after the integration token', async () => {
|
|
370
|
+
const settings = await authenticated();
|
|
371
|
+
process.env.CODEX_SANDBOX = 'seatbelt';
|
|
372
|
+
settings.integration = 'herdr/1.4.0';
|
|
373
|
+
expect(settings.headers['User-Agent'].endsWith(' herdr/1.4.0 agent/codex')).toBe(true);
|
|
374
|
+
});
|
|
375
|
+
it.each(['1', 'true'])('drops the token when DO_NOT_TRACK=%s', async (value) => {
|
|
376
|
+
const settings = await authenticated();
|
|
377
|
+
process.env.CLAUDECODE = '1';
|
|
378
|
+
process.env.DO_NOT_TRACK = value;
|
|
379
|
+
expect(settings.codingAgent).toBe('');
|
|
380
|
+
expect(settings.headers['User-Agent']).toMatch(new RegExp(sdkUserAgent.source + '$'));
|
|
381
|
+
});
|
|
382
|
+
it('drops the token when the process sets DO_NOT_TRACK=1 and the project .env says false', async () => {
|
|
383
|
+
vi.doMock('./node.js', async (importOriginal) => {
|
|
384
|
+
const original = await importOriginal();
|
|
385
|
+
const fs = original.fs;
|
|
386
|
+
const readFileSync = ((file, ...rest) => file === '.env' ? 'DO_NOT_TRACK=false\n' : fs.readFileSync(file, ...rest));
|
|
387
|
+
return { ...original, fs: { ...fs, readFileSync } };
|
|
388
|
+
});
|
|
389
|
+
try {
|
|
390
|
+
const settings = await authenticated();
|
|
391
|
+
const { env: projectEnv } = await import('./env.js');
|
|
392
|
+
expect(projectEnv.DO_NOT_TRACK).toBe('false');
|
|
393
|
+
process.env.CLAUDECODE = '1';
|
|
394
|
+
process.env.DO_NOT_TRACK = '1';
|
|
395
|
+
expect(settings.codingAgent).toBe('');
|
|
396
|
+
expect(settings.headers['User-Agent']).toMatch(new RegExp(sdkUserAgent.source + '$'));
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
vi.doUnmock('./node.js');
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
it('keeps the token when DO_NOT_TRACK turns tracking on', async () => {
|
|
403
|
+
const settings = await authenticated();
|
|
404
|
+
process.env.CLAUDECODE = '1';
|
|
405
|
+
process.env.DO_NOT_TRACK = '0';
|
|
406
|
+
expect(settings.codingAgent).toBe('claude-code');
|
|
407
|
+
});
|
|
408
|
+
it('keeps the token when config.yaml says tracking: false', async () => {
|
|
409
|
+
// bl login saves tracking: false until the user opts in to error reports.
|
|
410
|
+
const settings = await withConfig('tracking: false\n');
|
|
411
|
+
process.env.CURSOR_AGENT = '1';
|
|
412
|
+
expect(settings.codingAgent).toBe('cursor');
|
|
413
|
+
});
|
|
414
|
+
it('keeps the token when no tracking choice was saved', async () => {
|
|
415
|
+
const settings = await withConfig('context:\n workspace: test-ws\n');
|
|
416
|
+
process.env.CURSOR_AGENT = '1';
|
|
417
|
+
expect(settings.tracking).toBe(false);
|
|
418
|
+
expect(settings.codingAgent).toBe('cursor');
|
|
419
|
+
});
|
|
420
|
+
});
|