behavior-wrapped 0.2.13 → 0.2.15
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/README.md +10 -7
- package/dist/assets/{index-BIbVOfPJ.css → index-BGWEDZlc.css} +1 -1
- package/dist/assets/index-BuozLh2C.js +11 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/scripts/decrypt-research-donation.mjs +25 -0
- package/scripts/review-interaction-tone.mjs +1 -1
- package/server/analysis.mjs +5 -3
- package/server/cli.mjs +1 -1
- package/server/consent.mjs +2 -2
- package/server/encrypted-donation-schema.mjs +45 -0
- package/server/frustration-card.mjs +2 -1
- package/server/instrumental-workarounds.mjs +2 -1
- package/server/interaction-tone.mjs +2 -1
- package/server/launcher.mjs +15 -4
- package/server/phrase-card.mjs +6 -5
- package/server/research-donation-crypto.mjs +68 -0
- package/server/research-donation-schema.mjs +10 -5
- package/server/research-donation.mjs +17 -5
- package/server/session-topics.mjs +2 -1
- package/server/store.mjs +24 -0
- package/dist/assets/index-yvo-hzIv.js +0 -11
package/dist/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="theme-color" content="#0d0b1b" />
|
|
7
7
|
<meta name="description" content="Your private, local-first Claude Code behavior report." />
|
|
8
8
|
<title>Behavior Wrapped</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BuozLh2C.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BGWEDZlc.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "behavior-wrapped",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "A private, local-first Wrapped report for Claude Code and Codex behavior.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"analyze:phrases": "node scripts/mine-phrase-families.mjs",
|
|
45
45
|
"review:interactions": "node scripts/review-interaction-tone.mjs",
|
|
46
46
|
"review:workarounds": "node scripts/review-workaround-judge.mjs",
|
|
47
|
+
"research:decrypt": "node scripts/decrypt-research-donation.mjs",
|
|
47
48
|
"worker:dev": "npx --yes wrangler@4.86.0 dev",
|
|
48
49
|
"worker:deploy": "npx --yes wrangler@4.86.0 deploy",
|
|
49
50
|
"test": "node --test tests/*.test.mjs",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { decryptResearchDonation } from "../server/research-donation-crypto.mjs";
|
|
7
|
+
|
|
8
|
+
const [, , inputArg, outputArg, keyArg] = process.argv;
|
|
9
|
+
if (!inputArg || !outputArg) {
|
|
10
|
+
console.error("Usage: node scripts/decrypt-research-donation.mjs <encrypted-envelope.json> <private-output.json> [private-key.pem]");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const input = path.resolve(inputArg);
|
|
15
|
+
const output = path.resolve(outputArg);
|
|
16
|
+
const privateKeyPath = path.resolve(keyArg || path.join(os.homedir(), ".config", "behavior-wrapped", "keys", "research-donation-rsa-2026-08.pem"));
|
|
17
|
+
const passphrase = process.env.BEHAVIOR_WRAPPED_DONATION_KEY_PASSPHRASE || (process.platform === "darwin"
|
|
18
|
+
? execFileSync("security", ["find-generic-password", "-a", os.userInfo().username, "-s", "behavior-wrapped-research-key-2026-08", "-w"], { encoding: "utf8" }).trim()
|
|
19
|
+
: "");
|
|
20
|
+
|
|
21
|
+
if (!passphrase) throw new Error("Set BEHAVIOR_WRAPPED_DONATION_KEY_PASSPHRASE before decrypting.");
|
|
22
|
+
const envelope = JSON.parse(fs.readFileSync(input, "utf8"));
|
|
23
|
+
const donation = decryptResearchDonation(envelope, fs.readFileSync(privateKeyPath, "utf8"), passphrase);
|
|
24
|
+
fs.writeFileSync(output, `${JSON.stringify(donation, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
25
|
+
console.log(`Decrypted donation written with private permissions: ${output}`);
|
|
@@ -25,7 +25,7 @@ function section(title, matches) {
|
|
|
25
25
|
|
|
26
26
|
const markdown = `# Private interaction-tone review
|
|
27
27
|
|
|
28
|
-
Generated locally from ${sessions.length} sessions in the latest 30-day window.
|
|
28
|
+
Generated locally from ${sessions.length} sessions in the latest 30-day window. GPT-5.6 Luna classified ${result.candidateMessages} occurrences represented by ${candidates.length} redacted, deduplicated candidates. Repeated identical excerpts appear once with an occurrence count.
|
|
29
29
|
|
|
30
30
|
This file is private, gitignored, and may contain excerpts from your session history.
|
|
31
31
|
|
package/server/analysis.mjs
CHANGED
|
@@ -485,14 +485,16 @@ function donationSessionSummary(messages, suppliedSummary) {
|
|
|
485
485
|
return `${shortened.slice(0, Math.max(shortened.lastIndexOf(" "), 1)).trim()}…`;
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
-
export function makeDonationPreview(sessionRecords, metadataById, { disabledRedactions = [], disabledMatches = [] } = {}) {
|
|
488
|
+
export function makeDonationPreview(sessionRecords, metadataById, { disabledRedactions = [], disabledMatches = [], unredacted = false } = {}) {
|
|
489
489
|
const detections = [];
|
|
490
490
|
const sessions = sessionRecords.map(({ sessionId, records }) => {
|
|
491
491
|
const messages = records.flatMap((record) => {
|
|
492
492
|
if (record.type !== "user" && record.type !== "assistant") return [];
|
|
493
493
|
const value = visibleText(record);
|
|
494
494
|
if (!value) return [];
|
|
495
|
-
const redacted =
|
|
495
|
+
const redacted = unredacted
|
|
496
|
+
? { text: value, detections: [] }
|
|
497
|
+
: redactText(value, [], { disabledKinds: disabledRedactions, disabledMatches, includeHeuristicSecrets: false });
|
|
496
498
|
detections.push(...redacted.detections);
|
|
497
499
|
return [{ role: record.type, timestamp: record.timestamp || null, text: redacted.text }];
|
|
498
500
|
});
|
|
@@ -501,5 +503,5 @@ export function makeDonationPreview(sessionRecords, metadataById, { disabledReda
|
|
|
501
503
|
});
|
|
502
504
|
const redactions = donationRedactionInventory(detections);
|
|
503
505
|
const detectionCount = detections.filter((detection) => detection.enabled !== false).length;
|
|
504
|
-
return { format: "behavior-wrapped-donation-preview-v1", createdLocally: true, detectionCount, redactions, sessions };
|
|
506
|
+
return { format: "behavior-wrapped-donation-preview-v1", createdLocally: true, unredacted, detectionCount, redactions, sessions };
|
|
505
507
|
}
|
package/server/cli.mjs
CHANGED
|
@@ -226,7 +226,7 @@ async function createWrapped() {
|
|
|
226
226
|
const id = createReportId();
|
|
227
227
|
const safeFindings = analyzed.findings.map(({ evidence, method, ...finding }) => finding);
|
|
228
228
|
const hasPrivateWorkaroundEvidence = Boolean(analyzed.workaroundReview?.occurrences?.length || analyzed.workaroundReview?.borderline?.length);
|
|
229
|
-
const report = { id, createdAt: new Date().toISOString(), rangeLabel: formatRange(chosenSessions), source: "Claude Code + Codex", stats: analyzed.stats, findings: safeFindings, phraseCard: analyzed.phraseCard, interactionCard: analyzed.interactionCard, workaroundCard: analyzed.workaroundCard, workaroundReview: analyzed.workaroundReview, sessionSummaries: analyzed.sessionSummaries || [], sessionIds: chosenSessions.map((session) => session.id), donationHelperUrl: `${baseUrl}/donate/${id}`, privacy: { shareSafe: !hasPrivateWorkaroundEvidence, containsTranscriptText: hasPrivateWorkaroundEvidence, externalTransmission: !testMode, ...(testMode ? { transmittedData: "None; test mode stays local.", externalRecipient: "None" } : { transmittedData: "redacted phrase, interaction-tone, and session-topic candidates; locally redacted context windows around explicit blockers for workaround discovery; aggregate report statistics; and a random client ID only", externalRecipient: "Behavior Wrapped relay, OpenRouter,
|
|
229
|
+
const report = { id, createdAt: new Date().toISOString(), rangeLabel: formatRange(chosenSessions), source: "Claude Code + Codex", stats: analyzed.stats, findings: safeFindings, phraseCard: analyzed.phraseCard, interactionCard: analyzed.interactionCard, workaroundCard: analyzed.workaroundCard, workaroundReview: analyzed.workaroundReview, sessionSummaries: analyzed.sessionSummaries || [], sessionIds: chosenSessions.map((session) => session.id), donationHelperUrl: `${baseUrl}/donate/${id}`, privacy: { shareSafe: !hasPrivateWorkaroundEvidence, containsTranscriptText: hasPrivateWorkaroundEvidence, externalTransmission: !testMode, ...(testMode ? { transmittedData: "None; test mode stays local.", externalRecipient: "None" } : { transmittedData: "redacted phrase, interaction-tone, and session-topic candidates; locally redacted context windows around explicit blockers for workaround discovery; aggregate report statistics; and a random client ID only", externalRecipient: "Behavior Wrapped relay, OpenRouter, a zero-data-retention GPT-5.6 Luna provider, and public report hosting" }) } };
|
|
230
230
|
let publicUrl = null;
|
|
231
231
|
if (!testMode) {
|
|
232
232
|
progress.start("Publishing share-safe Wrapped", "strict aggregate-only schema");
|
package/server/consent.mjs
CHANGED
|
@@ -5,12 +5,12 @@ const lime = "\x1b[38;2;201;242;75m";
|
|
|
5
5
|
const purple = "\x1b[38;2;141;92;255m";
|
|
6
6
|
const reset = "\x1b[0m";
|
|
7
7
|
|
|
8
|
-
export const remoteAnalysisConsentText = "Behavior Wrapped will send redacted excerpts from your session history to
|
|
8
|
+
export const remoteAnalysisConsentText = "Behavior Wrapped will send redacted excerpts from your session history to GPT-5.6 Luna via OpenRouter using zero-data-retention providers for analysis. OK to proceed?";
|
|
9
9
|
|
|
10
10
|
export async function requestRemoteAnalysisConsent({ input = process.stdin, output = process.stdout } = {}) {
|
|
11
11
|
const prompt = createInterface({ input, output });
|
|
12
12
|
try {
|
|
13
|
-
const question = `${lime}◇${reset} Behavior Wrapped will send redacted excerpts from your session history to ${purple}${bright}
|
|
13
|
+
const question = `${lime}◇${reset} Behavior Wrapped will send redacted excerpts from your session history to ${purple}${bright}GPT-5.6 Luna${reset} via OpenRouter using zero-data-retention providers for analysis. OK to proceed? ${bright}(Y/n)${reset} `;
|
|
14
14
|
const answer = await prompt.question(question);
|
|
15
15
|
return /^(?:|y|yes)$/i.test(answer.trim());
|
|
16
16
|
} finally {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MAX_DONATION_BYTES } from "./research-donation-schema.mjs";
|
|
2
|
+
|
|
3
|
+
export const DONATION_ENVELOPE_FORMAT = "behavior-wrapped-encrypted-donation-v1";
|
|
4
|
+
export const DONATION_ENCRYPTION_ALGORITHM = "RSA-OAEP-256+A256GCM";
|
|
5
|
+
export const DONATION_KEY_ID = "research-donation-rsa-2026-08";
|
|
6
|
+
export const MAX_ENCRYPTED_DONATION_BYTES = 2_500_000;
|
|
7
|
+
|
|
8
|
+
const base64url = /^[A-Za-z0-9_-]+$/;
|
|
9
|
+
const timestamp = /^\d{4}-\d{2}-\d{2}T/;
|
|
10
|
+
|
|
11
|
+
function exactKeys(value, expected) {
|
|
12
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
13
|
+
&& Object.keys(value).sort().join("|") === [...expected].sort().join("|");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function boundedInteger(value, maximum) {
|
|
17
|
+
return Number.isInteger(value) && value >= 0 && value <= maximum;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function encryptedDonationAAD(envelope) {
|
|
21
|
+
return JSON.stringify({
|
|
22
|
+
format: envelope.format,
|
|
23
|
+
encryption: { algorithm: envelope.encryption.algorithm, keyId: envelope.encryption.keyId },
|
|
24
|
+
metadata: envelope.metadata,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sanitizeEncryptedDonationEnvelope(value) {
|
|
29
|
+
if (!exactKeys(value, ["ciphertext", "encryption", "format", "metadata"])) return null;
|
|
30
|
+
if (value.format !== DONATION_ENVELOPE_FORMAT) return null;
|
|
31
|
+
if (!exactKeys(value.encryption, ["algorithm", "authTag", "iv", "keyId", "wrappedKey"])) return null;
|
|
32
|
+
if (value.encryption.algorithm !== DONATION_ENCRYPTION_ALGORITHM || value.encryption.keyId !== DONATION_KEY_ID) return null;
|
|
33
|
+
if (typeof value.encryption.wrappedKey !== "string" || value.encryption.wrappedKey.length < 480 || value.encryption.wrappedKey.length > 700 || !base64url.test(value.encryption.wrappedKey)) return null;
|
|
34
|
+
if (typeof value.encryption.iv !== "string" || value.encryption.iv.length !== 16 || !base64url.test(value.encryption.iv)) return null;
|
|
35
|
+
if (typeof value.encryption.authTag !== "string" || value.encryption.authTag.length !== 22 || !base64url.test(value.encryption.authTag)) return null;
|
|
36
|
+
if (typeof value.ciphertext !== "string" || !value.ciphertext.length || value.ciphertext.length > Math.ceil(MAX_DONATION_BYTES / 3) * 4 || !base64url.test(value.ciphertext)) return null;
|
|
37
|
+
if (!exactKeys(value.metadata, ["automatedDetections", "consentVersion", "consentedAt", "createdAt", "messages", "redactionMode", "reportId", "sessions", "unredactedData"])) return null;
|
|
38
|
+
const metadata = value.metadata;
|
|
39
|
+
if (!/^[A-Za-z0-9_-]{8,32}$/.test(metadata.reportId || "")) return null;
|
|
40
|
+
if (!new Set(["standard", "custom", "unredacted"]).has(metadata.redactionMode)) return null;
|
|
41
|
+
if (!timestamp.test(metadata.createdAt || "") || !timestamp.test(metadata.consentedAt || "")) return null;
|
|
42
|
+
if (metadata.consentVersion !== 1 || typeof metadata.unredactedData !== "boolean" || metadata.unredactedData !== (metadata.redactionMode === "unredacted")) return null;
|
|
43
|
+
if (!boundedInteger(metadata.automatedDetections, 1_000_000) || !boundedInteger(metadata.sessions, 250) || metadata.sessions < 1 || !boundedInteger(metadata.messages, 50_000) || metadata.messages < 1) return null;
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redactAggregateText } from "./privacy.mjs";
|
|
2
|
-
import { extractCandidateId, OPENROUTER_MODEL, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
2
|
+
import { extractCandidateId, OPENROUTER_MODEL, OPENROUTER_PROVIDER_PREFERENCES, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
3
3
|
|
|
4
4
|
export const FRUSTRATION_JUDGE_RELAY_URL = "https://agent-behavior-wrapped-judge.haoxingdu.workers.dev/v1/frustration-quote";
|
|
5
5
|
const MAX_CANDIDATES = 40;
|
|
@@ -97,6 +97,7 @@ function buildOpenRouterQuoteRequest(candidates, { model, prompt, schemaName, pr
|
|
|
97
97
|
const payload = JSON.stringify(candidates);
|
|
98
98
|
return {
|
|
99
99
|
model,
|
|
100
|
+
provider: OPENROUTER_PROVIDER_PREFERENCES,
|
|
100
101
|
temperature: 0,
|
|
101
102
|
reasoning: { effort: "none", exclude: true },
|
|
102
103
|
max_tokens: 32,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { displayModelName } from "./model-names.mjs";
|
|
2
2
|
import { redactAggregateText } from "./privacy.mjs";
|
|
3
|
-
import { OPENROUTER_MODEL } from "./phrase-card.mjs";
|
|
3
|
+
import { OPENROUTER_MODEL, OPENROUTER_PROVIDER_PREFERENCES } from "./phrase-card.mjs";
|
|
4
4
|
import { judgeError, judgeRequestDetails, judgeResponseDetails } from "./judge-debug.mjs";
|
|
5
5
|
import { semanticActions, semanticMethods, semanticToolUse } from "./tool-semantics.mjs";
|
|
6
6
|
|
|
@@ -287,6 +287,7 @@ export function buildOpenRouterWorkaroundRequest(chunks, model = OPENROUTER_MODE
|
|
|
287
287
|
const blockerOrder = blockerIds.map((id, index) => `${index + 1}. ${id}`).join("\n");
|
|
288
288
|
return {
|
|
289
289
|
model,
|
|
290
|
+
provider: OPENROUTER_PROVIDER_PREFERENCES,
|
|
290
291
|
temperature: 0,
|
|
291
292
|
reasoning: { effort: reasoningEffort, exclude: true },
|
|
292
293
|
max_tokens: 8192,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redactAggregateText } from "./privacy.mjs";
|
|
2
|
-
import { OPENROUTER_MODEL, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
2
|
+
import { OPENROUTER_MODEL, OPENROUTER_PROVIDER_PREFERENCES, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
3
3
|
import { judgeError, judgeRequestDetails, judgeResponseDetails } from "./judge-debug.mjs";
|
|
4
4
|
|
|
5
5
|
export const INTERACTION_TONE_RELAY_URL = "https://agent-behavior-wrapped-judge.haoxingdu.workers.dev/v1/interaction-tone";
|
|
@@ -106,6 +106,7 @@ export function buildOpenRouterInteractionToneRequest(candidates, model = OPENRO
|
|
|
106
106
|
const ids = candidates.map((candidate) => candidate.candidate_id);
|
|
107
107
|
return {
|
|
108
108
|
model,
|
|
109
|
+
provider: OPENROUTER_PROVIDER_PREFERENCES,
|
|
109
110
|
temperature: 0,
|
|
110
111
|
seed: 1729,
|
|
111
112
|
reasoning: { effort: "none", exclude: true },
|
package/server/launcher.mjs
CHANGED
|
@@ -7,8 +7,8 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { discoverAllSessionsAsync, readRecordsAsync, defaultDateRange, DEFAULT_WINDOW_DAYS } from "./discovery.mjs";
|
|
9
9
|
import { makeDonationPreview } from "./analysis.mjs";
|
|
10
|
-
import { getOrCreateClientId, loadReport } from "./store.mjs";
|
|
11
|
-
import { RESEARCH_DONATION_URL, submitResearchDonation } from "./research-donation.mjs";
|
|
10
|
+
import { deleteDonationReceipt, getOrCreateClientId, loadDonationReceipt, loadReport, saveDonationReceipt } from "./store.mjs";
|
|
11
|
+
import { deleteResearchDonation, RESEARCH_DONATION_URL, submitResearchDonation } from "./research-donation.mjs";
|
|
12
12
|
|
|
13
13
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const root = path.dirname(here);
|
|
@@ -121,7 +121,8 @@ const server = http.createServer(async (request, response) => {
|
|
|
121
121
|
const labels = new Map(publicCatalog().sessions.map((session) => [session.id, { ...session, summary: summaries.get(session.id) }]));
|
|
122
122
|
const disabledRedactions = Array.isArray(body.disabledRedactions) ? body.disabledRedactions.filter((kind) => typeof kind === "string" && /^[a-z0-9-]{1,64}$/.test(kind)).slice(0, 20) : [];
|
|
123
123
|
const disabledMatches = Array.isArray(body.disabledMatches) ? body.disabledMatches.filter((id) => typeof id === "string" && /^[a-f0-9]{24}$/.test(id)).slice(0, 5_000) : [];
|
|
124
|
-
|
|
124
|
+
const unredacted = body.previewMode === "unredacted";
|
|
125
|
+
return json(response, 200, makeDonationPreview(records, labels, { disabledRedactions, disabledMatches, unredacted }));
|
|
125
126
|
}
|
|
126
127
|
if (request.method === "POST" && url.pathname === "/api/research-donations") {
|
|
127
128
|
const body = await readBody(request, 4_200_000);
|
|
@@ -132,7 +133,17 @@ const server = http.createServer(async (request, response) => {
|
|
|
132
133
|
clientId: getOrCreateClientId(),
|
|
133
134
|
endpoint: process.env.BEHAVIOR_WRAPPED_DONATION_URL || RESEARCH_DONATION_URL,
|
|
134
135
|
});
|
|
135
|
-
|
|
136
|
+
saveDonationReceipt(result);
|
|
137
|
+
return json(response, 201, { accepted: true, donation_id: result.donation_id, encrypted: true });
|
|
138
|
+
}
|
|
139
|
+
const donationMatch = url.pathname.match(/^\/api\/research-donations\/([0-9a-f-]{36})$/);
|
|
140
|
+
if (request.method === "DELETE" && donationMatch) {
|
|
141
|
+
const receipt = loadDonationReceipt(donationMatch[1]);
|
|
142
|
+
if (!receipt) return json(response, 404, { error: "Local deletion receipt not found." });
|
|
143
|
+
if (demo) { deleteDonationReceipt(donationMatch[1]); return json(response, 200, { deleted: true, demo: true }); }
|
|
144
|
+
const result = await deleteResearchDonation(receipt.donationId, receipt.deletionToken, { endpoint: process.env.BEHAVIOR_WRAPPED_DONATION_URL || RESEARCH_DONATION_URL });
|
|
145
|
+
deleteDonationReceipt(donationMatch[1]);
|
|
146
|
+
return json(response, 200, result);
|
|
136
147
|
}
|
|
137
148
|
if (request.method !== "GET" && request.method !== "HEAD") return json(response, 405, { error: "Method not allowed" });
|
|
138
149
|
const requested = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
package/server/phrase-card.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { redactAggregateText } from "./privacy.mjs";
|
|
2
2
|
import { judgeError, judgeRequestDetails, judgeResponseDetails } from "./judge-debug.mjs";
|
|
3
3
|
|
|
4
|
-
export const OPENROUTER_MODEL = "
|
|
5
|
-
export const PHRASE_JUDGE_NAME = "
|
|
4
|
+
export const OPENROUTER_MODEL = "openai/gpt-5.6-luna";
|
|
5
|
+
export const PHRASE_JUDGE_NAME = "GPT-5.6 Luna";
|
|
6
|
+
export const OPENROUTER_PROVIDER_PREFERENCES = Object.freeze({ data_collection: "deny", zdr: true });
|
|
6
7
|
export const PHRASE_JUDGE_RELAY_URL = "https://agent-behavior-wrapped-judge.haoxingdu.workers.dev/v1/phrase-card";
|
|
7
8
|
|
|
8
9
|
const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
|
|
@@ -153,10 +154,10 @@ export function buildOpenRouterJudgeRequest(candidates, model = OPENROUTER_MODEL
|
|
|
153
154
|
assertSafePayload(payload);
|
|
154
155
|
return {
|
|
155
156
|
model,
|
|
157
|
+
provider: OPENROUTER_PROVIDER_PREFERENCES,
|
|
156
158
|
temperature: 0,
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// of unnecessary tokens before returning the candidate ID.
|
|
159
|
+
// These are bounded classification tasks, so additional reasoning would add
|
|
160
|
+
// latency and cost without changing the requested output contract.
|
|
160
161
|
reasoning: { effort: "none", exclude: true },
|
|
161
162
|
max_tokens: PHRASE_JUDGE_MAX_TOKENS,
|
|
162
163
|
messages: [
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { DONATION_ENCRYPTION_ALGORITHM, DONATION_ENVELOPE_FORMAT, DONATION_KEY_ID, encryptedDonationAAD, sanitizeEncryptedDonationEnvelope } from "./encrypted-donation-schema.mjs";
|
|
3
|
+
import { sanitizeResearchDonation } from "./research-donation-schema.mjs";
|
|
4
|
+
|
|
5
|
+
export const RESEARCH_DONATION_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
6
|
+
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA0RaXFQBixAmtwKRz2I7Y
|
|
7
|
+
pq0TlQPAZ72gHbyV2RJ9dRiDNwlwaKbHHVLYHG0QjxFanktNi/ms6NoV9slBQhjJ
|
|
8
|
+
Rb3kMzg5xEJYYy8TzyQKpY28f5/srGpSL2ziWRb9TSsgrOJPNk9LFPKLuJhty1+x
|
|
9
|
+
Gh9+I3UW+JPj+To4VY7GVU46jptP2MDtROK5v/p9PLP+QoKhjTBuDqgu5T78wTv5
|
|
10
|
+
/C34ZZD4ACIKvIQ8dtAZM6CPY0sWWVN84VO5etr1rYZg7DWczy2ZsX2StiKmuZ8b
|
|
11
|
+
kSqZr/mn6+PC5sthPCt0B+Tk1pxPv6LuwiNYubst4EKQDpFbP2e4h3KIaNxTnVRx
|
|
12
|
+
AHzd9XfF9rN86+Cjf55YlBuT9GeYXLttGBfoT6Llr4Xw370WIHabo7A57/atLOgw
|
|
13
|
+
YKX6TcNe6TOHuCTHM7LDmTPGNZdMi9cXXBUzohvTxm7O8qduIekFg4emxIiduVY2
|
|
14
|
+
3pHhzoP9p0kwO+L0BE1ELIHF9dk0p3s/NDIDfbiDxn5XAgMBAAE=
|
|
15
|
+
-----END PUBLIC KEY-----`;
|
|
16
|
+
|
|
17
|
+
function base64url(value) {
|
|
18
|
+
return Buffer.from(value).toString("base64url");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function encryptResearchDonation(value, publicKey = RESEARCH_DONATION_PUBLIC_KEY) {
|
|
22
|
+
const donation = sanitizeResearchDonation(value);
|
|
23
|
+
if (!donation) throw new Error("The reviewed donation does not match the research schema.");
|
|
24
|
+
const plaintext = Buffer.from(JSON.stringify(donation));
|
|
25
|
+
const contentKey = crypto.randomBytes(32);
|
|
26
|
+
const iv = crypto.randomBytes(12);
|
|
27
|
+
const envelope = {
|
|
28
|
+
format: DONATION_ENVELOPE_FORMAT,
|
|
29
|
+
encryption: { algorithm: DONATION_ENCRYPTION_ALGORITHM, keyId: DONATION_KEY_ID },
|
|
30
|
+
metadata: {
|
|
31
|
+
reportId: donation.reportId,
|
|
32
|
+
redactionMode: donation.redactionMode,
|
|
33
|
+
createdAt: donation.createdAt,
|
|
34
|
+
consentedAt: donation.consent.consentedAt,
|
|
35
|
+
consentVersion: 1,
|
|
36
|
+
unredactedData: donation.redactionMode === "unredacted",
|
|
37
|
+
automatedDetections: donation.redactionSummary.automatedDetections,
|
|
38
|
+
sessions: donation.redactionSummary.sessions,
|
|
39
|
+
messages: donation.redactionSummary.messages,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", contentKey, iv);
|
|
43
|
+
cipher.setAAD(Buffer.from(encryptedDonationAAD(envelope)));
|
|
44
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
45
|
+
return sanitizeEncryptedDonationEnvelope({
|
|
46
|
+
...envelope,
|
|
47
|
+
encryption: {
|
|
48
|
+
...envelope.encryption,
|
|
49
|
+
wrappedKey: base64url(crypto.publicEncrypt({ key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" }, contentKey)),
|
|
50
|
+
iv: base64url(iv),
|
|
51
|
+
authTag: base64url(cipher.getAuthTag()),
|
|
52
|
+
},
|
|
53
|
+
ciphertext: base64url(ciphertext),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function decryptResearchDonation(value, privateKey, passphrase) {
|
|
58
|
+
const envelope = sanitizeEncryptedDonationEnvelope(value);
|
|
59
|
+
if (!envelope) throw new Error("The encrypted donation envelope is invalid.");
|
|
60
|
+
const contentKey = crypto.privateDecrypt({ key: privateKey, passphrase, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" }, Buffer.from(envelope.encryption.wrappedKey, "base64url"));
|
|
61
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", contentKey, Buffer.from(envelope.encryption.iv, "base64url"));
|
|
62
|
+
decipher.setAAD(Buffer.from(encryptedDonationAAD(envelope)));
|
|
63
|
+
decipher.setAuthTag(Buffer.from(envelope.encryption.authTag, "base64url"));
|
|
64
|
+
const plaintext = Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext, "base64url")), decipher.final()]);
|
|
65
|
+
const donation = sanitizeResearchDonation(JSON.parse(plaintext.toString("utf8")));
|
|
66
|
+
if (!donation) throw new Error("The decrypted donation is invalid.");
|
|
67
|
+
return donation;
|
|
68
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const MAX_DONATION_BYTES =
|
|
1
|
+
const MAX_DONATION_BYTES = 1_800_000;
|
|
2
2
|
const MAX_SESSIONS = 250;
|
|
3
3
|
const MAX_MESSAGES = 50_000;
|
|
4
4
|
const MAX_MESSAGE_LENGTH = 20_000;
|
|
@@ -11,7 +11,9 @@ export function sanitizeResearchDonation(value) {
|
|
|
11
11
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
12
12
|
if (value.consent?.researchDonation !== true) return null;
|
|
13
13
|
if (!/^[A-Za-z0-9_-]{8,32}$/.test(value.reportId || "")) return null;
|
|
14
|
-
if (!new Set(["standard", "custom"]).has(value.redactionMode)) return null;
|
|
14
|
+
if (!new Set(["standard", "custom", "unredacted"]).has(value.redactionMode)) return null;
|
|
15
|
+
const unredacted = value.redactionMode === "unredacted";
|
|
16
|
+
if (unredacted && value.consent?.unredactedData !== true) return null;
|
|
15
17
|
if (!Array.isArray(value.sessions) || !value.sessions.length || value.sessions.length > MAX_SESSIONS) return null;
|
|
16
18
|
let messageCount = 0;
|
|
17
19
|
const sessions = value.sessions.flatMap((session, sessionIndex) => {
|
|
@@ -33,18 +35,21 @@ export function sanitizeResearchDonation(value) {
|
|
|
33
35
|
redactionMode: value.redactionMode,
|
|
34
36
|
createdAt: /^\d{4}-\d{2}-\d{2}T/.test(value.createdAt || "") ? value.createdAt : new Date().toISOString(),
|
|
35
37
|
redactionSummary: {
|
|
36
|
-
automatedDetections: Math.round(Math.max(0, Math.min(Number(value.redactionSummary?.automatedDetections) || 0, 1_000_000))),
|
|
38
|
+
automatedDetections: unredacted ? 0 : Math.round(Math.max(0, Math.min(Number(value.redactionSummary?.automatedDetections) || 0, 1_000_000))),
|
|
37
39
|
sessions: sessions.length,
|
|
38
40
|
messages: messageCount,
|
|
39
41
|
},
|
|
40
42
|
sessions,
|
|
41
43
|
consent: {
|
|
42
44
|
researchDonation: true,
|
|
43
|
-
|
|
45
|
+
...(unredacted ? { unredactedData: true } : {}),
|
|
46
|
+
statement: unredacted
|
|
47
|
+
? "I understand this donation is not automatically redacted and may contain credentials, personal details, private code, URLs, and file paths. I consent to transmit it for research."
|
|
48
|
+
: "I consent for this reviewed data to be transmitted and used for research.",
|
|
44
49
|
consentedAt: /^\d{4}-\d{2}-\d{2}T/.test(value.consent.consentedAt || "") ? value.consent.consentedAt : new Date().toISOString(),
|
|
45
50
|
},
|
|
46
51
|
};
|
|
47
|
-
return JSON.stringify(donation).
|
|
52
|
+
return new TextEncoder().encode(JSON.stringify(donation)).byteLength <= MAX_DONATION_BYTES ? donation : null;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export { MAX_DONATION_BYTES };
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { encryptResearchDonation } from "./research-donation-crypto.mjs";
|
|
2
2
|
|
|
3
3
|
export const RESEARCH_DONATION_URL = "https://agent-behavior-wrapped-judge.haoxingdu.workers.dev/v1/research-donations";
|
|
4
4
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
5
5
|
|
|
6
6
|
export async function submitResearchDonation(value, { clientId, endpoint = RESEARCH_DONATION_URL, fetchImpl = fetch } = {}) {
|
|
7
|
-
const
|
|
8
|
-
if (!donation) throw new Error("The reviewed donation does not match the research schema.");
|
|
7
|
+
const encryptedDonation = encryptResearchDonation(value);
|
|
9
8
|
let response;
|
|
10
9
|
try {
|
|
11
10
|
response = await fetchImpl(endpoint, {
|
|
12
11
|
method: "POST",
|
|
13
12
|
headers: {
|
|
14
13
|
"content-type": "application/json",
|
|
15
|
-
"x-behavior-wrapped-protocol": "
|
|
14
|
+
"x-behavior-wrapped-protocol": "2",
|
|
16
15
|
...(clientId ? { "x-behavior-wrapped-client": clientId } : {}),
|
|
17
16
|
},
|
|
18
17
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
19
|
-
body: JSON.stringify({
|
|
18
|
+
body: JSON.stringify({ encryptedDonation }),
|
|
20
19
|
});
|
|
21
20
|
} catch (error) {
|
|
22
21
|
if (error?.name === "TimeoutError" || error?.name === "AbortError") throw new Error("The research donation timed out. Your data was not confirmed as received.");
|
|
@@ -26,3 +25,16 @@ export async function submitResearchDonation(value, { clientId, endpoint = RESEA
|
|
|
26
25
|
if (!response.ok) throw new Error(body?.error || "The research donation could not be accepted.");
|
|
27
26
|
return body;
|
|
28
27
|
}
|
|
28
|
+
|
|
29
|
+
export async function deleteResearchDonation(id, deletionToken, { endpoint = RESEARCH_DONATION_URL, fetchImpl = fetch } = {}) {
|
|
30
|
+
if (!/^[0-9a-f-]{36}$/.test(id || "") || !/^[A-Za-z0-9_-]{43}$/.test(deletionToken || "")) throw new Error("The local deletion receipt is invalid.");
|
|
31
|
+
const response = await fetchImpl(`${endpoint}/${id}`, {
|
|
32
|
+
method: "DELETE",
|
|
33
|
+
headers: { "x-behavior-wrapped-protocol": "2", "x-behavior-wrapped-deletion-token": deletionToken },
|
|
34
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
35
|
+
}).catch(() => null);
|
|
36
|
+
if (!response) throw new Error("The research donation service is temporarily unavailable.");
|
|
37
|
+
const body = await response.json().catch(() => ({}));
|
|
38
|
+
if (!response.ok) throw new Error(body?.error || "The research donation could not be deleted.");
|
|
39
|
+
return body;
|
|
40
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redactAggregateText } from "./privacy.mjs";
|
|
2
|
-
import { OPENROUTER_MODEL, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
2
|
+
import { OPENROUTER_MODEL, OPENROUTER_PROVIDER_PREFERENCES, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
3
3
|
import { judgeError, judgeRequestDetails, judgeResponseDetails } from "./judge-debug.mjs";
|
|
4
4
|
|
|
5
5
|
export const SESSION_TOPIC_RELAY_URL = "https://agent-behavior-wrapped-judge.haoxingdu.workers.dev/v1/session-topics";
|
|
@@ -130,6 +130,7 @@ export function buildOpenRouterSessionTopicRequest(candidates, model = OPENROUTE
|
|
|
130
130
|
const ids = candidates.map((candidate) => candidate.candidate_id);
|
|
131
131
|
return {
|
|
132
132
|
model,
|
|
133
|
+
provider: OPENROUTER_PROVIDER_PREFERENCES,
|
|
133
134
|
temperature: 0,
|
|
134
135
|
reasoning: { effort: "none", exclude: true },
|
|
135
136
|
max_tokens: Math.min(8192, Math.max(512, candidates.length * 56)),
|
package/server/store.mjs
CHANGED
|
@@ -5,10 +5,34 @@ import crypto from "node:crypto";
|
|
|
5
5
|
|
|
6
6
|
export const storeRoot = process.env.BEHAVIOR_WRAPPED_STORE_ROOT || path.join(os.homedir(), ".agent-behavior-wrapped");
|
|
7
7
|
export const reportsRoot = path.join(storeRoot, "reports");
|
|
8
|
+
export const donationReceiptsRoot = path.join(storeRoot, "donation-receipts");
|
|
8
9
|
const clientIdFile = path.join(storeRoot, "client-id");
|
|
9
10
|
|
|
10
11
|
function ensureStore() {
|
|
11
12
|
fs.mkdirSync(reportsRoot, { recursive: true, mode: 0o700 });
|
|
13
|
+
fs.mkdirSync(donationReceiptsRoot, { recursive: true, mode: 0o700 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function saveDonationReceipt(value) {
|
|
17
|
+
if (!/^[0-9a-f-]{36}$/.test(value?.donation_id || "") || !/^[A-Za-z0-9_-]{43}$/.test(value?.deletion_token || "")) throw new Error("The donation service returned an invalid deletion receipt.");
|
|
18
|
+
ensureStore();
|
|
19
|
+
const receipt = { donationId: value.donation_id, deletionToken: value.deletion_token, savedAt: new Date().toISOString() };
|
|
20
|
+
fs.writeFileSync(path.join(donationReceiptsRoot, `${receipt.donationId}.json`), `${JSON.stringify(receipt)}\n`, { mode: 0o600, flag: "wx" });
|
|
21
|
+
return receipt;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function loadDonationReceipt(id) {
|
|
25
|
+
if (!/^[0-9a-f-]{36}$/.test(id || "")) return null;
|
|
26
|
+
try { return JSON.parse(fs.readFileSync(path.join(donationReceiptsRoot, `${id}.json`), "utf8")); }
|
|
27
|
+
catch { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function deleteDonationReceipt(id) {
|
|
31
|
+
if (!/^[0-9a-f-]{36}$/.test(id || "")) return false;
|
|
32
|
+
const file = path.join(donationReceiptsRoot, `${id}.json`);
|
|
33
|
+
if (!fs.existsSync(file)) return false;
|
|
34
|
+
fs.unlinkSync(file);
|
|
35
|
+
return true;
|
|
12
36
|
}
|
|
13
37
|
|
|
14
38
|
export function createReportId() {
|