@remembrance-ai/opencode-plugin 0.1.53 → 0.1.55
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 +21 -7
- package/opencode.json +2 -2
- package/package.json +1 -1
- package/scripts/hook-core.mjs +690 -21
- package/servers/remembrance-mcp.mjs +542 -52
- package/skills/remembrancer/references/remembrance-payloads.md +6 -4
- package/skills/remembrancer/references/remembrance-setup.md +27 -11
- package/src/index.mjs +84 -5
package/scripts/hook-core.mjs
CHANGED
|
@@ -53,6 +53,14 @@ const DIRECT_SELECTION_MARKER_LIMIT = 20;
|
|
|
53
53
|
const DIRECT_SELECTION_MARKER_TTL_MS = 24 * 60 * 60 * 1000;
|
|
54
54
|
const MAX_LOCAL_CONFIG_BYTES = 64 * 1024;
|
|
55
55
|
const MAX_LOCAL_HEALTH_MARKER_BYTES = 16 * 1024;
|
|
56
|
+
const MAX_LOCAL_PLUGIN_ALERT_BYTES = 16 * 1024;
|
|
57
|
+
const MAX_LOCAL_CLIENT_UPDATE_BYTES = 16 * 1024;
|
|
58
|
+
const PLUGIN_ALERT_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
59
|
+
const PLUGIN_ALERT_LIMIT = 32;
|
|
60
|
+
const CLIENT_UPDATE_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
61
|
+
const CLIENT_UPDATE_TIMEOUT_MS = 1200;
|
|
62
|
+
export const HOST_POLICY_ALERT_TEXT =
|
|
63
|
+
"Remembrance was blocked by host policy before reaching Remembrance. Nothing was sent. Querying remains available.";
|
|
56
64
|
const PLUGIN_HEALTH_COMPONENTS = new Set([
|
|
57
65
|
"session_start",
|
|
58
66
|
"prompt_hook",
|
|
@@ -67,6 +75,63 @@ const PLUGIN_HEALTH_SURFACES = new Set([
|
|
|
67
75
|
"vs_code",
|
|
68
76
|
"opencode",
|
|
69
77
|
]);
|
|
78
|
+
const CLIENT_RELEASE_MANIFEST_SURFACES = new Set([
|
|
79
|
+
...PLUGIN_HEALTH_SURFACES,
|
|
80
|
+
"mcp",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const CLIENT_UPDATE_GUIDANCE = {
|
|
84
|
+
codex: {
|
|
85
|
+
command: [
|
|
86
|
+
'CODEX_CLI="${CODEX_CLI:-$(command -v codex || true)}"',
|
|
87
|
+
'[ -x "$CODEX_CLI" ] || CODEX_CLI="/Applications/ChatGPT.app/Contents/Resources/codex"',
|
|
88
|
+
'[ -x "$CODEX_CLI" ] || CODEX_CLI="/Applications/Codex.app/Contents/Resources/codex"',
|
|
89
|
+
'[ -x "$CODEX_CLI" ] || { printf \'%s\\n\' "Codex CLI not found. Install the Codex CLI, or update the ChatGPT desktop app, then try again." >&2; exit 1; }',
|
|
90
|
+
'"$CODEX_CLI" plugin marketplace upgrade remembrance',
|
|
91
|
+
'"$CODEX_CLI" plugin add remembrance@remembrance --json',
|
|
92
|
+
].join("\n"),
|
|
93
|
+
update:
|
|
94
|
+
"Ask the user for permission to update Remembrance. If approved, run the bundled Codex update command exactly as shown.",
|
|
95
|
+
restart:
|
|
96
|
+
"After it succeeds, tell the user to fully quit and reopen Codex. The current process remains on the installed version until restart.",
|
|
97
|
+
},
|
|
98
|
+
claude_code: {
|
|
99
|
+
command:
|
|
100
|
+
"claude plugin marketplace update remembrance\nclaude plugin update remembrance@remembrance",
|
|
101
|
+
update:
|
|
102
|
+
"Ask the user for permission to update Remembrance. If approved, run the bundled Claude Code update command exactly as shown.",
|
|
103
|
+
restart:
|
|
104
|
+
"After it succeeds, tell the user to run /reload-plugins or fully quit and reopen Claude Code. The current plugin process remains on the installed version until reload or restart.",
|
|
105
|
+
},
|
|
106
|
+
cursor: {
|
|
107
|
+
command: null,
|
|
108
|
+
update:
|
|
109
|
+
"Tell the user to open Cursor settings, refresh the marketplace that provides Remembrance, and choose Update for the Remembrance plugin.",
|
|
110
|
+
restart:
|
|
111
|
+
"After Cursor reports completion, tell the user to fully quit and reopen Cursor. The current process remains on the installed version until restart.",
|
|
112
|
+
},
|
|
113
|
+
openclaw: {
|
|
114
|
+
command: "openclaw plugins update remembrance\nopenclaw remembrance setup",
|
|
115
|
+
update:
|
|
116
|
+
"Ask the user for permission to update Remembrance. If approved, run the bundled OpenClaw update and setup commands exactly as shown.",
|
|
117
|
+
restart:
|
|
118
|
+
"After they succeed, tell the user to restart the OpenClaw Gateway unless its managed reload already restarted it, then begin a new agent session.",
|
|
119
|
+
},
|
|
120
|
+
vs_code: {
|
|
121
|
+
command: null,
|
|
122
|
+
update:
|
|
123
|
+
"Tell the user to refresh the marketplace or managed source that provides Remembrance and update the Remembrance plugin there.",
|
|
124
|
+
restart:
|
|
125
|
+
"After completion, tell the user to reload the VS Code window or fully quit and reopen VS Code. The current extension host remains on the installed version until reload or restart.",
|
|
126
|
+
},
|
|
127
|
+
opencode: {
|
|
128
|
+
command: "npx -y @remembrance-ai/opencode-plugin@latest setup",
|
|
129
|
+
update:
|
|
130
|
+
"Ask the user for permission to update Remembrance. If approved, run the bundled opencode setup command exactly as shown.",
|
|
131
|
+
restart:
|
|
132
|
+
"After it succeeds, tell the user to fully quit and reopen opencode. The current process remains on the installed version until restart.",
|
|
133
|
+
},
|
|
134
|
+
};
|
|
70
135
|
|
|
71
136
|
const SERVICE_PATTERNS = [
|
|
72
137
|
/\b(vercel|heroku|netlify|cloudflare|aws|gcp|azure)\b/i,
|
|
@@ -217,6 +282,39 @@ export function redactPrompt(prompt) {
|
|
|
217
282
|
// options; the Claude adapter builds its own payload with its own identity.
|
|
218
283
|
export const DEFAULT_AGENT_IDENTITY = { provider: "codex", model: "codex" };
|
|
219
284
|
export const DEFAULT_USER_AGENT = "@remembrance/codex-plugin";
|
|
285
|
+
const VERSIONED_CLIENT_USER_AGENTS = new Set([
|
|
286
|
+
"@remembrance/codex-plugin",
|
|
287
|
+
"@remembrance/claude-code-plugin",
|
|
288
|
+
"@remembrance/cursor-plugin",
|
|
289
|
+
"@remembrance/openclaw-plugin",
|
|
290
|
+
"@remembrance/vscode-plugin",
|
|
291
|
+
"@remembrance-ai/opencode-plugin",
|
|
292
|
+
]);
|
|
293
|
+
let cachedInstalledClientVersion;
|
|
294
|
+
|
|
295
|
+
export function installedClientVersion() {
|
|
296
|
+
if (cachedInstalledClientVersion !== undefined) {
|
|
297
|
+
return cachedInstalledClientVersion;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const value = JSON.parse(
|
|
301
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
302
|
+
)?.version;
|
|
303
|
+
cachedInstalledClientVersion = /^\d+\.\d+\.\d+$/.test(String(value ?? ""))
|
|
304
|
+
? String(value)
|
|
305
|
+
: null;
|
|
306
|
+
} catch {
|
|
307
|
+
cachedInstalledClientVersion = null;
|
|
308
|
+
}
|
|
309
|
+
return cachedInstalledClientVersion;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function clientUserAgent(base = DEFAULT_USER_AGENT) {
|
|
313
|
+
const normalized = String(base || DEFAULT_USER_AGENT).trim();
|
|
314
|
+
if (!VERSIONED_CLIENT_USER_AGENTS.has(normalized)) return normalized;
|
|
315
|
+
const version = installedClientVersion();
|
|
316
|
+
return version ? `${normalized}/${version}` : normalized;
|
|
317
|
+
}
|
|
220
318
|
|
|
221
319
|
export function buildQueryPayload(
|
|
222
320
|
prompt,
|
|
@@ -362,10 +460,7 @@ export function inferConstraints(prompt) {
|
|
|
362
460
|
for (const [pattern, value] of [
|
|
363
461
|
[/\b(mcp|model context protocol|tool server)\b/i, "mcp"],
|
|
364
462
|
[/\b(install|setup|set up|configure)\b/i, "setup"],
|
|
365
|
-
[
|
|
366
|
-
/\b(api key|enterprise key|organization key|credential)\b/i,
|
|
367
|
-
"api-key",
|
|
368
|
-
],
|
|
463
|
+
[/\b(api key|enterprise key|organization key|credential)\b/i, "api-key"],
|
|
369
464
|
[/\b(troubleshoot|troubleshooting)\b/i, "troubleshooting"],
|
|
370
465
|
[/\b(ci|github actions?|circleci)\b/i, "ci"],
|
|
371
466
|
[/\b(deploy|deployment|vercel|heroku)\b/i, "deployment"],
|
|
@@ -401,7 +496,7 @@ export async function queryRemembrance(payload, options = {}) {
|
|
|
401
496
|
}
|
|
402
497
|
const headers = {
|
|
403
498
|
"content-type": "application/json",
|
|
404
|
-
"user-agent": options.userAgent
|
|
499
|
+
"user-agent": clientUserAgent(options.userAgent),
|
|
405
500
|
};
|
|
406
501
|
const apiKey = credential.apiKey;
|
|
407
502
|
if (apiKey) {
|
|
@@ -484,7 +579,7 @@ export async function reportDirectiveEvent(event, options = {}) {
|
|
|
484
579
|
if (isUnusableConfigurationSource(credential.source)) return false;
|
|
485
580
|
const headers = {
|
|
486
581
|
"content-type": "application/json",
|
|
487
|
-
"user-agent": options.userAgent
|
|
582
|
+
"user-agent": clientUserAgent(options.userAgent),
|
|
488
583
|
};
|
|
489
584
|
const apiKey = credential.apiKey;
|
|
490
585
|
if (apiKey) headers["x-remembrance-api-key"] = apiKey;
|
|
@@ -1100,7 +1195,7 @@ export function contributionReason(
|
|
|
1100
1195
|
"Call submit_feedback once for each listed skill with useful true or false and a concise post-use lesson.",
|
|
1101
1196
|
"If submit_feedback returns next_step.submit_remembrance_payload, submit that reviewed evidence when the lesson is reusable.",
|
|
1102
1197
|
"Report failed attempts, corrections, security findings, and reusable workflow improvements instead of silently changing the skill.",
|
|
1103
|
-
|
|
1198
|
+
`If host policy blocks the request, tell the user exactly: "${HOST_POLICY_ALERT_TEXT}" Do not retry the blocked content through another transport.`,
|
|
1104
1199
|
"Redact secrets, private URLs, credentials, prompts, outputs, source paths, and proprietary content.",
|
|
1105
1200
|
"If nothing is genuinely worth capturing, say so in one line; you will not be asked again for this use.",
|
|
1106
1201
|
]
|
|
@@ -1117,7 +1212,7 @@ export function contributionReason(
|
|
|
1117
1212
|
"• submit_remembrance — post the ready next_step.submit_remembrance_payload returned by submit_feedback when the lesson is reusable, adding redacted evidence when available.",
|
|
1118
1213
|
"• propose_private_skill — the default proposal path. Use it for anything repository-derived or organization-specific, after the host and organization approve Remembrance as the destination. It cannot create a public candidate under any credential state, so it stays private even if your key stops resolving.",
|
|
1119
1214
|
"• propose_skill_idea — only if no existing skill fit, you built a reusable workflow, AND a public candidate is an acceptable outcome. An active organization key keeps it private; intentionally omitting a key makes the call PUBLIC. A supplied invalid/inactive key fails with 401 and an insufficient key fails with 403, creating no candidate. Read `visibility` in a successful response (organization_private or public_candidate) and report where it landed. Never remove or bypass the key to force a public candidate.",
|
|
1120
|
-
|
|
1215
|
+
`Use the Remembrance MCP tools directly when they are visible. If this runtime has no MCP tools exposed, use the REST endpoints from the query response. If host policy blocks the request, tell the user exactly: "${HOST_POLICY_ALERT_TEXT}" Do not retry the blocked content through another transport. If the API itself is unavailable, emit REMEMBRANCE_SUBMISSION_PAYLOAD with the redacted JSON you would submit.`,
|
|
1121
1216
|
"Redact secrets, private URLs, and proprietary content; submit redacted summaries and hashes, not raw traces.",
|
|
1122
1217
|
"Attach evidence: reproduction detail in outcome.failure_modes, artifact hashes of redacted logs/diffs, or an attestation. Evidence-less public reports wait in unverified intake until corroborated; evidence-backed ones verify faster and rank higher.",
|
|
1123
1218
|
"If nothing is genuinely worth capturing, just say so in one line — you will not be asked again this session.",
|
|
@@ -1169,6 +1264,17 @@ export function detectHighValueLessonSignalInText(text) {
|
|
|
1169
1264
|
return null;
|
|
1170
1265
|
}
|
|
1171
1266
|
|
|
1267
|
+
export function hostPolicyAlertWasReported(input) {
|
|
1268
|
+
const value = String(
|
|
1269
|
+
input?.last_assistant_message ??
|
|
1270
|
+
input?.lastAssistantMessage ??
|
|
1271
|
+
input?.assistant_message ??
|
|
1272
|
+
input?.message ??
|
|
1273
|
+
"",
|
|
1274
|
+
);
|
|
1275
|
+
return value.includes(HOST_POLICY_ALERT_TEXT);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1172
1278
|
// Pure decision function (unit-tested): compare completed registry use and task
|
|
1173
1279
|
// eligibility with the last-prompted count. A completed use prompts for a
|
|
1174
1280
|
// contribution; eligible work with no completed query prompts for full-context
|
|
@@ -1182,6 +1288,9 @@ export function decideStop(input, options = {}) {
|
|
|
1182
1288
|
if (input?.stop_hook_active) {
|
|
1183
1289
|
return { allow: true, why: "stop_hook_active" };
|
|
1184
1290
|
}
|
|
1291
|
+
if (hostPolicyAlertWasReported(input)) {
|
|
1292
|
+
return { allow: true, why: "host_policy_alert_reported" };
|
|
1293
|
+
}
|
|
1185
1294
|
const sessionId = sessionIdFor(input);
|
|
1186
1295
|
const readUse = options.readUseCount ?? readRegistryUseCount;
|
|
1187
1296
|
const useCount = readUse(sessionId, env);
|
|
@@ -2146,7 +2255,7 @@ async function postTaskOutcome(payload, options = {}) {
|
|
|
2146
2255
|
if (isUnusableConfigurationSource(credential.source)) return false;
|
|
2147
2256
|
const headers = {
|
|
2148
2257
|
"content-type": "application/json",
|
|
2149
|
-
"user-agent": options.userAgent
|
|
2258
|
+
"user-agent": clientUserAgent(options.userAgent),
|
|
2150
2259
|
};
|
|
2151
2260
|
const apiKey = credential.apiKey;
|
|
2152
2261
|
if (apiKey) headers["x-remembrance-api-key"] = apiKey;
|
|
@@ -2304,6 +2413,549 @@ function pluginHealthDir(env = process.env) {
|
|
|
2304
2413
|
return join(homedir(), ".cache", "remembrance", "plugin-health");
|
|
2305
2414
|
}
|
|
2306
2415
|
|
|
2416
|
+
function pluginAlertDir(env = process.env) {
|
|
2417
|
+
if (env?.REMEMBRANCE_PLUGIN_ALERT_DIR) {
|
|
2418
|
+
return String(env.REMEMBRANCE_PLUGIN_ALERT_DIR);
|
|
2419
|
+
}
|
|
2420
|
+
if (process.env.VITEST) {
|
|
2421
|
+
return join(
|
|
2422
|
+
tmpdir(),
|
|
2423
|
+
"remembrance-plugin-alert-tests",
|
|
2424
|
+
String(process.pid),
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
return join(homedir(), ".cache", "remembrance", "plugin-alerts");
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
function clientUpdateDir(env = process.env) {
|
|
2431
|
+
if (env?.REMEMBRANCE_CLIENT_UPDATE_DIR) {
|
|
2432
|
+
return String(env.REMEMBRANCE_CLIENT_UPDATE_DIR);
|
|
2433
|
+
}
|
|
2434
|
+
if (process.env.VITEST) {
|
|
2435
|
+
return join(
|
|
2436
|
+
tmpdir(),
|
|
2437
|
+
"remembrance-client-update-tests",
|
|
2438
|
+
String(process.pid),
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
return join(homedir(), ".cache", "remembrance", "client-updates");
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
function clientUpdatePath(surface, env = process.env) {
|
|
2445
|
+
const normalized = normalizedPluginHealthSurface(surface);
|
|
2446
|
+
return normalized ? join(clientUpdateDir(env), `${normalized}.json`) : null;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
export async function checkForClientUpdate(
|
|
2450
|
+
{
|
|
2451
|
+
surface,
|
|
2452
|
+
currentVersion,
|
|
2453
|
+
fetchImpl = fetch,
|
|
2454
|
+
now = Date.now(),
|
|
2455
|
+
timeoutMs = CLIENT_UPDATE_TIMEOUT_MS,
|
|
2456
|
+
},
|
|
2457
|
+
env = process.env,
|
|
2458
|
+
) {
|
|
2459
|
+
const normalizedSurface = normalizedPluginHealthSurface(surface);
|
|
2460
|
+
if (
|
|
2461
|
+
!normalizedSurface ||
|
|
2462
|
+
disabled(env.REMEMBRANCE_CLIENT_UPDATE_CHECK) ||
|
|
2463
|
+
!parseStableClientVersion(currentVersion)
|
|
2464
|
+
) {
|
|
2465
|
+
return null;
|
|
2466
|
+
}
|
|
2467
|
+
const configuration = resolveApiConfiguration(env);
|
|
2468
|
+
if (isUnusableConfigurationSource(configuration.source)) return null;
|
|
2469
|
+
const path = clientUpdatePath(normalizedSurface, env);
|
|
2470
|
+
let manifest = readCachedClientRelease(path, now);
|
|
2471
|
+
if (!manifest) {
|
|
2472
|
+
const controller = new AbortController();
|
|
2473
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2474
|
+
try {
|
|
2475
|
+
const response = await fetchImpl(
|
|
2476
|
+
`${configuration.apiUrl.replace(/\/+$/, "")}/.well-known/remembrance-client-release.json`,
|
|
2477
|
+
{
|
|
2478
|
+
headers: { accept: "application/json" },
|
|
2479
|
+
signal: controller.signal,
|
|
2480
|
+
},
|
|
2481
|
+
);
|
|
2482
|
+
if (!response?.ok) return null;
|
|
2483
|
+
const text = await response.text();
|
|
2484
|
+
if (Buffer.byteLength(text, "utf8") > MAX_LOCAL_CLIENT_UPDATE_BYTES) {
|
|
2485
|
+
return null;
|
|
2486
|
+
}
|
|
2487
|
+
try {
|
|
2488
|
+
manifest = parseClientReleaseManifest(JSON.parse(text));
|
|
2489
|
+
} catch {
|
|
2490
|
+
return null;
|
|
2491
|
+
}
|
|
2492
|
+
if (!manifest) return null;
|
|
2493
|
+
writeCachedClientRelease(path, manifest, now);
|
|
2494
|
+
} catch {
|
|
2495
|
+
return null;
|
|
2496
|
+
} finally {
|
|
2497
|
+
clearTimeout(timer);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
if (
|
|
2501
|
+
!manifest.surfaces.includes(normalizedSurface) ||
|
|
2502
|
+
compareStableClientVersions(manifest.latest_version, currentVersion) <= 0
|
|
2503
|
+
) {
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
const guidance = CLIENT_UPDATE_GUIDANCE[normalizedSurface];
|
|
2507
|
+
if (!guidance) return null;
|
|
2508
|
+
const command = guidance.command
|
|
2509
|
+
? `\nTrusted update command bundled with this installed client:\n${guidance.command}`
|
|
2510
|
+
: "";
|
|
2511
|
+
return {
|
|
2512
|
+
current_version: currentVersion,
|
|
2513
|
+
latest_version: manifest.latest_version,
|
|
2514
|
+
surface: normalizedSurface,
|
|
2515
|
+
notice: [
|
|
2516
|
+
`Remembrance update available: installed ${currentVersion}, published ${manifest.latest_version}.`,
|
|
2517
|
+
guidance.update,
|
|
2518
|
+
command,
|
|
2519
|
+
guidance.restart,
|
|
2520
|
+
"Do not claim the new version is active until a fresh process reports it.",
|
|
2521
|
+
]
|
|
2522
|
+
.filter(Boolean)
|
|
2523
|
+
.join("\n"),
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
function readCachedClientRelease(path, now) {
|
|
2528
|
+
if (!path) return null;
|
|
2529
|
+
try {
|
|
2530
|
+
const parsed = JSON.parse(
|
|
2531
|
+
readSecureHookFile(path, MAX_LOCAL_CLIENT_UPDATE_BYTES),
|
|
2532
|
+
);
|
|
2533
|
+
if (
|
|
2534
|
+
parsed?.schema_version !== 1 ||
|
|
2535
|
+
!Number.isFinite(parsed?.checked_at) ||
|
|
2536
|
+
parsed.checked_at > now + 5 * 60 * 1000 ||
|
|
2537
|
+
parsed.checked_at + CLIENT_UPDATE_CACHE_TTL_MS <= now
|
|
2538
|
+
) {
|
|
2539
|
+
return null;
|
|
2540
|
+
}
|
|
2541
|
+
return parseClientReleaseManifest(parsed.manifest);
|
|
2542
|
+
} catch {
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function writeCachedClientRelease(path, manifest, now) {
|
|
2548
|
+
if (!path) return false;
|
|
2549
|
+
const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
2550
|
+
try {
|
|
2551
|
+
ensureSecureHookDirectory(dirname(path));
|
|
2552
|
+
writeFileSync(
|
|
2553
|
+
temporaryPath,
|
|
2554
|
+
`${JSON.stringify({ schema_version: 1, checked_at: now, manifest })}\n`,
|
|
2555
|
+
{ mode: 0o600, flag: "wx" },
|
|
2556
|
+
);
|
|
2557
|
+
renameSync(temporaryPath, path);
|
|
2558
|
+
return true;
|
|
2559
|
+
} catch {
|
|
2560
|
+
try {
|
|
2561
|
+
rmSync(temporaryPath, { force: true });
|
|
2562
|
+
} catch {
|
|
2563
|
+
// Update checks are advisory and never break the host lifecycle.
|
|
2564
|
+
}
|
|
2565
|
+
return false;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
function parseClientReleaseManifest(value) {
|
|
2570
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2571
|
+
if (value.schema_version !== "1") return null;
|
|
2572
|
+
if (!parseStableClientVersion(value.latest_version)) return null;
|
|
2573
|
+
if (
|
|
2574
|
+
typeof value.published_at !== "string" ||
|
|
2575
|
+
!isCanonicalClientReleaseTimestamp(value.published_at) ||
|
|
2576
|
+
!Array.isArray(value.surfaces)
|
|
2577
|
+
) {
|
|
2578
|
+
return null;
|
|
2579
|
+
}
|
|
2580
|
+
const surfaces = value.surfaces.filter(
|
|
2581
|
+
(item) =>
|
|
2582
|
+
typeof item === "string" && CLIENT_RELEASE_MANIFEST_SURFACES.has(item),
|
|
2583
|
+
);
|
|
2584
|
+
if (
|
|
2585
|
+
surfaces.length !== value.surfaces.length ||
|
|
2586
|
+
new Set(surfaces).size !== surfaces.length
|
|
2587
|
+
) {
|
|
2588
|
+
return null;
|
|
2589
|
+
}
|
|
2590
|
+
return {
|
|
2591
|
+
schema_version: "1",
|
|
2592
|
+
latest_version: value.latest_version,
|
|
2593
|
+
published_at: new Date(value.published_at).toISOString(),
|
|
2594
|
+
surfaces,
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
function isCanonicalClientReleaseTimestamp(value) {
|
|
2599
|
+
const timestamp = Date.parse(value);
|
|
2600
|
+
return (
|
|
2601
|
+
Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
function parseStableClientVersion(value) {
|
|
2606
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(
|
|
2607
|
+
String(value ?? ""),
|
|
2608
|
+
);
|
|
2609
|
+
if (!match) return null;
|
|
2610
|
+
const parts = match.slice(1).map(Number);
|
|
2611
|
+
return parts.every(
|
|
2612
|
+
(part) => Number.isSafeInteger(part) && part >= 0 && part <= 9_999,
|
|
2613
|
+
)
|
|
2614
|
+
? parts
|
|
2615
|
+
: null;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
function compareStableClientVersions(left, right) {
|
|
2619
|
+
const leftParts = parseStableClientVersion(left);
|
|
2620
|
+
const rightParts = parseStableClientVersion(right);
|
|
2621
|
+
if (!leftParts || !rightParts) return 0;
|
|
2622
|
+
for (let index = 0; index < 3; index += 1) {
|
|
2623
|
+
const difference = leftParts[index] - rightParts[index];
|
|
2624
|
+
if (difference !== 0) return Math.sign(difference);
|
|
2625
|
+
}
|
|
2626
|
+
return 0;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
function pluginAlertSessionPath(surface, sessionId, env = process.env) {
|
|
2630
|
+
const normalized = normalizedPluginHealthSurface(surface);
|
|
2631
|
+
if (!normalized) return null;
|
|
2632
|
+
const hash = createHash("sha256")
|
|
2633
|
+
.update(String(sessionId ?? "unknown"), "utf8")
|
|
2634
|
+
.digest("hex")
|
|
2635
|
+
.slice(0, 24);
|
|
2636
|
+
return join(pluginAlertDir(env), `${normalized}.${hash}.json`);
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
function hostPolicyOperationClass(toolName) {
|
|
2640
|
+
const normalized = String(toolName ?? "").toLowerCase();
|
|
2641
|
+
if (
|
|
2642
|
+
normalized.endsWith("query_skills") ||
|
|
2643
|
+
normalized.endsWith("list_skills") ||
|
|
2644
|
+
normalized.endsWith("get_skill") ||
|
|
2645
|
+
normalized.endsWith("get_resource") ||
|
|
2646
|
+
normalized.endsWith("invoke_skill") ||
|
|
2647
|
+
normalized.endsWith("run_connection_doctor") ||
|
|
2648
|
+
normalized.endsWith("get_connection_status")
|
|
2649
|
+
) {
|
|
2650
|
+
return "query";
|
|
2651
|
+
}
|
|
2652
|
+
if (
|
|
2653
|
+
normalized.endsWith("propose_private_skill") ||
|
|
2654
|
+
normalized.endsWith("queue_private_skill_import")
|
|
2655
|
+
) {
|
|
2656
|
+
return "private_contribution";
|
|
2657
|
+
}
|
|
2658
|
+
if (
|
|
2659
|
+
normalized.endsWith("propose_skill_idea") ||
|
|
2660
|
+
normalized.endsWith("submit_remembrance") ||
|
|
2661
|
+
normalized.endsWith("submit_suggestion") ||
|
|
2662
|
+
normalized.endsWith("submit_resource") ||
|
|
2663
|
+
normalized.endsWith("submit_resource_review")
|
|
2664
|
+
) {
|
|
2665
|
+
return "contribution";
|
|
2666
|
+
}
|
|
2667
|
+
if (
|
|
2668
|
+
normalized.endsWith("submit_query_feedback") ||
|
|
2669
|
+
normalized.endsWith("submit_feedback") ||
|
|
2670
|
+
normalized.endsWith("report_task_outcome")
|
|
2671
|
+
) {
|
|
2672
|
+
return "feedback";
|
|
2673
|
+
}
|
|
2674
|
+
return "other";
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
function isRemembranceToolName(toolName) {
|
|
2678
|
+
const normalized = String(toolName ?? "").toLowerCase();
|
|
2679
|
+
return (
|
|
2680
|
+
normalized.includes("remembrance") ||
|
|
2681
|
+
[
|
|
2682
|
+
"run_connection_doctor",
|
|
2683
|
+
"get_connection_status",
|
|
2684
|
+
"query_skills",
|
|
2685
|
+
"list_skills",
|
|
2686
|
+
"get_skill",
|
|
2687
|
+
"get_resource",
|
|
2688
|
+
"invoke_skill",
|
|
2689
|
+
"get_value_proof",
|
|
2690
|
+
"report_task_outcome",
|
|
2691
|
+
"submit_query_feedback",
|
|
2692
|
+
"submit_feedback",
|
|
2693
|
+
"submit_remembrance",
|
|
2694
|
+
"propose_skill_idea",
|
|
2695
|
+
"propose_private_skill",
|
|
2696
|
+
"queue_private_skill_import",
|
|
2697
|
+
"submit_suggestion",
|
|
2698
|
+
"submit_resource",
|
|
2699
|
+
"submit_resource_review",
|
|
2700
|
+
].some((candidate) => normalized.endsWith(candidate))
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
function boundedHostPolicyText(value, depth = 0) {
|
|
2705
|
+
if (depth > 3 || value == null) return "";
|
|
2706
|
+
if (typeof value === "string") return value.slice(0, 2_000);
|
|
2707
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
2708
|
+
return String(value);
|
|
2709
|
+
}
|
|
2710
|
+
if (Array.isArray(value)) {
|
|
2711
|
+
return value
|
|
2712
|
+
.slice(0, 8)
|
|
2713
|
+
.map((item) => boundedHostPolicyText(item, depth + 1))
|
|
2714
|
+
.join(" ")
|
|
2715
|
+
.slice(0, 4_000);
|
|
2716
|
+
}
|
|
2717
|
+
if (typeof value !== "object") return "";
|
|
2718
|
+
return [
|
|
2719
|
+
value.message,
|
|
2720
|
+
value.reason,
|
|
2721
|
+
value.permission_decision_reason,
|
|
2722
|
+
value.permissionDecisionReason,
|
|
2723
|
+
value.decision,
|
|
2724
|
+
value.response,
|
|
2725
|
+
value.error,
|
|
2726
|
+
value.detail,
|
|
2727
|
+
value.details,
|
|
2728
|
+
value.output,
|
|
2729
|
+
value.result,
|
|
2730
|
+
value.tool_response,
|
|
2731
|
+
value.toolResponse,
|
|
2732
|
+
value.properties?.error,
|
|
2733
|
+
value.properties?.message,
|
|
2734
|
+
value.properties?.reason,
|
|
2735
|
+
]
|
|
2736
|
+
.map((item) => boundedHostPolicyText(item, depth + 1))
|
|
2737
|
+
.filter(Boolean)
|
|
2738
|
+
.join(" ")
|
|
2739
|
+
.slice(0, 4_000);
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
export function classifyHostPolicyDenial({
|
|
2743
|
+
eventType = "",
|
|
2744
|
+
toolName = "",
|
|
2745
|
+
value = null,
|
|
2746
|
+
} = {}) {
|
|
2747
|
+
if (!isRemembranceToolName(toolName)) return null;
|
|
2748
|
+
const text = boundedHostPolicyText(value);
|
|
2749
|
+
const denied =
|
|
2750
|
+
/\b(?:block(?:ed|ing)?|den(?:y|ied|ial)|disallow(?:ed)?|prohibit(?:ed)?|not permitted|permission refused|cannot (?:send|share|export)|may not (?:send|share|export))\b/i.test(
|
|
2751
|
+
text,
|
|
2752
|
+
);
|
|
2753
|
+
const policyContext =
|
|
2754
|
+
/\b(?:host|tenant|workspace|organization|administrator|admin|privacy|security|policy|data[- ](?:export|egress|loss prevention)|dlp|external service|trusted service)\b/i.test(
|
|
2755
|
+
text,
|
|
2756
|
+
);
|
|
2757
|
+
const normalizedEvent = String(eventType ?? "").toLowerCase();
|
|
2758
|
+
const permissionEvent = normalizedEvent.includes("permission");
|
|
2759
|
+
const explicitPreTransportContext =
|
|
2760
|
+
/\b(?:host(?:ed)?|tenant|workspace)\s+(?:(?:privacy|security|execution|tool|mcp|data[- ](?:export|egress))\s+)?policy\b|\b(?:privacy|security|data[- ](?:export|egress|loss prevention)|dlp)\s+(?:guard|policy|rule|restriction)\b|\bbefore\s+(?:contacting|reaching|calling|sending\s+(?:it\s+)?to)\s+(?:remembrance|the\s+(?:mcp|external service)|an?\s+external service)\b|\b(?:external|trusted)\s+service\b/i.test(
|
|
2761
|
+
text,
|
|
2762
|
+
);
|
|
2763
|
+
// A host permission event is itself pre-transport evidence. Generic failed
|
|
2764
|
+
// tool events need an explicit host/export signal so a Remembrance API 403
|
|
2765
|
+
// such as an organization-policy refusal is never mislabeled as host policy.
|
|
2766
|
+
if (
|
|
2767
|
+
!denied ||
|
|
2768
|
+
!policyContext ||
|
|
2769
|
+
(!permissionEvent && !explicitPreTransportContext)
|
|
2770
|
+
) {
|
|
2771
|
+
return null;
|
|
2772
|
+
}
|
|
2773
|
+
return {
|
|
2774
|
+
denial_class: permissionEvent
|
|
2775
|
+
? "host_permission_policy"
|
|
2776
|
+
: "host_execution_policy",
|
|
2777
|
+
operation_class: hostPolicyOperationClass(toolName),
|
|
2778
|
+
before_mcp: permissionEvent ? "yes" : "unknown",
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
function readPluginAlertState(surface, sessionId, env = process.env) {
|
|
2783
|
+
const path = pluginAlertSessionPath(surface, sessionId, env);
|
|
2784
|
+
if (!path) return null;
|
|
2785
|
+
try {
|
|
2786
|
+
const parsed = JSON.parse(
|
|
2787
|
+
readSecureHookFile(path, MAX_LOCAL_PLUGIN_ALERT_BYTES),
|
|
2788
|
+
);
|
|
2789
|
+
if (parsed?.schema_version !== 1 || !Array.isArray(parsed?.observations)) {
|
|
2790
|
+
return null;
|
|
2791
|
+
}
|
|
2792
|
+
return parsed;
|
|
2793
|
+
} catch {
|
|
2794
|
+
return null;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
function writePluginAlertState(surface, sessionId, state, env = process.env) {
|
|
2799
|
+
const path = pluginAlertSessionPath(surface, sessionId, env);
|
|
2800
|
+
if (!path) return false;
|
|
2801
|
+
const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
2802
|
+
try {
|
|
2803
|
+
ensureSecureHookDirectory(dirname(path));
|
|
2804
|
+
writeFileSync(temporaryPath, `${JSON.stringify(state)}\n`, {
|
|
2805
|
+
mode: 0o600,
|
|
2806
|
+
flag: "wx",
|
|
2807
|
+
});
|
|
2808
|
+
renameSync(temporaryPath, path);
|
|
2809
|
+
prunePluginAlertFiles(path, env);
|
|
2810
|
+
return true;
|
|
2811
|
+
} catch {
|
|
2812
|
+
try {
|
|
2813
|
+
rmSync(temporaryPath, { force: true });
|
|
2814
|
+
} catch {
|
|
2815
|
+
// Local policy observations are advisory and must never break the host.
|
|
2816
|
+
}
|
|
2817
|
+
return false;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function prunePluginAlertFiles(keepPath, env) {
|
|
2822
|
+
const directory = pluginAlertDir(env);
|
|
2823
|
+
const cutoff = Date.now() - PLUGIN_ALERT_TTL_MS;
|
|
2824
|
+
try {
|
|
2825
|
+
const candidates = readdirSync(directory)
|
|
2826
|
+
.filter((name) => /^[a-z_]+\.[a-f0-9]{24}\.json$/.test(name))
|
|
2827
|
+
.map((name) => {
|
|
2828
|
+
const path = join(directory, name);
|
|
2829
|
+
return { path, modified: statSync(path).mtimeMs };
|
|
2830
|
+
})
|
|
2831
|
+
.sort((left, right) => right.modified - left.modified);
|
|
2832
|
+
for (const candidate of candidates) {
|
|
2833
|
+
if (
|
|
2834
|
+
candidate.path !== keepPath &&
|
|
2835
|
+
(candidate.modified < cutoff ||
|
|
2836
|
+
candidates.indexOf(candidate) >= PLUGIN_ALERT_LIMIT)
|
|
2837
|
+
) {
|
|
2838
|
+
rmSync(candidate.path, { force: true });
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
} catch {
|
|
2842
|
+
// Pruning is advisory.
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
export function recordHostPolicyDenial(
|
|
2847
|
+
{
|
|
2848
|
+
surface,
|
|
2849
|
+
sessionId,
|
|
2850
|
+
eventType = "",
|
|
2851
|
+
toolName = "",
|
|
2852
|
+
value = null,
|
|
2853
|
+
pluginVersion = null,
|
|
2854
|
+
hostVersion = null,
|
|
2855
|
+
},
|
|
2856
|
+
env = process.env,
|
|
2857
|
+
) {
|
|
2858
|
+
const normalizedSurface = normalizedPluginHealthSurface(surface);
|
|
2859
|
+
const classification = classifyHostPolicyDenial({
|
|
2860
|
+
eventType,
|
|
2861
|
+
toolName,
|
|
2862
|
+
value,
|
|
2863
|
+
});
|
|
2864
|
+
if (!normalizedSurface || !classification) return null;
|
|
2865
|
+
const now = new Date().toISOString();
|
|
2866
|
+
const sessionHash = createHash("sha256")
|
|
2867
|
+
.update(String(sessionId ?? "unknown"), "utf8")
|
|
2868
|
+
.digest("hex")
|
|
2869
|
+
.slice(0, 24);
|
|
2870
|
+
const id = createHash("sha256")
|
|
2871
|
+
.update(
|
|
2872
|
+
[
|
|
2873
|
+
normalizedSurface,
|
|
2874
|
+
sessionHash,
|
|
2875
|
+
classification.operation_class,
|
|
2876
|
+
classification.denial_class,
|
|
2877
|
+
].join(":"),
|
|
2878
|
+
"utf8",
|
|
2879
|
+
)
|
|
2880
|
+
.digest("hex")
|
|
2881
|
+
.slice(0, 24);
|
|
2882
|
+
const existing = readPluginAlertState(normalizedSurface, sessionId, env);
|
|
2883
|
+
const current = existing?.observations?.find((item) => item?.id === id);
|
|
2884
|
+
const observation = {
|
|
2885
|
+
id,
|
|
2886
|
+
denial_class: classification.denial_class,
|
|
2887
|
+
operation_class: classification.operation_class,
|
|
2888
|
+
before_mcp: classification.before_mcp,
|
|
2889
|
+
plugin_version: safeText(pluginVersion ?? "", 64),
|
|
2890
|
+
host_version: safeText(hostVersion ?? "", 64),
|
|
2891
|
+
first_seen_at: current?.first_seen_at ?? now,
|
|
2892
|
+
last_seen_at: now,
|
|
2893
|
+
count: Math.min(999, Math.max(0, Number(current?.count) || 0) + 1),
|
|
2894
|
+
alerted_at: current?.alerted_at ?? null,
|
|
2895
|
+
};
|
|
2896
|
+
const observations = [
|
|
2897
|
+
observation,
|
|
2898
|
+
...(existing?.observations ?? []).filter((item) => item?.id !== id),
|
|
2899
|
+
]
|
|
2900
|
+
.filter((item) => {
|
|
2901
|
+
const seenAt = Date.parse(item?.last_seen_at ?? "");
|
|
2902
|
+
return (
|
|
2903
|
+
Number.isFinite(seenAt) && Date.now() - seenAt <= PLUGIN_ALERT_TTL_MS
|
|
2904
|
+
);
|
|
2905
|
+
})
|
|
2906
|
+
.slice(0, 12);
|
|
2907
|
+
const state = {
|
|
2908
|
+
schema_version: 1,
|
|
2909
|
+
surface: normalizedSurface,
|
|
2910
|
+
session_hash: sessionHash,
|
|
2911
|
+
observations,
|
|
2912
|
+
updated_at: now,
|
|
2913
|
+
};
|
|
2914
|
+
return writePluginAlertState(normalizedSurface, sessionId, state, env)
|
|
2915
|
+
? observation
|
|
2916
|
+
: null;
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
export function readPendingHostPolicyAlert(
|
|
2920
|
+
surface,
|
|
2921
|
+
sessionId,
|
|
2922
|
+
env = process.env,
|
|
2923
|
+
) {
|
|
2924
|
+
const state = readPluginAlertState(surface, sessionId, env);
|
|
2925
|
+
return (
|
|
2926
|
+
state?.observations?.find(
|
|
2927
|
+
(item) =>
|
|
2928
|
+
!item?.alerted_at &&
|
|
2929
|
+
Date.now() - Date.parse(item?.last_seen_at ?? "") <=
|
|
2930
|
+
PLUGIN_ALERT_TTL_MS,
|
|
2931
|
+
) ?? null
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
export function markHostPolicyAlertDelivered(
|
|
2936
|
+
surface,
|
|
2937
|
+
sessionId,
|
|
2938
|
+
observationId,
|
|
2939
|
+
env = process.env,
|
|
2940
|
+
) {
|
|
2941
|
+
const state = readPluginAlertState(surface, sessionId, env);
|
|
2942
|
+
if (!state) return false;
|
|
2943
|
+
const now = new Date().toISOString();
|
|
2944
|
+
let changed = false;
|
|
2945
|
+
const observations = state.observations.map((item) => {
|
|
2946
|
+
if (item?.id !== observationId || item?.alerted_at) return item;
|
|
2947
|
+
changed = true;
|
|
2948
|
+
return { ...item, alerted_at: now };
|
|
2949
|
+
});
|
|
2950
|
+
if (!changed) return false;
|
|
2951
|
+
return writePluginAlertState(
|
|
2952
|
+
surface,
|
|
2953
|
+
sessionId,
|
|
2954
|
+
{ ...state, observations, updated_at: now },
|
|
2955
|
+
env,
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2307
2959
|
function normalizedPluginHealthSurface(value) {
|
|
2308
2960
|
const surface = String(value ?? "")
|
|
2309
2961
|
.trim()
|
|
@@ -2439,8 +3091,24 @@ export function recordPluginLifecycleHealth(
|
|
|
2439
3091
|
.update(apiConfiguration.apiUrl.replace(/\/+$/, ""), "utf8")
|
|
2440
3092
|
.digest("hex")
|
|
2441
3093
|
.slice(0, 16);
|
|
3094
|
+
const configuredEvidenceOrigin = String(
|
|
3095
|
+
env?.REMEMBRANCE_PLUGIN_HEALTH_EVIDENCE_ORIGIN ?? "host_runtime",
|
|
3096
|
+
)
|
|
3097
|
+
.trim()
|
|
3098
|
+
.toLowerCase();
|
|
3099
|
+
const evidenceOrigin = [
|
|
3100
|
+
"host_runtime",
|
|
3101
|
+
"host_process",
|
|
3102
|
+
"direct_adapter",
|
|
3103
|
+
].includes(configuredEvidenceOrigin)
|
|
3104
|
+
? configuredEvidenceOrigin
|
|
3105
|
+
: "host_runtime";
|
|
3106
|
+
const releaseRunId =
|
|
3107
|
+
evidenceOrigin === "host_process"
|
|
3108
|
+
? safeText(env?.REMEMBRANCE_RELEASE_RUN_ID ?? "", 96)
|
|
3109
|
+
: "";
|
|
2442
3110
|
const payload = {
|
|
2443
|
-
schema_version:
|
|
3111
|
+
schema_version: 2,
|
|
2444
3112
|
surface: normalizedSurface,
|
|
2445
3113
|
session_hash:
|
|
2446
3114
|
sessionId && sessionId !== "unknown"
|
|
@@ -2465,6 +3133,8 @@ export function recordPluginLifecycleHealth(
|
|
|
2465
3133
|
: "none",
|
|
2466
3134
|
api_destination_source: apiConfiguration.source,
|
|
2467
3135
|
api_destination_fingerprint: apiDestinationFingerprint,
|
|
3136
|
+
evidence_origin: evidenceOrigin,
|
|
3137
|
+
release_run_id: releaseRunId || null,
|
|
2468
3138
|
components: {
|
|
2469
3139
|
...currentComponents,
|
|
2470
3140
|
[normalizedComponent]: now,
|
|
@@ -2551,9 +3221,7 @@ export function resolveApiCredential(env = process.env) {
|
|
|
2551
3221
|
}
|
|
2552
3222
|
const environmentKey = String(env.REMEMBRANCE_API_KEY ?? "").trim();
|
|
2553
3223
|
if (environmentKey) {
|
|
2554
|
-
const explicitBinding = String(
|
|
2555
|
-
env.REMEMBRANCE_API_KEY_ORIGIN ?? "",
|
|
2556
|
-
).trim();
|
|
3224
|
+
const explicitBinding = String(env.REMEMBRANCE_API_KEY_ORIGIN ?? "").trim();
|
|
2557
3225
|
const binding = explicitBinding
|
|
2558
3226
|
? normalizeApiUrl(explicitBinding, env)
|
|
2559
3227
|
: { apiUrl: DEFAULT_API_URL, issue: null };
|
|
@@ -2573,9 +3241,7 @@ export function resolveApiCredential(env = process.env) {
|
|
|
2573
3241
|
return { apiKey: "", source: "none" };
|
|
2574
3242
|
}
|
|
2575
3243
|
try {
|
|
2576
|
-
const parsed = JSON.parse(
|
|
2577
|
-
readSecureHookFile(path, MAX_LOCAL_CONFIG_BYTES),
|
|
2578
|
-
);
|
|
3244
|
+
const parsed = JSON.parse(readSecureHookFile(path, MAX_LOCAL_CONFIG_BYTES));
|
|
2579
3245
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2580
3246
|
return unusableSharedConfigCredential();
|
|
2581
3247
|
}
|
|
@@ -2671,9 +3337,7 @@ export function resolveApiConfiguration(env = process.env) {
|
|
|
2671
3337
|
return { apiUrl: DEFAULT_API_URL, source: "default" };
|
|
2672
3338
|
}
|
|
2673
3339
|
try {
|
|
2674
|
-
const parsed = JSON.parse(
|
|
2675
|
-
readSecureHookFile(path, MAX_LOCAL_CONFIG_BYTES),
|
|
2676
|
-
);
|
|
3340
|
+
const parsed = JSON.parse(readSecureHookFile(path, MAX_LOCAL_CONFIG_BYTES));
|
|
2677
3341
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2678
3342
|
return {
|
|
2679
3343
|
apiUrl: DEFAULT_API_URL,
|
|
@@ -2738,7 +3402,10 @@ function normalizeApiUrl(value, env = process.env) {
|
|
|
2738
3402
|
}
|
|
2739
3403
|
|
|
2740
3404
|
function credentialForApiDestination(credential, configuration) {
|
|
2741
|
-
if (
|
|
3405
|
+
if (
|
|
3406
|
+
!credential.apiKey ||
|
|
3407
|
+
isUnusableConfigurationSource(configuration.source)
|
|
3408
|
+
) {
|
|
2742
3409
|
return credential.apiKey
|
|
2743
3410
|
? unusableDestinationCredential()
|
|
2744
3411
|
: { apiKey: "", source: credential.source };
|
|
@@ -2811,7 +3478,9 @@ function isPrivateDestinationHostname(hostname) {
|
|
|
2811
3478
|
}
|
|
2812
3479
|
|
|
2813
3480
|
function normalizeHostname(hostname) {
|
|
2814
|
-
return String(hostname)
|
|
3481
|
+
return String(hostname)
|
|
3482
|
+
.replace(/^\[|\]$/g, "")
|
|
3483
|
+
.toLowerCase();
|
|
2815
3484
|
}
|
|
2816
3485
|
|
|
2817
3486
|
function readSecureHookFile(path, maxBytes) {
|