patchwork-os 1.2.0-beta.3.canary.883 → 1.2.0-beta.3.canary.885
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/analyticsReporter.d.ts +40 -0
- package/dist/analyticsReporter.js +76 -0
- package/dist/analyticsReporter.js.map +1 -0
- package/dist/analyticsSend.d.ts +33 -17
- package/dist/analyticsSend.js +60 -26
- package/dist/analyticsSend.js.map +1 -1
- package/dist/bridge.d.ts +1 -0
- package/dist/bridge.js +20 -11
- package/dist/bridge.js.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/server.js +7 -16
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Periodic + shutdown usage reporting.
|
|
3
|
+
*
|
|
4
|
+
* Telemetry used to be sent only at bridge shutdown. Long-running bridges
|
|
5
|
+
* (launchd services, containers) almost never shut down, so they never
|
|
6
|
+
* reported. The reporter sends on an interval and once more at shutdown.
|
|
7
|
+
*
|
|
8
|
+
* Each activity entry is counted exactly once: a send covers entries newer
|
|
9
|
+
* than the previous send's cutoff, and the cutoff advances even when the send
|
|
10
|
+
* fails (a failed send is dropped, never retried — retrying would double-count
|
|
11
|
+
* if the collector had in fact stored it). Activity while telemetry is not
|
|
12
|
+
* opted in advances the cutoff too, so opting in later never uploads history
|
|
13
|
+
* the user had not consented to.
|
|
14
|
+
*/
|
|
15
|
+
import type { ActivityEntry } from "./activityTypes.js";
|
|
16
|
+
import { type AnalyticsSummary } from "./analyticsAggregator.js";
|
|
17
|
+
export declare const DEFAULT_REPORT_INTERVAL_MS: number;
|
|
18
|
+
export interface ReporterDeps {
|
|
19
|
+
/** Current in-memory activity entries (any order). */
|
|
20
|
+
entries: () => readonly ActivityEntry[];
|
|
21
|
+
/** True when the user has opted in. Checked at every flush. */
|
|
22
|
+
enabled: () => boolean;
|
|
23
|
+
bridgeVersion: string;
|
|
24
|
+
salt: () => string;
|
|
25
|
+
send: (summary: AnalyticsSummary) => Promise<void>;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
}
|
|
28
|
+
export declare class AnalyticsReporter {
|
|
29
|
+
private readonly deps;
|
|
30
|
+
private cutoffMs;
|
|
31
|
+
private windowStartMs;
|
|
32
|
+
private timer;
|
|
33
|
+
private inFlight;
|
|
34
|
+
constructor(deps: ReporterDeps);
|
|
35
|
+
start(intervalMs?: number): void;
|
|
36
|
+
stop(): void;
|
|
37
|
+
/** Send everything since the last cutoff. Never rejects. Serialised. */
|
|
38
|
+
flush(): Promise<void>;
|
|
39
|
+
private doFlush;
|
|
40
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Periodic + shutdown usage reporting.
|
|
3
|
+
*
|
|
4
|
+
* Telemetry used to be sent only at bridge shutdown. Long-running bridges
|
|
5
|
+
* (launchd services, containers) almost never shut down, so they never
|
|
6
|
+
* reported. The reporter sends on an interval and once more at shutdown.
|
|
7
|
+
*
|
|
8
|
+
* Each activity entry is counted exactly once: a send covers entries newer
|
|
9
|
+
* than the previous send's cutoff, and the cutoff advances even when the send
|
|
10
|
+
* fails (a failed send is dropped, never retried — retrying would double-count
|
|
11
|
+
* if the collector had in fact stored it). Activity while telemetry is not
|
|
12
|
+
* opted in advances the cutoff too, so opting in later never uploads history
|
|
13
|
+
* the user had not consented to.
|
|
14
|
+
*/
|
|
15
|
+
import { buildSummary } from "./analyticsAggregator.js";
|
|
16
|
+
export const DEFAULT_REPORT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
export class AnalyticsReporter {
|
|
18
|
+
deps;
|
|
19
|
+
cutoffMs;
|
|
20
|
+
windowStartMs;
|
|
21
|
+
timer = null;
|
|
22
|
+
inFlight = null;
|
|
23
|
+
constructor(deps) {
|
|
24
|
+
this.deps = deps;
|
|
25
|
+
const now = (deps.now ?? Date.now)();
|
|
26
|
+
this.cutoffMs = now;
|
|
27
|
+
this.windowStartMs = now;
|
|
28
|
+
}
|
|
29
|
+
start(intervalMs = DEFAULT_REPORT_INTERVAL_MS) {
|
|
30
|
+
if (this.timer)
|
|
31
|
+
return;
|
|
32
|
+
this.timer = setInterval(() => {
|
|
33
|
+
void this.flush();
|
|
34
|
+
}, intervalMs);
|
|
35
|
+
this.timer.unref?.();
|
|
36
|
+
}
|
|
37
|
+
stop() {
|
|
38
|
+
if (this.timer)
|
|
39
|
+
clearInterval(this.timer);
|
|
40
|
+
this.timer = null;
|
|
41
|
+
}
|
|
42
|
+
/** Send everything since the last cutoff. Never rejects. Serialised. */
|
|
43
|
+
flush() {
|
|
44
|
+
const run = (this.inFlight ?? Promise.resolve()).then(() => this.doFlush());
|
|
45
|
+
this.inFlight = run.finally(() => {
|
|
46
|
+
if (this.inFlight === run)
|
|
47
|
+
this.inFlight = null;
|
|
48
|
+
});
|
|
49
|
+
return run;
|
|
50
|
+
}
|
|
51
|
+
async doFlush() {
|
|
52
|
+
const now = (this.deps.now ?? Date.now)();
|
|
53
|
+
const since = this.cutoffMs;
|
|
54
|
+
const batch = this.deps.entries().filter((e) => {
|
|
55
|
+
const t = Date.parse(e.timestamp);
|
|
56
|
+
return Number.isFinite(t) && t > since && t <= now;
|
|
57
|
+
});
|
|
58
|
+
const windowMs = now - this.windowStartMs;
|
|
59
|
+
// Advance BEFORE sending: success, failure and opted-out all consume the
|
|
60
|
+
// window, so nothing is ever counted twice or uploaded retroactively.
|
|
61
|
+
this.cutoffMs = now;
|
|
62
|
+
if (batch.length === 0)
|
|
63
|
+
return;
|
|
64
|
+
this.windowStartMs = now;
|
|
65
|
+
if (!this.deps.enabled())
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
const summary = buildSummary(batch, Math.max(0, windowMs), this.deps.bridgeVersion, this.deps.salt());
|
|
69
|
+
await this.deps.send(summary);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Dropped by design — see the header.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=analyticsReporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyticsReporter.js","sourceRoot":"","sources":["../src/analyticsReporter.ts"],"names":[],"mappings":"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"}
|
package/dist/analyticsSend.d.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sends an anonymized analytics summary to the usage endpoint
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
2
|
+
* Sends an anonymized analytics summary to the usage endpoint (opt-in only;
|
|
3
|
+
* the caller checks the preference).
|
|
4
|
+
*
|
|
5
|
+
* - Callers await this with a timeout race; it never rejects.
|
|
6
|
+
* - Endpoint resolution (per call, so config changes apply without restart):
|
|
7
|
+
* 1. env PATCHWORK_ANALYTICS_ENDPOINT (CI / headless / containers)
|
|
8
|
+
* 2. config file ~/.claude/ide/analytics-config.json
|
|
9
|
+
* (managed by `patchwork analytics configure`)
|
|
10
|
+
* 3. DEFAULT_ANALYTICS_ENDPOINT
|
|
11
|
+
* Never read from the network or the payload — a redirect attacker cannot
|
|
12
|
+
* choose where telemetry goes.
|
|
13
|
+
* - Stops, in order: DO_NOT_TRACK (Console Do Not Track convention),
|
|
14
|
+
* PATCHWORK_TELEMETRY_DEBUG (prints the payload instead of sending), an
|
|
15
|
+
* engaged write kill switch.
|
|
16
|
+
* - Transport is `safeFetch`, never raw fetch, and never follows redirects.
|
|
17
|
+
* The default endpoint must resolve to a PUBLIC address; an operator-
|
|
18
|
+
* configured endpoint may be private or plain http (e.g. a collector on
|
|
19
|
+
* a private network), because the operator chose it.
|
|
14
20
|
*/
|
|
15
21
|
import type { AnalyticsSummary } from "./analyticsAggregator.js";
|
|
22
|
+
import { type SafeFetchOptions } from "./ssrfGuard.js";
|
|
23
|
+
/** The live public collector. The single definition; import it, never copy it. */
|
|
24
|
+
export declare const DEFAULT_ANALYTICS_ENDPOINT = "https://analytics.patchworkos.com/v1/usage";
|
|
16
25
|
export declare function resolveAnalyticsTarget(): {
|
|
17
26
|
endpoint: string;
|
|
18
27
|
key: string | undefined;
|
|
@@ -21,8 +30,15 @@ export declare function resolveAnalyticsTarget(): {
|
|
|
21
30
|
key: "env" | "config" | "none";
|
|
22
31
|
};
|
|
23
32
|
};
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
/** Why telemetry is suppressed in this environment, or null. */
|
|
34
|
+
export declare function telemetrySuppressed(env?: NodeJS.ProcessEnv): "do_not_track" | "debug" | null;
|
|
35
|
+
/** Injection seam for tests; production uses the defaults. */
|
|
36
|
+
export interface SendDeps {
|
|
37
|
+
fetchImpl?: SafeFetchOptions["fetchImpl"];
|
|
38
|
+
resolveDns?: SafeFetchOptions["resolveDns"];
|
|
39
|
+
killSwitchEngaged?: () => boolean;
|
|
40
|
+
writeDebug?: (line: string) => void;
|
|
41
|
+
onSent?: () => void;
|
|
42
|
+
}
|
|
43
|
+
/** Sends the summary. Resolves (never rejects) — all errors are swallowed. */
|
|
44
|
+
export declare function sendAnalytics(summary: AnalyticsSummary, deps?: SendDeps): Promise<void>;
|
package/dist/analyticsSend.js
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sends an anonymized analytics summary to the usage endpoint
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
2
|
+
* Sends an anonymized analytics summary to the usage endpoint (opt-in only;
|
|
3
|
+
* the caller checks the preference).
|
|
4
|
+
*
|
|
5
|
+
* - Callers await this with a timeout race; it never rejects.
|
|
6
|
+
* - Endpoint resolution (per call, so config changes apply without restart):
|
|
7
|
+
* 1. env PATCHWORK_ANALYTICS_ENDPOINT (CI / headless / containers)
|
|
8
|
+
* 2. config file ~/.claude/ide/analytics-config.json
|
|
9
|
+
* (managed by `patchwork analytics configure`)
|
|
10
|
+
* 3. DEFAULT_ANALYTICS_ENDPOINT
|
|
11
|
+
* Never read from the network or the payload — a redirect attacker cannot
|
|
12
|
+
* choose where telemetry goes.
|
|
13
|
+
* - Stops, in order: DO_NOT_TRACK (Console Do Not Track convention),
|
|
14
|
+
* PATCHWORK_TELEMETRY_DEBUG (prints the payload instead of sending), an
|
|
15
|
+
* engaged write kill switch.
|
|
16
|
+
* - Transport is `safeFetch`, never raw fetch, and never follows redirects.
|
|
17
|
+
* The default endpoint must resolve to a PUBLIC address; an operator-
|
|
18
|
+
* configured endpoint may be private or plain http (e.g. a collector on
|
|
19
|
+
* a private network), because the operator chose it.
|
|
14
20
|
*/
|
|
15
21
|
import { getAnalyticsConfig } from "./analyticsConfig.js";
|
|
16
22
|
import { recordAnalyticsSent } from "./analyticsPrefs.js";
|
|
17
|
-
|
|
23
|
+
import { readKillSwitch } from "./governance/killSwitchPolicy.js";
|
|
24
|
+
import { safeFetch } from "./ssrfGuard.js";
|
|
25
|
+
/** The live public collector. The single definition; import it, never copy it. */
|
|
26
|
+
export const DEFAULT_ANALYTICS_ENDPOINT = "https://analytics.patchworkos.com/v1/usage";
|
|
27
|
+
const SEND_TIMEOUT_MS = 3000;
|
|
18
28
|
function validUrl(value) {
|
|
19
29
|
if (!value)
|
|
20
30
|
return undefined;
|
|
@@ -33,7 +43,7 @@ export function resolveAnalyticsTarget() {
|
|
|
33
43
|
const envKey = process.env.PATCHWORK_ANALYTICS_KEY;
|
|
34
44
|
const cfg = getAnalyticsConfig();
|
|
35
45
|
const cfgEndpoint = validUrl(cfg.endpoint);
|
|
36
|
-
let endpoint =
|
|
46
|
+
let endpoint = DEFAULT_ANALYTICS_ENDPOINT;
|
|
37
47
|
let endpointSource = "default";
|
|
38
48
|
if (envEndpoint) {
|
|
39
49
|
endpoint = envEndpoint;
|
|
@@ -59,14 +69,34 @@ export function resolveAnalyticsTarget() {
|
|
|
59
69
|
source: { endpoint: endpointSource, key: keySource },
|
|
60
70
|
};
|
|
61
71
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
function truthy(v) {
|
|
73
|
+
if (v === undefined)
|
|
74
|
+
return false;
|
|
75
|
+
const s = v.trim().toLowerCase();
|
|
76
|
+
return s !== "" && s !== "0" && s !== "false" && s !== "no";
|
|
77
|
+
}
|
|
78
|
+
/** Why telemetry is suppressed in this environment, or null. */
|
|
79
|
+
export function telemetrySuppressed(env = process.env) {
|
|
80
|
+
if (truthy(env.DO_NOT_TRACK))
|
|
81
|
+
return "do_not_track";
|
|
82
|
+
if (truthy(env.PATCHWORK_TELEMETRY_DEBUG))
|
|
83
|
+
return "debug";
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/** Sends the summary. Resolves (never rejects) — all errors are swallowed. */
|
|
87
|
+
export async function sendAnalytics(summary, deps = {}) {
|
|
68
88
|
try {
|
|
69
|
-
const
|
|
89
|
+
const suppressed = telemetrySuppressed();
|
|
90
|
+
if (suppressed === "do_not_track")
|
|
91
|
+
return;
|
|
92
|
+
if (suppressed === "debug") {
|
|
93
|
+
(deps.writeDebug ?? ((l) => process.stderr.write(l)))(`[telemetry debug — not sent] ${JSON.stringify(summary)}\n`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const engaged = deps.killSwitchEngaged ?? (() => readKillSwitch().engaged);
|
|
97
|
+
if (engaged())
|
|
98
|
+
return;
|
|
99
|
+
const { endpoint, key, source } = resolveAnalyticsTarget();
|
|
70
100
|
const controller = new AbortController();
|
|
71
101
|
const timer = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
|
|
72
102
|
try {
|
|
@@ -75,22 +105,26 @@ export async function sendAnalytics(summary) {
|
|
|
75
105
|
};
|
|
76
106
|
if (key)
|
|
77
107
|
headers["X-Analytics-Key"] = key;
|
|
78
|
-
const
|
|
108
|
+
const { response } = await safeFetch(endpoint, {
|
|
79
109
|
method: "POST",
|
|
80
110
|
headers,
|
|
81
111
|
body: JSON.stringify(summary),
|
|
82
112
|
signal: controller.signal,
|
|
113
|
+
}, {
|
|
114
|
+
allowPrivate: source.endpoint !== "default",
|
|
115
|
+
followRedirects: false,
|
|
116
|
+
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
117
|
+
...(deps.resolveDns ? { resolveDns: deps.resolveDns } : {}),
|
|
83
118
|
});
|
|
84
|
-
if (
|
|
85
|
-
recordAnalyticsSent();
|
|
86
|
-
}
|
|
119
|
+
if (response.ok)
|
|
120
|
+
(deps.onSent ?? recordAnalyticsSent)();
|
|
87
121
|
}
|
|
88
122
|
finally {
|
|
89
123
|
clearTimeout(timer);
|
|
90
124
|
}
|
|
91
125
|
}
|
|
92
126
|
catch {
|
|
93
|
-
//
|
|
127
|
+
// Telemetry must never surface to the user or affect the bridge.
|
|
94
128
|
}
|
|
95
129
|
}
|
|
96
130
|
//# sourceMappingURL=analyticsSend.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyticsSend.js","sourceRoot":"","sources":["../src/analyticsSend.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"analyticsSend.js","sourceRoot":"","sources":["../src/analyticsSend.ts"],"names":[],"mappings":"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"}
|
package/dist/bridge.d.ts
CHANGED
package/dist/bridge.js
CHANGED
|
@@ -4,8 +4,8 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { WebSocket } from "ws";
|
|
6
6
|
import { ActivityLog } from "./activityLog.js";
|
|
7
|
-
import { buildSummary } from "./analyticsAggregator.js";
|
|
8
7
|
import { getAnalyticsPref, getAnalyticsSalt } from "./analyticsPrefs.js";
|
|
8
|
+
import { AnalyticsReporter } from "./analyticsReporter.js";
|
|
9
9
|
import { sendAnalytics } from "./analyticsSend.js";
|
|
10
10
|
import { dispatchCancelPush, enqueueApprovalWithDispatch, } from "./approvalHttp.js";
|
|
11
11
|
import { resolveApprovalLogDir } from "./approvalPersistence.js";
|
|
@@ -116,6 +116,7 @@ export class Bridge {
|
|
|
116
116
|
server;
|
|
117
117
|
extensionClient;
|
|
118
118
|
activityLog;
|
|
119
|
+
analyticsReporter = null;
|
|
119
120
|
authToken;
|
|
120
121
|
sessions = new Map();
|
|
121
122
|
/**
|
|
@@ -958,6 +959,18 @@ export class Bridge {
|
|
|
958
959
|
async start() {
|
|
959
960
|
// 0. Initialize OpenTelemetry (no-op when OTEL_EXPORTER_OTLP_ENDPOINT is unset)
|
|
960
961
|
initTelemetry();
|
|
962
|
+
// 0. Opt-in usage telemetry: daily + at shutdown, so long-running bridges
|
|
963
|
+
// report too. The preference is re-read at every flush.
|
|
964
|
+
this.analyticsReporter = new AnalyticsReporter({
|
|
965
|
+
entries: () => this.activityLog.queryAll(),
|
|
966
|
+
enabled: () => (this.config.analyticsEnabled !== null
|
|
967
|
+
? this.config.analyticsEnabled
|
|
968
|
+
: getAnalyticsPref()) === true,
|
|
969
|
+
bridgeVersion: PACKAGE_VERSION,
|
|
970
|
+
salt: getAnalyticsSalt,
|
|
971
|
+
send: (summary) => sendAnalytics(summary),
|
|
972
|
+
});
|
|
973
|
+
this.analyticsReporter.start();
|
|
961
974
|
// 0a. Snapshot env-derived kill-switch flags. After this, plugins or
|
|
962
975
|
// recipe steps mutating `process.env.PATCHWORK_FLAG_*` cannot disable an
|
|
963
976
|
// active emergency stop. Closes MED-2 from the 2026-04-28 audit.
|
|
@@ -2244,22 +2257,18 @@ export class Bridge {
|
|
|
2244
2257
|
? `, ${totalErrors} error${totalErrors === 1 ? "" : "s"}`
|
|
2245
2258
|
: "";
|
|
2246
2259
|
this.logger.info(`Shutdown complete — ${totalSessions} session${totalSessions === 1 ? "" : "s"}, ${totalCalls} tool call${totalCalls === 1 ? "" : "s"}${shutdownErrorPart}`);
|
|
2247
|
-
//
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
if (analyticsOn === true && totalSessions > 0) {
|
|
2260
|
+
// Final telemetry flush (opt-in only; the reporter checks). Covers activity
|
|
2261
|
+
// since the last periodic send. Awaited with a 2s cap before exit.
|
|
2262
|
+
if (this.analyticsReporter) {
|
|
2263
|
+
this.analyticsReporter.stop();
|
|
2252
2264
|
try {
|
|
2253
|
-
const entries = this.activityLog.query({ last: 500 });
|
|
2254
|
-
const salt = getAnalyticsSalt();
|
|
2255
|
-
const summary = buildSummary(entries, maxDurationMs, PACKAGE_VERSION, salt);
|
|
2256
2265
|
await Promise.race([
|
|
2257
|
-
|
|
2266
|
+
this.analyticsReporter.flush(),
|
|
2258
2267
|
new Promise((resolve) => setTimeout(resolve, 2000)),
|
|
2259
2268
|
]);
|
|
2260
2269
|
}
|
|
2261
2270
|
catch {
|
|
2262
|
-
//
|
|
2271
|
+
// Telemetry must never affect shutdown.
|
|
2263
2272
|
}
|
|
2264
2273
|
}
|
|
2265
2274
|
// Send aggregate session-end notification to the extension before disconnecting
|