@remembrance-ai/opencode-plugin 0.1.47 → 0.1.49
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 +9 -3
- package/opencode.json +2 -2
- package/package.json +1 -1
- package/scripts/hook-core.mjs +318 -38
- package/servers/remembrance-mcp.mjs +1200 -170
- package/skills/remembrancer/SKILL.md +34 -19
- package/skills/remembrancer/references/remembrance-payloads.md +8 -7
- package/skills/remembrancer/references/remembrance-setup.md +66 -25
- package/src/index.mjs +2 -2
|
@@ -4345,6 +4345,8 @@ var clientHealthIssueCodeSchema = external_exports.enum([
|
|
|
4345
4345
|
"mcp_registration_failed",
|
|
4346
4346
|
"mcp_authentication_failed",
|
|
4347
4347
|
"credential_source_mismatch",
|
|
4348
|
+
"api_destination_mismatch",
|
|
4349
|
+
"api_destination_not_observed",
|
|
4348
4350
|
"plugin_version_mismatch",
|
|
4349
4351
|
"lifecycle_marker_stale",
|
|
4350
4352
|
"invalid_lifecycle_marker",
|
|
@@ -4483,10 +4485,319 @@ function isUnsafeDestinationHostname(hostname) {
|
|
|
4483
4485
|
return false;
|
|
4484
4486
|
}
|
|
4485
4487
|
|
|
4488
|
+
// ../core/src/connection-doctor.ts
|
|
4489
|
+
function buildConnectionDoctorReport(input) {
|
|
4490
|
+
const status = record(input.connection_status);
|
|
4491
|
+
const transport = record(status.transport);
|
|
4492
|
+
const registry = record(status.registry);
|
|
4493
|
+
const authentication = record(registry.authentication);
|
|
4494
|
+
const capabilities = record(authentication.capabilities);
|
|
4495
|
+
const sharedConfig = record(transport.shared_config);
|
|
4496
|
+
const pluginHealth = record(status.plugin_health);
|
|
4497
|
+
const identity = record(status.local_signing_identity);
|
|
4498
|
+
const destination = destinationFromTransport(transport);
|
|
4499
|
+
const credentialSource = stringValue(transport.credential_source) ?? "none";
|
|
4500
|
+
const scope = scopeValue(authentication.scope);
|
|
4501
|
+
const registryReady = status.status !== "error" && registry.status === "ready";
|
|
4502
|
+
const queryAuthorized = capabilities.query === true;
|
|
4503
|
+
const submitAuthorized = capabilities.submit === true;
|
|
4504
|
+
const configuredCredential = ["environment", "shared_config"].includes(
|
|
4505
|
+
credentialSource
|
|
4506
|
+
) || [
|
|
4507
|
+
"authorization_bearer",
|
|
4508
|
+
"x_remembrance_api_key",
|
|
4509
|
+
"request_header"
|
|
4510
|
+
].includes(credentialSource);
|
|
4511
|
+
const checks = [];
|
|
4512
|
+
checks.push(
|
|
4513
|
+
input.host_registration_observed ? pass("mcp_transport", "The active MCP transport executed the doctor.") : warning(
|
|
4514
|
+
"mcp_transport",
|
|
4515
|
+
"Registry diagnostics ran outside the host, so MCP registration is not observable.",
|
|
4516
|
+
"verify_host_registration",
|
|
4517
|
+
"Open the host, confirm that run_connection_doctor is available, and run it there. If it is absent, update or reinstall the host-specific Remembrance plugin and fully restart the host."
|
|
4518
|
+
)
|
|
4519
|
+
);
|
|
4520
|
+
const configurationInvalid = ["unusable_environment", "unusable_shared_config"].includes(
|
|
4521
|
+
stringValue(transport.api_url_source) ?? ""
|
|
4522
|
+
) || ["unusable_shared_config", "unusable_destination_binding"].includes(
|
|
4523
|
+
credentialSource
|
|
4524
|
+
);
|
|
4525
|
+
checks.push(
|
|
4526
|
+
configurationInvalid ? fail(
|
|
4527
|
+
"configuration",
|
|
4528
|
+
"The configured credential or registry destination is invalid.",
|
|
4529
|
+
"repair_configuration",
|
|
4530
|
+
"Repair or intentionally remove the invalid Remembrance setting, then rerun the doctor. Remembrance will not silently change scope or destination."
|
|
4531
|
+
) : pass(
|
|
4532
|
+
"configuration",
|
|
4533
|
+
configuredCredential ? "A credential source is configured." : "No credential is configured; public anonymous access is intentional."
|
|
4534
|
+
)
|
|
4535
|
+
);
|
|
4536
|
+
const containsKey = sharedConfig.api_key_present === true;
|
|
4537
|
+
const sharedConfigPermissionCommand = sharedConfig.location === "default_shared_config" ? "chmod 600 ~/.config/remembrance/config.json" : void 0;
|
|
4538
|
+
checks.push(
|
|
4539
|
+
containsKey && sharedConfig.secure_permissions === false ? fail(
|
|
4540
|
+
"credential_security",
|
|
4541
|
+
"The shared credential file is readable by other local users.",
|
|
4542
|
+
"restrict_shared_config_permissions",
|
|
4543
|
+
"Restrict the shared Remembrance config to the current user, then rerun the doctor.",
|
|
4544
|
+
sharedConfigPermissionCommand
|
|
4545
|
+
) : containsKey && sharedConfig.secure_permissions === null ? warning(
|
|
4546
|
+
"credential_security",
|
|
4547
|
+
"The shared credential file permissions could not be verified.",
|
|
4548
|
+
"verify_shared_config_permissions",
|
|
4549
|
+
"Verify that only the current user can read the shared Remembrance config, then rerun the doctor."
|
|
4550
|
+
) : sharedConfig.present === true && sharedConfig.secure_permissions === false ? warning(
|
|
4551
|
+
"credential_security",
|
|
4552
|
+
"The shared config is broadly readable, but it contains no API key.",
|
|
4553
|
+
"restrict_shared_config_permissions",
|
|
4554
|
+
"Restrict the shared Remembrance config to the current user.",
|
|
4555
|
+
void 0,
|
|
4556
|
+
false,
|
|
4557
|
+
sharedConfigPermissionCommand
|
|
4558
|
+
) : pass(
|
|
4559
|
+
"credential_security",
|
|
4560
|
+
containsKey ? "The shared credential file is restricted to the current user." : "No locally stored API key requires a permission check."
|
|
4561
|
+
)
|
|
4562
|
+
);
|
|
4563
|
+
checks.push(
|
|
4564
|
+
registryReady ? pass("registry_connectivity", "The Remembrance registry responded.") : fail(
|
|
4565
|
+
"registry_connectivity",
|
|
4566
|
+
"The Remembrance registry did not return a healthy status response.",
|
|
4567
|
+
"retry_registry_connection",
|
|
4568
|
+
"Check network access and the configured registry destination, then rerun the doctor."
|
|
4569
|
+
)
|
|
4570
|
+
);
|
|
4571
|
+
checks.push(
|
|
4572
|
+
configuredCredential && scope !== "organization" ? fail(
|
|
4573
|
+
"authentication",
|
|
4574
|
+
"A credential is configured, but the registry did not authenticate an organization scope.",
|
|
4575
|
+
"repair_organization_credential",
|
|
4576
|
+
"Create or select an active organization key, run the one-click setup again, and fully restart the host."
|
|
4577
|
+
) : pass(
|
|
4578
|
+
"authentication",
|
|
4579
|
+
scope === "organization" ? "The active request is authenticated to an organization." : "The active request is using public anonymous scope."
|
|
4580
|
+
)
|
|
4581
|
+
);
|
|
4582
|
+
checks.push(
|
|
4583
|
+
queryAuthorized ? pass("query_authorization", "The active scope can query skills.") : fail(
|
|
4584
|
+
"query_authorization",
|
|
4585
|
+
"The active scope cannot query skills.",
|
|
4586
|
+
"grant_query_scope",
|
|
4587
|
+
"Use an active key with agent:query permission, or remove the invalid credential to use public anonymous querying."
|
|
4588
|
+
)
|
|
4589
|
+
);
|
|
4590
|
+
checks.push(readProbeCheck(input.active_read_probe));
|
|
4591
|
+
checks.push(
|
|
4592
|
+
!input.check_organization_write_authorization || scope !== "organization" ? notApplicable(
|
|
4593
|
+
"organization_write_authorization",
|
|
4594
|
+
scope === "organization" ? "Organization write authorization was not requested." : "Organization-private submissions require an organization credential."
|
|
4595
|
+
) : submitAuthorized ? pass(
|
|
4596
|
+
"organization_write_authorization",
|
|
4597
|
+
"The active organization key can create reviewed submissions."
|
|
4598
|
+
) : warning(
|
|
4599
|
+
"organization_write_authorization",
|
|
4600
|
+
"Querying works, but the active organization key cannot create submissions.",
|
|
4601
|
+
"grant_submission_scope",
|
|
4602
|
+
"Use an organization key with submission:create permission when contributions are required."
|
|
4603
|
+
)
|
|
4604
|
+
);
|
|
4605
|
+
const pluginIssues = issueCodes(pluginHealth.issues);
|
|
4606
|
+
const destinationMismatch = pluginIssues.includes(
|
|
4607
|
+
"api_destination_mismatch"
|
|
4608
|
+
);
|
|
4609
|
+
checks.push(pluginLifecycleCheck(pluginHealth, pluginIssues));
|
|
4610
|
+
checks.push(
|
|
4611
|
+
pluginHealth.expected !== true ? notApplicable(
|
|
4612
|
+
"destination_consistency",
|
|
4613
|
+
"No native plugin lifecycle is declared for this MCP-only connection."
|
|
4614
|
+
) : destinationMismatch ? warning(
|
|
4615
|
+
"destination_consistency",
|
|
4616
|
+
"The native hooks and bundled MCP resolve different Remembrance registries.",
|
|
4617
|
+
"align_registry_destination",
|
|
4618
|
+
"Configure the native hooks and bundled MCP to use the same Remembrance registry, then fully restart the host.",
|
|
4619
|
+
pluginIssues
|
|
4620
|
+
) : pluginIssues.includes("api_destination_not_observed") ? warning(
|
|
4621
|
+
"destination_consistency",
|
|
4622
|
+
"The native hook destination has not been observed in the current lifecycle marker.",
|
|
4623
|
+
"refresh_destination_observation",
|
|
4624
|
+
"Fully restart the host, submit one prompt, and rerun the doctor.",
|
|
4625
|
+
pluginIssues
|
|
4626
|
+
) : pass(
|
|
4627
|
+
"destination_consistency",
|
|
4628
|
+
"Native hooks and bundled MCP resolve the same registry destination."
|
|
4629
|
+
)
|
|
4630
|
+
);
|
|
4631
|
+
checks.push(signingIdentityCheck(identity, input.transport));
|
|
4632
|
+
const blockingIds = /* @__PURE__ */ new Set([
|
|
4633
|
+
"configuration",
|
|
4634
|
+
"credential_security",
|
|
4635
|
+
"registry_connectivity",
|
|
4636
|
+
"authentication",
|
|
4637
|
+
"query_authorization",
|
|
4638
|
+
"active_read_probe"
|
|
4639
|
+
]);
|
|
4640
|
+
const blocked = checks.some(
|
|
4641
|
+
(check) => check.status === "fail" && blockingIds.has(check.id)
|
|
4642
|
+
);
|
|
4643
|
+
const attention = checks.some(
|
|
4644
|
+
(check) => check.status === "warning" || check.status === "fail"
|
|
4645
|
+
);
|
|
4646
|
+
const readSucceeded = !input.active_read_probe.requested || input.active_read_probe.succeeded === true;
|
|
4647
|
+
return {
|
|
4648
|
+
schema_version: "1",
|
|
4649
|
+
status: blocked ? "blocked" : attention ? "attention" : "healthy",
|
|
4650
|
+
transport: input.transport,
|
|
4651
|
+
scope,
|
|
4652
|
+
destination,
|
|
4653
|
+
safe_to_query: !blocked && registryReady && queryAuthorized && readSucceeded,
|
|
4654
|
+
organization_write_authorized: scope === "organization" && submitAuthorized,
|
|
4655
|
+
signed_contributions_ready: input.transport === "hosted_http_mcp" ? null : identity.status === "ready",
|
|
4656
|
+
active_read_probe: input.active_read_probe,
|
|
4657
|
+
checks
|
|
4658
|
+
};
|
|
4659
|
+
}
|
|
4660
|
+
function readProbeCheck(probe) {
|
|
4661
|
+
if (!probe.requested) {
|
|
4662
|
+
return notApplicable(
|
|
4663
|
+
"active_read_probe",
|
|
4664
|
+
"The active catalog read probe was disabled."
|
|
4665
|
+
);
|
|
4666
|
+
}
|
|
4667
|
+
return probe.succeeded ? pass(
|
|
4668
|
+
"active_read_probe",
|
|
4669
|
+
`An authorized catalog read succeeded (${probe.item_count ?? 0} item${probe.item_count === 1 ? "" : "s"} returned).`
|
|
4670
|
+
) : fail(
|
|
4671
|
+
"active_read_probe",
|
|
4672
|
+
"The non-mutating authorized catalog read failed.",
|
|
4673
|
+
"repair_catalog_access",
|
|
4674
|
+
"Confirm agent:query access and registry availability, then rerun the doctor."
|
|
4675
|
+
);
|
|
4676
|
+
}
|
|
4677
|
+
function pluginLifecycleCheck(pluginHealth, issues) {
|
|
4678
|
+
if (pluginHealth.expected !== true) {
|
|
4679
|
+
return notApplicable(
|
|
4680
|
+
"plugin_lifecycle",
|
|
4681
|
+
"This is an MCP-only registration; native hooks are not expected."
|
|
4682
|
+
);
|
|
4683
|
+
}
|
|
4684
|
+
if (pluginHealth.status === "active") {
|
|
4685
|
+
return pass(
|
|
4686
|
+
"plugin_lifecycle",
|
|
4687
|
+
"The native plugin startup and prompt lifecycle is active."
|
|
4688
|
+
);
|
|
4689
|
+
}
|
|
4690
|
+
if (pluginHealth.status === "partial") {
|
|
4691
|
+
return warning(
|
|
4692
|
+
"plugin_lifecycle",
|
|
4693
|
+
"The core native lifecycle is active; tool or completion events have not occurred yet.",
|
|
4694
|
+
"exercise_plugin_lifecycle",
|
|
4695
|
+
"Use one Remembrance tool, complete one turn, and rerun the doctor.",
|
|
4696
|
+
issues
|
|
4697
|
+
);
|
|
4698
|
+
}
|
|
4699
|
+
return warning(
|
|
4700
|
+
"plugin_lifecycle",
|
|
4701
|
+
"The native plugin lifecycle is only partially active.",
|
|
4702
|
+
"repair_plugin_lifecycle",
|
|
4703
|
+
"Update or reinstall the host-specific Remembrance plugin, fully restart the host, submit one prompt, and rerun the doctor.",
|
|
4704
|
+
issues
|
|
4705
|
+
);
|
|
4706
|
+
}
|
|
4707
|
+
function signingIdentityCheck(identity, transport) {
|
|
4708
|
+
if (transport === "hosted_http_mcp") {
|
|
4709
|
+
return notApplicable(
|
|
4710
|
+
"signing_identity",
|
|
4711
|
+
"Hosted MCP cannot inspect a signing key on the caller's machine."
|
|
4712
|
+
);
|
|
4713
|
+
}
|
|
4714
|
+
if (identity.status === "ready") {
|
|
4715
|
+
return pass(
|
|
4716
|
+
"signing_identity",
|
|
4717
|
+
"The local signing identity is valid and restricted to the current user."
|
|
4718
|
+
);
|
|
4719
|
+
}
|
|
4720
|
+
if (identity.status === "missing") {
|
|
4721
|
+
return warning(
|
|
4722
|
+
"signing_identity",
|
|
4723
|
+
"No local signing identity exists yet; signed contributions will initialize it on first use.",
|
|
4724
|
+
"initialize_signing_identity",
|
|
4725
|
+
"Call bootstrap_agent_identity for preflight, or allow the first signed contribution to initialize it automatically.",
|
|
4726
|
+
void 0,
|
|
4727
|
+
true
|
|
4728
|
+
);
|
|
4729
|
+
}
|
|
4730
|
+
return warning(
|
|
4731
|
+
"signing_identity",
|
|
4732
|
+
"The local signing identity needs repair before signed contributions can be sent.",
|
|
4733
|
+
"repair_signing_identity",
|
|
4734
|
+
"Call bootstrap_agent_identity and follow its bounded repair guidance. Invalid identity files are never overwritten silently."
|
|
4735
|
+
);
|
|
4736
|
+
}
|
|
4737
|
+
function destinationFromTransport(transport) {
|
|
4738
|
+
const destination = record(transport.api_destination);
|
|
4739
|
+
const kind = ["remembrance_cloud", "custom"].includes(
|
|
4740
|
+
stringValue(destination.kind) ?? ""
|
|
4741
|
+
) ? destination.kind : "unknown";
|
|
4742
|
+
return {
|
|
4743
|
+
kind,
|
|
4744
|
+
source: stringValue(destination.source) ?? stringValue(transport.api_url_source) ?? "unknown"
|
|
4745
|
+
};
|
|
4746
|
+
}
|
|
4747
|
+
function scopeValue(value) {
|
|
4748
|
+
return value === "anonymous" || value === "organization" ? value : "unknown";
|
|
4749
|
+
}
|
|
4750
|
+
function issueCodes(value) {
|
|
4751
|
+
if (!Array.isArray(value)) return [];
|
|
4752
|
+
return [...new Set(
|
|
4753
|
+
value.map((issue) => stringValue(record(issue).code)).filter((code) => Boolean(code))
|
|
4754
|
+
)].sort();
|
|
4755
|
+
}
|
|
4756
|
+
function pass(id, summary) {
|
|
4757
|
+
return { id, status: "pass", summary, remediation: null };
|
|
4758
|
+
}
|
|
4759
|
+
function notApplicable(id, summary) {
|
|
4760
|
+
return { id, status: "not_applicable", summary, remediation: null };
|
|
4761
|
+
}
|
|
4762
|
+
function warning(id, summary, code, action, issueCodesValue, automatic = false, command) {
|
|
4763
|
+
return {
|
|
4764
|
+
id,
|
|
4765
|
+
status: "warning",
|
|
4766
|
+
summary,
|
|
4767
|
+
...issueCodesValue && issueCodesValue.length > 0 ? { issue_codes: issueCodesValue } : {},
|
|
4768
|
+
remediation: {
|
|
4769
|
+
code,
|
|
4770
|
+
action,
|
|
4771
|
+
automatic,
|
|
4772
|
+
...command ? { command } : {}
|
|
4773
|
+
}
|
|
4774
|
+
};
|
|
4775
|
+
}
|
|
4776
|
+
function fail(id, summary, code, action, command) {
|
|
4777
|
+
return {
|
|
4778
|
+
id,
|
|
4779
|
+
status: "fail",
|
|
4780
|
+
summary,
|
|
4781
|
+
remediation: {
|
|
4782
|
+
code,
|
|
4783
|
+
action,
|
|
4784
|
+
automatic: false,
|
|
4785
|
+
...command ? { command } : {}
|
|
4786
|
+
}
|
|
4787
|
+
};
|
|
4788
|
+
}
|
|
4789
|
+
function record(value) {
|
|
4790
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4791
|
+
}
|
|
4792
|
+
function stringValue(value) {
|
|
4793
|
+
return typeof value === "string" && value ? value : null;
|
|
4794
|
+
}
|
|
4795
|
+
|
|
4486
4796
|
// ../core/src/agent-guidance.ts
|
|
4487
4797
|
var REMEMBRANCE_QUERY_TOOL_DESCRIPTION = "Call before non-trivial service, API, tool, library, workflow, UI, review, test, security, or deployment work to find relevant Remembrance skills and resources. For short context-dependent follow-ups, infer the concrete task from the full conversation, preserve any plugin-supplied client_context.directive_id/runtime/trigger_reason, and query anyway. Compare each candidate's bounded why_matched evidence and applicability conditions before opening it; discard stated unlikely or corner-case mismatches. High matches should be fetched before custom work; possible and exploratory matches remain optional. Do not use for broad web search or one-off facts.";
|
|
4488
4798
|
var REMEMBRANCE_CONNECTION_STATUS_TOOL_DESCRIPTION = "Inspect the active Remembrance transport, credential source, and authenticated registry scope without exposing credentials. Call this before diagnosing authentication, checking environment variables, or making anonymous REST/browser probes. Local or bundled MCP can read REMEMBRANCE_API_KEY or ~/.config/remembrance/config.json; hosted MCP cannot read local files and authenticates from its request header. When a declared native lifecycle is degraded, local MCP may submit bounded, content-free health evidence unless REMEMBRANCE_HEALTH_REPORTING=0. Never infer the plugin's scope from REMEMBRANCE_API_KEY alone.";
|
|
4489
|
-
var
|
|
4799
|
+
var REMEMBRANCE_CONNECTION_DOCTOR_TOOL_DESCRIPTION = "Run a bounded, non-mutating Remembrance connection diagnostic. It verifies the active transport, a redacted registry destination, authenticated scope, query authorization, a real catalog read, and organization submission capability. Local MCP also checks shared-config permissions, destination consistency, native plugin lifecycle, and signing identity; hosted MCP marks local-only checks unavailable. It never submits content, creates query demand, opens a review item, or returns credentials, absolute paths, private registry URLs, prompts, or repository data.";
|
|
4800
|
+
var REMEMBRANCE_MCP_SERVER_INSTRUCTIONS = "Remembrance is shared operational memory for agents. Before claiming that Remembrance is anonymous, unconfigured, partially active, or missing an organization key, call run_connection_doctor for the transport you will actually use. It performs a safe catalog read and returns exact bounded remediation without submitting content. Use get_connection_status only when you need the underlying transport and lifecycle fields. An unset REMEMBRANCE_API_KEY does not prove that a local plugin is anonymous because local hooks and bundled MCP may read ~/.config/remembrance/config.json. Conversely, hosted MCP cannot read that local file. Anonymous curl or browser probes describe only those requests, not plugin authentication. Never print or request the raw key. When the user explicitly names an authorized Remembrance skill or supplies a remembrance://skills/{slug} URI, resolve ambiguous names with list_skills using its normalized slug-prefix filter, then call invoke_skill with an exact returned slug. Never guess a slug. list_skills is catalog resolution, not relevance search; use query_skills when the user wants relevant candidates rather than a known selection. invoke_skill rechecks authorization and organization policy, resolves the current reviewed version, and returns the only full instruction body. Catalog listings and MCP resource reads are lightweight handles, not skill use. Do not run query_skills merely to rediscover that explicit selection, and do not submit query-fit feedback for it. After meaningful direct use, follow the returned task-outcome and post-use feedback instructions once. BEFORE non-trivial service/API/tool/library/workflow/UI/review/test/security/deployment work, call query_skills to reuse proven skills and resources instead of rediscovering them. A local repository change still qualifies when it involves a reusable workflow or lesson; skip only genuinely trivial throwaway edits and one-off facts. For short follow-ups such as 'fix these issues', 'continue', or 'try again', infer the concrete task, domain, and constraints from the full conversation and still call query_skills; do not wait for the current prompt to repeat trigger keywords. Treat match_tier as a decision aid, then inspect why_matched and applicability before opening a result. These fields show bounded matched terms/capabilities, satisfied and missed constraints, qualitative lexical/semantic evidence, declared scope, use conditions, and exclusions without exposing unstable raw ranking scores. Discard an unlikely or irrelevant corner-case result and report fit: poor; do not force its use. For a remaining high match, call get_skill or get_resource before custom work and pass the returned query_id/result_id so the surfaced-to-fetch funnel closes; possible and exploratory matches are optional. The response also includes approximate context tokens, verified uses, risk, tags, permissions, dependencies, and contraindications so you can weigh the detour safely. Honor query_skills.skill_access on every response. When policy is org_only, use only returned organization skills and never substitute bundled or live public skill references; if an organization key is configured and the query is unavailable, fail closed because the policy cannot be confirmed. AFTER query_skills, call submit_query_feedback once with one complete set of explicit good/partial/poor judgments using the returned query_id and result_id values; leave unrated results neutral, and remember that a poor query match is not the same as a globally bad skill. Reuse the same organization or anonymous auth scope as the query. AFTER you actually use a skill or resource, close the post-use loop with submit_feedback (useful true/false plus a one-line lesson and the originating query_id/result_id; it returns a ready submit_remembrance payload), then submit_remembrance if the lesson is reusable. If nothing fit and you built a new workflow, use propose_private_skill for an organization-private candidate. With an organization key, propose_skill_idea also remains organization-private. Never remove, suppress, or bypass that key to force a public submission; submit privately, then use the reviewed public-propagation flow when the organization chooses to share it. When delegating, pass the selected slug, exact version, query_id, and result_id to the subagent; it should invoke/fetch that result or run its own full-context query. The parent reports the terminal outcome unless the subagent creates its own invocation. Before finishing any reusable task, self-check for a missed query. If you catch your own mistake, the user catches one, CI/deploy fails, a security issue appears, or you fix a release/versioning miss, submit a failure_report remembrance even if no skill was used; raw MCP clients have no plugin Stop hook to remind you later. A host privacy or tenant-policy denial happens before Remembrance receives the request. Do not retry private repository content through another transport or claim submission. If the blocked contribution is an organization skill, use the local-only queue_private_skill_import tool or bundled handoff script, then wait for an organization admin import receipt. Attach evidence (reproduction detail, artifact hashes, or an attestation); evidence-less public reports wait in unverified intake until corroborated. Redact secrets, private URLs, credentials, raw logs, and proprietary content; submit summaries and hashes, not raw traces.";
|
|
4490
4801
|
|
|
4491
4802
|
// ../core/src/redaction.ts
|
|
4492
4803
|
var SECRET_PATTERN_SPECS = [
|
|
@@ -4604,6 +4915,7 @@ function buildOrganizationSkillHandoffBundle(input, createdAt = /* @__PURE__ */
|
|
|
4604
4915
|
// ../core/src/remembrance-mcp-policy.ts
|
|
4605
4916
|
var REMEMBRANCE_MCP_READ_TOOLS = [
|
|
4606
4917
|
"get_connection_status",
|
|
4918
|
+
"run_connection_doctor",
|
|
4607
4919
|
"query_skills",
|
|
4608
4920
|
"list_skills",
|
|
4609
4921
|
"invoke_skill",
|
|
@@ -5009,6 +5321,14 @@ var skillCatalogRequestSchema = external_exports.object({
|
|
|
5009
5321
|
}
|
|
5010
5322
|
});
|
|
5011
5323
|
var connectionStatusRequestSchema = external_exports.object({}).strict();
|
|
5324
|
+
var connectionDoctorRequestSchema = external_exports.object({
|
|
5325
|
+
active_read_probe: external_exports.boolean().default(true).describe(
|
|
5326
|
+
"Verify a real authorized catalog read without creating query, feedback, or review records."
|
|
5327
|
+
),
|
|
5328
|
+
check_organization_write_authorization: external_exports.boolean().default(true).describe(
|
|
5329
|
+
"Report organization submission authorization from the authenticated capability response without sending a write request."
|
|
5330
|
+
)
|
|
5331
|
+
}).strict();
|
|
5012
5332
|
var skillCatalogEntrySchema = external_exports.object({
|
|
5013
5333
|
slug: boundedString(MAX_SHORT_TEXT_LENGTH),
|
|
5014
5334
|
name: boundedString(MAX_SHORT_TEXT_LENGTH),
|
|
@@ -5610,7 +5930,7 @@ var seedSkills = [
|
|
|
5610
5930
|
summary: "Operational setup and troubleshooting workflow for Remembrance across Claude Code, Codex, OpenClaw, Cursor, Gemini, MCP, REST, skill-only installs, enterprise keys, and local agent identity.",
|
|
5611
5931
|
status: "active",
|
|
5612
5932
|
visibility: "public",
|
|
5613
|
-
version: "0.1.
|
|
5933
|
+
version: "0.1.9",
|
|
5614
5934
|
domains: ["agent-skills", "mcp", "resource-discovery"],
|
|
5615
5935
|
tags: [
|
|
5616
5936
|
"remembrance",
|
|
@@ -5661,7 +5981,7 @@ var seedSkills = [
|
|
|
5661
5981
|
"gemini",
|
|
5662
5982
|
"rest"
|
|
5663
5983
|
],
|
|
5664
|
-
version: "0.1.
|
|
5984
|
+
version: "0.1.9",
|
|
5665
5985
|
status: "active",
|
|
5666
5986
|
visibility: "public",
|
|
5667
5987
|
providers: ["codex", "claude", "cursor", "openclaw", "generic"],
|
|
@@ -5743,7 +6063,7 @@ REST/HTTPS, skill-only installs, enterprise org keys, local identity, and common
|
|
|
5743
6063
|
Cursor, Gemini, or another agent.
|
|
5744
6064
|
- The user has an enterprise/org API key and needs to make an agent use
|
|
5745
6065
|
org-scoped skills or private overlays.
|
|
5746
|
-
- MCP tools such as get_connection_status, query_skills, list_skills,
|
|
6066
|
+
- MCP tools such as run_connection_doctor, get_connection_status, query_skills, list_skills,
|
|
5747
6067
|
invoke_skill, submit_query_feedback, submit_feedback, submit_remembrance,
|
|
5748
6068
|
propose_private_skill, queue_private_skill_import, report_task_outcome,
|
|
5749
6069
|
get_value_proof, get_skill, get_resource, or bootstrap_agent_identity are
|
|
@@ -5861,7 +6181,7 @@ openclaw remembrance setup
|
|
|
5861
6181
|
If ClawHub search shows multiple Remembrance matches, use the official package
|
|
5862
6182
|
that points to "dreamarkinc/remembrance-skills", mentions the Remembrance agent
|
|
5863
6183
|
skill/resource service, and exposes the expected Remembrance MCP tools such as
|
|
5864
|
-
get_connection_status, query_skills, list_skills, invoke_skill,
|
|
6184
|
+
run_connection_doctor, get_connection_status, query_skills, list_skills, invoke_skill,
|
|
5865
6185
|
submit_query_feedback, submit_remembrance, get_skill, and get_resource, plus
|
|
5866
6186
|
report_task_outcome and get_value_proof. Do not install
|
|
5867
6187
|
unrelated roots, genealogy, ancestry, or memorial packages.
|
|
@@ -5922,9 +6242,11 @@ chmod 600 ~/.config/remembrance/config.json
|
|
|
5922
6242
|
~~~
|
|
5923
6243
|
|
|
5924
6244
|
Do not infer connection scope by checking one environment variable. After
|
|
5925
|
-
setup,
|
|
5926
|
-
|
|
5927
|
-
|
|
6245
|
+
setup, run MCP \`run_connection_doctor\`. It performs a non-mutating catalog
|
|
6246
|
+
read and names the active transport, credential source, verified
|
|
6247
|
+
organization/public scope, and config permission status with one exact
|
|
6248
|
+
remediation, without exposing the key, absolute paths, or custom registry URLs.
|
|
6249
|
+
Use \`get_connection_status\` only for the underlying fields. An anonymous curl or browser probe describes
|
|
5928
6250
|
only that request, not the plugin. Raw REST clients do not load this file
|
|
5929
6251
|
automatically; they must deliberately read it or send a key header.
|
|
5930
6252
|
|
|
@@ -5935,14 +6257,30 @@ export REMEMBRANCE_API_KEY="YOUR_ORG_KEY"
|
|
|
5935
6257
|
export REMEMBRANCE_API_URL="https://remembrance.dev"
|
|
5936
6258
|
~~~
|
|
5937
6259
|
|
|
6260
|
+
For a custom registry, bind the key to that exact destination. Store \`apiKey\`
|
|
6261
|
+
and \`apiUrl\` together in the shared config, or bind environment credentials
|
|
6262
|
+
explicitly:
|
|
6263
|
+
|
|
6264
|
+
~~~bash
|
|
6265
|
+
export REMEMBRANCE_API_KEY="YOUR_ORG_KEY"
|
|
6266
|
+
export REMEMBRANCE_API_URL="https://registry.example"
|
|
6267
|
+
export REMEMBRANCE_API_KEY_ORIGIN="https://registry.example"
|
|
6268
|
+
~~~
|
|
6269
|
+
|
|
6270
|
+
Every remote registry requires HTTPS; only loopback development may use HTTP.
|
|
6271
|
+
An intentionally trusted private or link-local HTTPS registry also requires
|
|
6272
|
+
\`REMEMBRANCE_ALLOW_PRIVATE_REGISTRY=true\`. If the destination and credential
|
|
6273
|
+
binding do not match exactly, Remembrance pauses remote calls instead of
|
|
6274
|
+
forwarding the key.
|
|
6275
|
+
|
|
5938
6276
|
For Codex Desktop, the packaged plugin now runs a bundled local MCP server.
|
|
5939
6277
|
The native hooks and MCP process both read the shared file above, so the GUI
|
|
5940
6278
|
does not need \`launchctl setenv\` for the normal plugin path. Fully quit and
|
|
5941
|
-
reopen Codex after installing or updating the plugin, then
|
|
5942
|
-
\`
|
|
6279
|
+
reopen Codex after installing or updating the plugin, then run
|
|
6280
|
+
\`run_connection_doctor\`. A healthy install reports \`local_stdio_mcp\`, the
|
|
5943
6281
|
expected organization scope, and an active \`plugin_health\` lifecycle.
|
|
5944
6282
|
|
|
5945
|
-
If filesystem skills are visible but \`
|
|
6283
|
+
If filesystem skills are visible but \`run_connection_doctor\` is absent, or if
|
|
5946
6284
|
that tool reports missing native hooks, treat the install as partially active.
|
|
5947
6285
|
Update or reinstall from the Remembrance marketplace, fully restart Codex, and
|
|
5948
6286
|
run the check again. The plugin records only local component timestamps and
|
|
@@ -5951,6 +6289,12 @@ codes for deduplicated global-admin triage. It never submits prompts, keys,
|
|
|
5951
6289
|
paths, or raw logs. Set \`REMEMBRANCE_HEALTH_REPORTING=0\` to disable that
|
|
5952
6290
|
best-effort report.
|
|
5953
6291
|
|
|
6292
|
+
If no Remembrance MCP tool is visible, run
|
|
6293
|
+
\`npx @remembrance-ai/mcp-server doctor\`. This safely verifies registry,
|
|
6294
|
+
credential, and catalog-read access outside the host, but it deliberately
|
|
6295
|
+
reports host registration as unobservable. Update or reinstall the native
|
|
6296
|
+
plugin, fully restart the host, then rerun \`run_connection_doctor\` inside it.
|
|
6297
|
+
|
|
5954
6298
|
A manually configured hosted MCP URL is still supported, but hosted MCP cannot
|
|
5955
6299
|
read the shared file and must receive its own request credential. Only that
|
|
5956
6300
|
manual override needs a process environment or HTTP header credential. A Codex
|
|
@@ -6078,9 +6422,8 @@ overrides \`launchctl\` and the config file.
|
|
|
6078
6422
|
For the Claude Code desktop app, prefer the shared mode-0600 config file above.
|
|
6079
6423
|
The plugin hooks and bundled local MCP server both read it, so the GUI process
|
|
6080
6424
|
does not need to inherit shell exports or duplicate the key in Claude settings.
|
|
6081
|
-
Fully quit and relaunch Claude Code after changing the file, then
|
|
6082
|
-
\`
|
|
6083
|
-
active plugin health, and the expected organization scope.
|
|
6425
|
+
Fully quit and relaunch Claude Code after changing the file, then run
|
|
6426
|
+
\`run_connection_doctor\` and require \`safe_to_query: true\`.
|
|
6084
6427
|
|
|
6085
6428
|
For Cursor, prefer the shared config file above. The Cursor plugin-managed MCP
|
|
6086
6429
|
server and local hooks read it. If using a non-prod Remembrance endpoint, include
|
|
@@ -6169,8 +6512,8 @@ The exclusive managed MCP file lives at
|
|
|
6169
6512
|
\`/etc/claude-code/managed-mcp.json\` on Linux/WSL, and
|
|
6170
6513
|
\`C:\\Program Files\\ClaudeCode\\managed-mcp.json\` on Windows. The sandbox
|
|
6171
6514
|
domain applies to command/REST fallbacks; managed HTTP MCP authorization remains
|
|
6172
|
-
the exact server URL plus named tools. Verify with \`claude mcp list\`, then
|
|
6173
|
-
\`
|
|
6515
|
+
the exact server URL plus named tools. Verify with \`claude mcp list\`, then run
|
|
6516
|
+
\`run_connection_doctor\` and require organization scope before contribution
|
|
6174
6517
|
work. See the official
|
|
6175
6518
|
[managed MCP](https://code.claude.com/docs/en/managed-mcp),
|
|
6176
6519
|
[permissions](https://code.claude.com/docs/en/permissions), and
|
|
@@ -6207,7 +6550,7 @@ System settings live at \`/Library/Application Support/GeminiCli/settings.json\`
|
|
|
6207
6550
|
on macOS, \`/etc/gemini-cli/settings.json\` on Linux, and
|
|
6208
6551
|
\`C:\\ProgramData\\gemini-cli\\settings.json\` on Windows. Also allow
|
|
6209
6552
|
\`remembrance.dev\` in the organization's egress policy. Restart Gemini CLI,
|
|
6210
|
-
inspect the registered server, then
|
|
6553
|
+
inspect the registered server, then run \`run_connection_doctor\` and require
|
|
6211
6554
|
organization scope before contribution work. See the official
|
|
6212
6555
|
[enterprise configuration](https://google-gemini.github.io/gemini-cli/docs/cli/enterprise.html)
|
|
6213
6556
|
and [MCP settings](https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html).
|
|
@@ -6269,8 +6612,8 @@ the server's \`toolFilter\`. Merge this separately into \`policy.jsonc\`:
|
|
|
6269
6612
|
~~~
|
|
6270
6613
|
|
|
6271
6614
|
Keep the organization key in the plugin's mode-0600 shared config or another
|
|
6272
|
-
approved secret source, not in \`policy.jsonc\`. After the probes,
|
|
6273
|
-
\`
|
|
6615
|
+
approved secret source, not in \`policy.jsonc\`. After the probes, run
|
|
6616
|
+
\`run_connection_doctor\` and require organization scope. See the official
|
|
6274
6617
|
[plugin policy](https://docs.openclaw.ai/tools/plugin),
|
|
6275
6618
|
[conversation hook policy](https://docs.openclaw.ai/plugins/hooks),
|
|
6276
6619
|
[MCP tool filters](https://docs.openclaw.ai/cli/mcp), and
|
|
@@ -6288,7 +6631,7 @@ based fallback paths.
|
|
|
6288
6631
|
Cursor's current public enterprise documentation does not define a managed
|
|
6289
6632
|
per-MCP-tool allowlist equivalent to Codex, Claude Code, Gemini CLI, or
|
|
6290
6633
|
OpenClaw. Keep Cursor's normal tool approvals enabled and do not claim an
|
|
6291
|
-
undocumented control exists.
|
|
6634
|
+
undocumented control exists. Run \`run_connection_doctor\` and require
|
|
6292
6635
|
organization scope on every enabled surface, then verify query, invocation,
|
|
6293
6636
|
feedback, and private-contribution receipts; local plugin hooks and cloud-agent
|
|
6294
6637
|
hooks can differ. See the official
|
|
@@ -6305,8 +6648,8 @@ tool list above when the client supports tool filtering. Keep normal approval
|
|
|
6305
6648
|
behavior for non-read-only calls unless unattended organization-private
|
|
6306
6649
|
contribution is explicitly approved. A client with no server/tool policy must
|
|
6307
6650
|
use its existing destination control or the zero-network handoff. Supply the
|
|
6308
|
-
organization key through the client's secret/header mechanism, then
|
|
6309
|
-
\`
|
|
6651
|
+
organization key through the client's secret/header mechanism, then run
|
|
6652
|
+
\`run_connection_doctor\` and require organization scope before private writes.
|
|
6310
6653
|
|
|
6311
6654
|
If any host still denies the export, that denial remains authoritative. The
|
|
6312
6655
|
portable local handoff and dashboard import work identically for Codex, Claude
|
|
@@ -6337,6 +6680,16 @@ Local stdio MCP server:
|
|
|
6337
6680
|
npx @remembrance-ai/mcp-server
|
|
6338
6681
|
~~~
|
|
6339
6682
|
|
|
6683
|
+
Independent setup check when the host does not expose Remembrance tools:
|
|
6684
|
+
|
|
6685
|
+
~~~bash
|
|
6686
|
+
npx @remembrance-ai/mcp-server doctor
|
|
6687
|
+
~~~
|
|
6688
|
+
|
|
6689
|
+
This verifies registry, credential, and catalog-read access without submitting
|
|
6690
|
+
content. It cannot prove host MCP registration; after repair, rerun
|
|
6691
|
+
\`run_connection_doctor\` inside the host.
|
|
6692
|
+
|
|
6340
6693
|
Cursor MCP fallback config (use this only when plugin install is unavailable):
|
|
6341
6694
|
|
|
6342
6695
|
~~~json
|
|
@@ -6400,16 +6753,17 @@ be copied to ".agents/skills/remembrancer/SKILL.md" for compatible providers.
|
|
|
6400
6753
|
|
|
6401
6754
|
1. Start a fresh agent session.
|
|
6402
6755
|
2. Check whether Remembrance MCP tools are visible. Expected tools include
|
|
6403
|
-
get_connection_status, query_skills, list_skills, invoke_skill, get_skill,
|
|
6756
|
+
run_connection_doctor, get_connection_status, query_skills, list_skills, invoke_skill, get_skill,
|
|
6404
6757
|
get_resource, submit_query_feedback, submit_feedback, submit_remembrance,
|
|
6405
6758
|
propose_private_skill, report_task_outcome, get_value_proof, and
|
|
6406
6759
|
bootstrap_agent_identity. Local MCP also exposes
|
|
6407
6760
|
queue_private_skill_import. Clients
|
|
6408
6761
|
with MCP resource discovery should also expose paginated
|
|
6409
6762
|
\`remembrance://skills/{slug}\` handles.
|
|
6410
|
-
3. Call
|
|
6411
|
-
|
|
6412
|
-
variables or making a raw probe.
|
|
6763
|
+
3. Call run_connection_doctor and require \`safe_to_query: true\`. Confirm the
|
|
6764
|
+
active transport and expected public/organization scope before inspecting
|
|
6765
|
+
environment variables or making a raw probe. Follow its exact remediation
|
|
6766
|
+
for any warning or failure.
|
|
6413
6767
|
4. Ask the agent to query Remembrance for a known task, for example:
|
|
6414
6768
|
"Query Remembrance for web UI QA before reviewing a responsive dashboard."
|
|
6415
6769
|
5. Follow with a context-only prompt such as "fix these issues". Confirm the
|
|
@@ -6453,8 +6807,10 @@ be copied to ".agents/skills/remembrancer/SKILL.md" for compatible providers.
|
|
|
6453
6807
|
queue_private_skill_import instead, upload its JSON in the dashboard, and
|
|
6454
6808
|
verify the agent reports a local queue receipt separately from the server
|
|
6455
6809
|
import receipt.
|
|
6456
|
-
14. If using local MCP,
|
|
6457
|
-
|
|
6810
|
+
14. If using local MCP, verify local_signing_identity in get_connection_status.
|
|
6811
|
+
A missing opaque identity initializes automatically on the first signed
|
|
6812
|
+
contribution; bootstrap_agent_identity with no arguments is an optional
|
|
6813
|
+
preflight or recovery action.
|
|
6458
6814
|
|
|
6459
6815
|
## Troubleshooting matrix
|
|
6460
6816
|
|
|
@@ -7255,12 +7611,11 @@ var verifierOutputSchema = external_exports.object({
|
|
|
7255
7611
|
});
|
|
7256
7612
|
|
|
7257
7613
|
// src/server.ts
|
|
7258
|
-
import { existsSync as
|
|
7259
|
-
import { mkdir as mkdir2,
|
|
7260
|
-
import {
|
|
7261
|
-
import { dirname, join as join3 } from "node:path";
|
|
7614
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
7615
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
7616
|
+
import { dirname as dirname2 } from "node:path";
|
|
7262
7617
|
import {
|
|
7263
|
-
createPrivateKey,
|
|
7618
|
+
createPrivateKey as createPrivateKey2,
|
|
7264
7619
|
generateKeyPairSync,
|
|
7265
7620
|
sign as signPayload
|
|
7266
7621
|
} from "node:crypto";
|
|
@@ -8559,7 +8914,9 @@ var getValueProofSchema = external_exports.object({
|
|
|
8559
8914
|
id: external_exports.string().min(1).describe("Value proof id returned with potential_savings.")
|
|
8560
8915
|
});
|
|
8561
8916
|
var bootstrapAgentIdentitySchema = external_exports.object({
|
|
8562
|
-
subject: external_exports.string().min(1).describe(
|
|
8917
|
+
subject: external_exports.string().min(1).optional().describe(
|
|
8918
|
+
"Optional stable agent identity. Omit it to generate a private opaque subject from the new public-key fingerprint."
|
|
8919
|
+
),
|
|
8563
8920
|
provider: external_exports.enum(["other", "codex", "cursor", "claude_code"]).default("other").describe(
|
|
8564
8921
|
"Use other for independent TOFU keys. Use codex/cursor/claude_code only for Remembrance-registered plugin keys."
|
|
8565
8922
|
),
|
|
@@ -8569,7 +8926,7 @@ var bootstrapAgentIdentitySchema = external_exports.object({
|
|
|
8569
8926
|
});
|
|
8570
8927
|
var feedbackToolSchema = agentFeedbackRequestBaseSchema.extend({
|
|
8571
8928
|
verified_attestation: external_exports.boolean().default(false).describe(
|
|
8572
|
-
"When true, sign the feedback with the local TOFU key.
|
|
8929
|
+
"When true, sign the feedback with the local TOFU key. Missing local identity is initialized automatically; bootstrap_agent_identity remains available for preflight or recovery."
|
|
8573
8930
|
)
|
|
8574
8931
|
}).superRefine((value, ctx) => {
|
|
8575
8932
|
if (Boolean(value.query_id) !== Boolean(value.result_id)) {
|
|
@@ -8582,7 +8939,7 @@ var feedbackToolSchema = agentFeedbackRequestBaseSchema.extend({
|
|
|
8582
8939
|
});
|
|
8583
8940
|
var remembranceToolSchema = remembrancePayloadSchema.extend({
|
|
8584
8941
|
verified_attestation: external_exports.boolean().default(false).describe(
|
|
8585
|
-
"When true, sign the remembrance with the local TOFU key.
|
|
8942
|
+
"When true, sign the remembrance with the local TOFU key. Missing local identity is initialized automatically; skill.slug is also required."
|
|
8586
8943
|
)
|
|
8587
8944
|
});
|
|
8588
8945
|
var toolDefinitions = [
|
|
@@ -8593,6 +8950,13 @@ var toolDefinitions = [
|
|
|
8593
8950
|
connectionStatusRequestSchema,
|
|
8594
8951
|
"GET"
|
|
8595
8952
|
),
|
|
8953
|
+
tool(
|
|
8954
|
+
"run_connection_doctor",
|
|
8955
|
+
REMEMBRANCE_CONNECTION_DOCTOR_TOOL_DESCRIPTION,
|
|
8956
|
+
"/api/v1/agent/connection-status",
|
|
8957
|
+
connectionDoctorRequestSchema,
|
|
8958
|
+
"GET"
|
|
8959
|
+
),
|
|
8596
8960
|
tool(
|
|
8597
8961
|
"query_skills",
|
|
8598
8962
|
REMEMBRANCE_QUERY_TOOL_DESCRIPTION,
|
|
@@ -8628,7 +8992,7 @@ var toolDefinitions = [
|
|
|
8628
8992
|
),
|
|
8629
8993
|
localTool(
|
|
8630
8994
|
"bootstrap_agent_identity",
|
|
8631
|
-
"Create or reuse a local Ed25519 TOFU key and register it with Remembrance.
|
|
8995
|
+
"Create or reuse a local Ed25519 TOFU key and register it with Remembrance. No arguments are required. Rerun to repair permissions or re-bootstrap if the local agent-key.json was lost.",
|
|
8632
8996
|
"bootstrap_agent_identity",
|
|
8633
8997
|
bootstrapAgentIdentitySchema
|
|
8634
8998
|
),
|
|
@@ -8734,7 +9098,7 @@ function localTool(name, description, local, schema) {
|
|
|
8734
9098
|
};
|
|
8735
9099
|
}
|
|
8736
9100
|
function annotationsForTool(name) {
|
|
8737
|
-
const readOnly = name === "list_skills" || name === "get_value_proof";
|
|
9101
|
+
const readOnly = name === "list_skills" || name === "get_value_proof" || name === "run_connection_doctor";
|
|
8738
9102
|
const openWorld = name === "submit_feedback" || name === "submit_remembrance" || name === "propose_skill_idea" || name === "submit_suggestion" || name === "submit_resource" || name === "submit_resource_review";
|
|
8739
9103
|
return {
|
|
8740
9104
|
readOnlyHint: readOnly,
|
|
@@ -8812,9 +9176,296 @@ function isAlreadyExistsError(error) {
|
|
|
8812
9176
|
}
|
|
8813
9177
|
|
|
8814
9178
|
// src/connection-status.ts
|
|
8815
|
-
import { existsSync
|
|
8816
|
-
import {
|
|
9179
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
9180
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
9181
|
+
import { isIP } from "node:net";
|
|
9182
|
+
import { homedir as homedir3 } from "node:os";
|
|
9183
|
+
import { join as join3 } from "node:path";
|
|
9184
|
+
|
|
9185
|
+
// src/local-agent-identity.ts
|
|
9186
|
+
import { createPrivateKey, createPublicKey as createPublicKey2 } from "node:crypto";
|
|
9187
|
+
import { existsSync } from "node:fs";
|
|
9188
|
+
import { homedir as homedir2 } from "node:os";
|
|
8817
9189
|
import { join as join2 } from "node:path";
|
|
9190
|
+
|
|
9191
|
+
// src/secure-local-file.ts
|
|
9192
|
+
import {
|
|
9193
|
+
closeSync,
|
|
9194
|
+
constants,
|
|
9195
|
+
fchmodSync,
|
|
9196
|
+
fstatSync,
|
|
9197
|
+
lstatSync,
|
|
9198
|
+
openSync,
|
|
9199
|
+
opendirSync,
|
|
9200
|
+
readSync
|
|
9201
|
+
} from "node:fs";
|
|
9202
|
+
import { dirname } from "node:path";
|
|
9203
|
+
var MAX_LOCAL_CONFIG_BYTES = 64 * 1024;
|
|
9204
|
+
var MAX_LOCAL_IDENTITY_BYTES = 64 * 1024;
|
|
9205
|
+
var MAX_LOCAL_LIFECYCLE_MARKER_BYTES = 16 * 1024;
|
|
9206
|
+
var MAX_LOCAL_LIFECYCLE_MARKERS = 64;
|
|
9207
|
+
var MAX_LOCAL_DIRECTORY_ENTRIES = 256;
|
|
9208
|
+
function readSecureLocalText(path, options) {
|
|
9209
|
+
assertSecureParentDirectory(dirname(path));
|
|
9210
|
+
const before = lstatSync(path);
|
|
9211
|
+
assertRegularFile(before, path, options.maxBytes);
|
|
9212
|
+
const descriptor = openSync(
|
|
9213
|
+
path,
|
|
9214
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
|
|
9215
|
+
);
|
|
9216
|
+
try {
|
|
9217
|
+
let opened = fstatSync(descriptor);
|
|
9218
|
+
assertRegularFile(opened, path, options.maxBytes);
|
|
9219
|
+
if (before.dev !== opened.dev || before.ino !== opened.ino) {
|
|
9220
|
+
throw new Error("Local state file changed while it was being opened.");
|
|
9221
|
+
}
|
|
9222
|
+
assertCurrentUserOwner(opened.uid, path);
|
|
9223
|
+
if (isPosix() && (opened.mode & 63) !== 0) {
|
|
9224
|
+
if (options.allowInsecurePermissions) {
|
|
9225
|
+
} else if (!options.repairPermissions) {
|
|
9226
|
+
throw new Error("Local state file permissions are not private.");
|
|
9227
|
+
} else {
|
|
9228
|
+
fchmodSync(descriptor, 384);
|
|
9229
|
+
opened = fstatSync(descriptor);
|
|
9230
|
+
if ((opened.mode & 63) !== 0) {
|
|
9231
|
+
throw new Error("Local state file permissions could not be repaired.");
|
|
9232
|
+
}
|
|
9233
|
+
}
|
|
9234
|
+
}
|
|
9235
|
+
return readBoundedDescriptor(descriptor, options.maxBytes);
|
|
9236
|
+
} finally {
|
|
9237
|
+
closeSync(descriptor);
|
|
9238
|
+
}
|
|
9239
|
+
}
|
|
9240
|
+
function secureLocalFileMode(path) {
|
|
9241
|
+
return lstatSync(path).mode;
|
|
9242
|
+
}
|
|
9243
|
+
function listSecureLocalDirectory(path) {
|
|
9244
|
+
assertSecureDirectory(path);
|
|
9245
|
+
const directory = opendirSync(path);
|
|
9246
|
+
const entries = [];
|
|
9247
|
+
try {
|
|
9248
|
+
while (entries.length < MAX_LOCAL_DIRECTORY_ENTRIES) {
|
|
9249
|
+
const entry = directory.readSync();
|
|
9250
|
+
if (!entry) break;
|
|
9251
|
+
entries.push(entry.name);
|
|
9252
|
+
}
|
|
9253
|
+
} finally {
|
|
9254
|
+
directory.closeSync();
|
|
9255
|
+
}
|
|
9256
|
+
return entries;
|
|
9257
|
+
}
|
|
9258
|
+
function assertSecureParentDirectory(path) {
|
|
9259
|
+
const metadata = lstatSync(path);
|
|
9260
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
9261
|
+
throw new Error("Local state parent is not a regular directory.");
|
|
9262
|
+
}
|
|
9263
|
+
assertCurrentUserOwner(metadata.uid, path);
|
|
9264
|
+
if (isPosix() && (metadata.mode & 18) !== 0) {
|
|
9265
|
+
throw new Error("Local state parent is writable by another user.");
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
function assertSecureDirectory(path) {
|
|
9269
|
+
assertSecureParentDirectory(path);
|
|
9270
|
+
}
|
|
9271
|
+
function assertRegularFile(metadata, path, maxBytes) {
|
|
9272
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
9273
|
+
throw new Error(`Local state path is not a regular file: ${path}`);
|
|
9274
|
+
}
|
|
9275
|
+
if (metadata.size > maxBytes) {
|
|
9276
|
+
throw new Error("Local state file exceeds its size limit.");
|
|
9277
|
+
}
|
|
9278
|
+
}
|
|
9279
|
+
function assertCurrentUserOwner(uid, path) {
|
|
9280
|
+
if (isPosix() && uid !== process.getuid()) {
|
|
9281
|
+
throw new Error(`Local state path is not owned by the current user: ${path}`);
|
|
9282
|
+
}
|
|
9283
|
+
}
|
|
9284
|
+
function readBoundedDescriptor(descriptor, maxBytes) {
|
|
9285
|
+
const chunks = [];
|
|
9286
|
+
let total = 0;
|
|
9287
|
+
while (total <= maxBytes) {
|
|
9288
|
+
const chunk = Buffer.allocUnsafe(Math.min(8 * 1024, maxBytes + 1 - total));
|
|
9289
|
+
const bytesRead = readSync(descriptor, chunk, 0, chunk.length, null);
|
|
9290
|
+
if (bytesRead === 0) break;
|
|
9291
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
9292
|
+
total += bytesRead;
|
|
9293
|
+
}
|
|
9294
|
+
if (total > maxBytes) {
|
|
9295
|
+
throw new Error("Local state file exceeds its size limit.");
|
|
9296
|
+
}
|
|
9297
|
+
return Buffer.concat(chunks, total).toString("utf8");
|
|
9298
|
+
}
|
|
9299
|
+
function isPosix() {
|
|
9300
|
+
return typeof process.getuid === "function";
|
|
9301
|
+
}
|
|
9302
|
+
|
|
9303
|
+
// src/local-agent-identity.ts
|
|
9304
|
+
var nodeFileSystem = {
|
|
9305
|
+
exists: existsSync,
|
|
9306
|
+
read: (path, maxBytes = MAX_LOCAL_IDENTITY_BYTES) => readSecureLocalText(path, {
|
|
9307
|
+
maxBytes,
|
|
9308
|
+
allowInsecurePermissions: true
|
|
9309
|
+
}),
|
|
9310
|
+
mode: secureLocalFileMode
|
|
9311
|
+
};
|
|
9312
|
+
var supportedProviders = /* @__PURE__ */ new Set([
|
|
9313
|
+
"other",
|
|
9314
|
+
"codex",
|
|
9315
|
+
"cursor",
|
|
9316
|
+
"claude_code"
|
|
9317
|
+
]);
|
|
9318
|
+
function localAgentIdentityPath(env = process.env, homeDirectory = homedir2()) {
|
|
9319
|
+
const explicit = env.REMEMBRANCE_AGENT_KEY_PATH?.trim();
|
|
9320
|
+
if (explicit) return explicit;
|
|
9321
|
+
return join2(
|
|
9322
|
+
env.XDG_CONFIG_HOME ?? join2(homeDirectory, ".config"),
|
|
9323
|
+
"remembrance",
|
|
9324
|
+
"agent-key.json"
|
|
9325
|
+
);
|
|
9326
|
+
}
|
|
9327
|
+
function parseStoredIdentity(value) {
|
|
9328
|
+
if (!isRecord(value)) return null;
|
|
9329
|
+
if (!supportedProviders.has(value.provider)) {
|
|
9330
|
+
return null;
|
|
9331
|
+
}
|
|
9332
|
+
for (const field of [
|
|
9333
|
+
"subject",
|
|
9334
|
+
"key_id",
|
|
9335
|
+
"public_key",
|
|
9336
|
+
"private_key",
|
|
9337
|
+
"created_at"
|
|
9338
|
+
]) {
|
|
9339
|
+
if (typeof value[field] !== "string" || !value[field].trim()) return null;
|
|
9340
|
+
}
|
|
9341
|
+
if (!Number.isFinite(Date.parse(value.created_at))) return null;
|
|
9342
|
+
try {
|
|
9343
|
+
const publicDer = createPublicKey2(value.public_key).export({
|
|
9344
|
+
type: "spki",
|
|
9345
|
+
format: "der"
|
|
9346
|
+
});
|
|
9347
|
+
const derivedPublicDer = createPublicKey2(
|
|
9348
|
+
createPrivateKey(value.private_key)
|
|
9349
|
+
).export({ type: "spki", format: "der" });
|
|
9350
|
+
if (!publicDer.equals(derivedPublicDer)) return null;
|
|
9351
|
+
} catch {
|
|
9352
|
+
return null;
|
|
9353
|
+
}
|
|
9354
|
+
return value;
|
|
9355
|
+
}
|
|
9356
|
+
function localAgentIdentityStatus(env = process.env, fileSystem = nodeFileSystem) {
|
|
9357
|
+
const path = localAgentIdentityPath(env);
|
|
9358
|
+
if (!fileSystem.exists(path)) {
|
|
9359
|
+
return identityStatus({
|
|
9360
|
+
status: "missing",
|
|
9361
|
+
present: false,
|
|
9362
|
+
valid: null,
|
|
9363
|
+
nextAction: {
|
|
9364
|
+
code: "bootstrap_agent_identity",
|
|
9365
|
+
arguments: {},
|
|
9366
|
+
automatic: true,
|
|
9367
|
+
confirmation: false
|
|
9368
|
+
}
|
|
9369
|
+
});
|
|
9370
|
+
}
|
|
9371
|
+
let mode = null;
|
|
9372
|
+
let securePermissions = null;
|
|
9373
|
+
try {
|
|
9374
|
+
const permissionBits = fileSystem.mode(path) & 511;
|
|
9375
|
+
mode = permissionBits.toString(8).padStart(4, "0");
|
|
9376
|
+
securePermissions = (permissionBits & 63) === 0;
|
|
9377
|
+
} catch {
|
|
9378
|
+
}
|
|
9379
|
+
let identity = null;
|
|
9380
|
+
try {
|
|
9381
|
+
identity = parseStoredIdentity(
|
|
9382
|
+
JSON.parse(fileSystem.read(path, MAX_LOCAL_IDENTITY_BYTES))
|
|
9383
|
+
);
|
|
9384
|
+
} catch {
|
|
9385
|
+
}
|
|
9386
|
+
if (!identity) {
|
|
9387
|
+
return identityStatus({
|
|
9388
|
+
status: "invalid",
|
|
9389
|
+
present: true,
|
|
9390
|
+
valid: false,
|
|
9391
|
+
mode,
|
|
9392
|
+
securePermissions,
|
|
9393
|
+
nextAction: {
|
|
9394
|
+
code: "restore_or_rotate_agent_identity",
|
|
9395
|
+
arguments: { force_rotate: true },
|
|
9396
|
+
automatic: false,
|
|
9397
|
+
confirmation: true
|
|
9398
|
+
}
|
|
9399
|
+
});
|
|
9400
|
+
}
|
|
9401
|
+
if (securePermissions === false) {
|
|
9402
|
+
return identityStatus({
|
|
9403
|
+
status: "insecure_permissions",
|
|
9404
|
+
present: true,
|
|
9405
|
+
valid: true,
|
|
9406
|
+
mode,
|
|
9407
|
+
securePermissions,
|
|
9408
|
+
identity,
|
|
9409
|
+
nextAction: {
|
|
9410
|
+
code: "repair_agent_identity_permissions",
|
|
9411
|
+
arguments: {},
|
|
9412
|
+
automatic: true,
|
|
9413
|
+
confirmation: false
|
|
9414
|
+
}
|
|
9415
|
+
});
|
|
9416
|
+
}
|
|
9417
|
+
if (securePermissions === null) {
|
|
9418
|
+
return identityStatus({
|
|
9419
|
+
status: "permissions_unknown",
|
|
9420
|
+
present: true,
|
|
9421
|
+
valid: true,
|
|
9422
|
+
mode,
|
|
9423
|
+
securePermissions,
|
|
9424
|
+
identity,
|
|
9425
|
+
nextAction: {
|
|
9426
|
+
code: "repair_agent_identity_permissions",
|
|
9427
|
+
arguments: {},
|
|
9428
|
+
automatic: true,
|
|
9429
|
+
confirmation: false
|
|
9430
|
+
}
|
|
9431
|
+
});
|
|
9432
|
+
}
|
|
9433
|
+
return identityStatus({
|
|
9434
|
+
status: "ready",
|
|
9435
|
+
present: true,
|
|
9436
|
+
valid: true,
|
|
9437
|
+
mode,
|
|
9438
|
+
securePermissions,
|
|
9439
|
+
identity,
|
|
9440
|
+
nextAction: null
|
|
9441
|
+
});
|
|
9442
|
+
}
|
|
9443
|
+
function identityStatus(args) {
|
|
9444
|
+
return {
|
|
9445
|
+
status: args.status,
|
|
9446
|
+
present: args.present,
|
|
9447
|
+
valid: args.valid,
|
|
9448
|
+
mode: args.mode ?? null,
|
|
9449
|
+
secure_permissions: args.securePermissions ?? null,
|
|
9450
|
+
provider: args.identity?.provider ?? null,
|
|
9451
|
+
key_id: args.identity?.key_id ?? null,
|
|
9452
|
+
created_at: args.identity?.created_at ?? null,
|
|
9453
|
+
signed_contributions_ready: args.status === "ready",
|
|
9454
|
+
next_action: args.nextAction ? {
|
|
9455
|
+
code: args.nextAction.code,
|
|
9456
|
+
tool: "bootstrap_agent_identity",
|
|
9457
|
+
arguments: args.nextAction.arguments,
|
|
9458
|
+
automatic_on_first_signed_submission: args.nextAction.automatic,
|
|
9459
|
+
requires_confirmation: args.nextAction.confirmation
|
|
9460
|
+
} : null
|
|
9461
|
+
};
|
|
9462
|
+
}
|
|
9463
|
+
function isRecord(value) {
|
|
9464
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
9465
|
+
}
|
|
9466
|
+
|
|
9467
|
+
// src/connection-status.ts
|
|
9468
|
+
var DEFAULT_API_URL = "https://remembrance.dev";
|
|
8818
9469
|
var PLUGIN_HEALTH_COMPONENTS = [
|
|
8819
9470
|
"session_start",
|
|
8820
9471
|
"prompt_hook",
|
|
@@ -8823,83 +9474,125 @@ var PLUGIN_HEALTH_COMPONENTS = [
|
|
|
8823
9474
|
];
|
|
8824
9475
|
var PLUGIN_HEALTH_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8825
9476
|
var PLUGIN_HEALTH_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
|
|
8826
|
-
var
|
|
8827
|
-
exists:
|
|
8828
|
-
read: (path) =>
|
|
8829
|
-
mode:
|
|
8830
|
-
list:
|
|
9477
|
+
var nodeFileSystem2 = {
|
|
9478
|
+
exists: existsSync2,
|
|
9479
|
+
read: (path, maxBytes = MAX_LOCAL_CONFIG_BYTES) => readSecureLocalText(path, { maxBytes }),
|
|
9480
|
+
mode: secureLocalFileMode,
|
|
9481
|
+
list: listSecureLocalDirectory
|
|
8831
9482
|
};
|
|
8832
|
-
function remembranceConfigPath(env = process.env, homeDirectory =
|
|
8833
|
-
const configRoot = env.REMEMBRANCE_PLUGIN_HOST?.trim().toLowerCase() === "openclaw" ?
|
|
8834
|
-
return
|
|
9483
|
+
function remembranceConfigPath(env = process.env, homeDirectory = homedir3()) {
|
|
9484
|
+
const configRoot = env.REMEMBRANCE_PLUGIN_HOST?.trim().toLowerCase() === "openclaw" ? join3(homeDirectory, ".config") : env.XDG_CONFIG_HOME ?? join3(homeDirectory, ".config");
|
|
9485
|
+
return join3(configRoot, "remembrance", "config.json");
|
|
8835
9486
|
}
|
|
8836
|
-
function resolveApiCredential(env = process.env, fileSystem =
|
|
9487
|
+
function resolveApiCredential(env = process.env, fileSystem = nodeFileSystem2) {
|
|
9488
|
+
const apiConfiguration2 = resolveApiConfiguration(env, fileSystem);
|
|
8837
9489
|
const environmentKey = env.REMEMBRANCE_API_KEY?.trim();
|
|
8838
9490
|
if (environmentKey) {
|
|
8839
|
-
|
|
9491
|
+
const explicitBinding = env.REMEMBRANCE_API_KEY_ORIGIN?.trim();
|
|
9492
|
+
const binding = explicitBinding ? normalizeApiUrl(explicitBinding, env) : { baseUrl: DEFAULT_API_URL, issue: null };
|
|
9493
|
+
if (!binding.baseUrl || binding.issue) {
|
|
9494
|
+
return unusableDestinationBinding();
|
|
9495
|
+
}
|
|
9496
|
+
return credentialForDestination(
|
|
9497
|
+
{
|
|
9498
|
+
apiKey: environmentKey,
|
|
9499
|
+
source: "environment",
|
|
9500
|
+
boundBaseUrl: binding.baseUrl
|
|
9501
|
+
},
|
|
9502
|
+
apiConfiguration2
|
|
9503
|
+
);
|
|
8840
9504
|
}
|
|
8841
9505
|
const path = remembranceConfigPath(env);
|
|
8842
9506
|
if (!fileSystem.exists(path)) {
|
|
8843
9507
|
return { apiKey: "", source: "none" };
|
|
8844
9508
|
}
|
|
8845
9509
|
try {
|
|
8846
|
-
const parsed = JSON.parse(
|
|
8847
|
-
|
|
8848
|
-
|
|
9510
|
+
const parsed = JSON.parse(
|
|
9511
|
+
fileSystem.read(path, MAX_LOCAL_CONFIG_BYTES)
|
|
9512
|
+
);
|
|
9513
|
+
if (!isRecord2(parsed)) {
|
|
9514
|
+
return unusableSharedConfig();
|
|
8849
9515
|
}
|
|
8850
9516
|
if (Object.prototype.hasOwnProperty.call(parsed, "apiKey") && (typeof parsed.apiKey !== "string" || !parsed.apiKey.trim())) {
|
|
8851
|
-
return
|
|
9517
|
+
return unusableSharedConfig();
|
|
8852
9518
|
}
|
|
8853
|
-
const
|
|
8854
|
-
|
|
9519
|
+
const config = normalizeConfig(parsed);
|
|
9520
|
+
if (!config.apiKey) {
|
|
9521
|
+
return { apiKey: "", source: "none" };
|
|
9522
|
+
}
|
|
9523
|
+
const binding = config.apiUrl ? normalizeApiUrl(config.apiUrl, env) : { baseUrl: DEFAULT_API_URL, issue: null };
|
|
9524
|
+
if (!binding.baseUrl || binding.issue) {
|
|
9525
|
+
return unusableSharedConfig();
|
|
9526
|
+
}
|
|
9527
|
+
return credentialForDestination(
|
|
9528
|
+
{
|
|
9529
|
+
apiKey: config.apiKey,
|
|
9530
|
+
source: "shared_config",
|
|
9531
|
+
boundBaseUrl: binding.baseUrl
|
|
9532
|
+
},
|
|
9533
|
+
apiConfiguration2
|
|
9534
|
+
);
|
|
8855
9535
|
} catch {
|
|
8856
|
-
return
|
|
9536
|
+
return unusableSharedConfig();
|
|
8857
9537
|
}
|
|
8858
9538
|
}
|
|
8859
|
-
function resolveApiKey(env = process.env, fileSystem =
|
|
9539
|
+
function resolveApiKey(env = process.env, fileSystem = nodeFileSystem2) {
|
|
8860
9540
|
return resolveApiCredential(env, fileSystem).apiKey;
|
|
8861
9541
|
}
|
|
8862
|
-
function resolveApiConfiguration(env = process.env, fileSystem =
|
|
9542
|
+
function resolveApiConfiguration(env = process.env, fileSystem = nodeFileSystem2) {
|
|
8863
9543
|
const environmentUrl = env.REMEMBRANCE_API_URL?.trim();
|
|
8864
9544
|
if (environmentUrl) {
|
|
8865
|
-
const normalized = normalizeApiUrl(environmentUrl);
|
|
8866
|
-
|
|
8867
|
-
baseUrl:
|
|
8868
|
-
|
|
9545
|
+
const normalized = normalizeApiUrl(environmentUrl, env);
|
|
9546
|
+
if (normalized.baseUrl !== null) {
|
|
9547
|
+
return { baseUrl: normalized.baseUrl, source: "environment" };
|
|
9548
|
+
}
|
|
9549
|
+
return {
|
|
9550
|
+
baseUrl: DEFAULT_API_URL,
|
|
9551
|
+
source: "unusable_environment",
|
|
9552
|
+
issue: normalized.issue
|
|
8869
9553
|
};
|
|
8870
9554
|
}
|
|
8871
9555
|
const path = remembranceConfigPath(env);
|
|
8872
9556
|
if (!fileSystem.exists(path)) {
|
|
8873
|
-
return { baseUrl:
|
|
9557
|
+
return { baseUrl: DEFAULT_API_URL, source: "default" };
|
|
8874
9558
|
}
|
|
8875
9559
|
try {
|
|
8876
|
-
const parsed = JSON.parse(
|
|
8877
|
-
|
|
9560
|
+
const parsed = JSON.parse(
|
|
9561
|
+
fileSystem.read(path, MAX_LOCAL_CONFIG_BYTES)
|
|
9562
|
+
);
|
|
9563
|
+
if (!isRecord2(parsed)) {
|
|
8878
9564
|
return {
|
|
8879
|
-
baseUrl:
|
|
8880
|
-
source: "unusable_shared_config"
|
|
9565
|
+
baseUrl: DEFAULT_API_URL,
|
|
9566
|
+
source: "unusable_shared_config",
|
|
9567
|
+
issue: "invalid_url"
|
|
8881
9568
|
};
|
|
8882
9569
|
}
|
|
8883
9570
|
if (!Object.prototype.hasOwnProperty.call(parsed, "apiUrl")) {
|
|
8884
|
-
return { baseUrl:
|
|
9571
|
+
return { baseUrl: DEFAULT_API_URL, source: "default" };
|
|
8885
9572
|
}
|
|
8886
|
-
const normalized = normalizeApiUrl(parsed.apiUrl);
|
|
8887
|
-
|
|
8888
|
-
baseUrl:
|
|
8889
|
-
|
|
9573
|
+
const normalized = normalizeApiUrl(parsed.apiUrl, env);
|
|
9574
|
+
if (normalized.baseUrl !== null) {
|
|
9575
|
+
return { baseUrl: normalized.baseUrl, source: "shared_config" };
|
|
9576
|
+
}
|
|
9577
|
+
return {
|
|
9578
|
+
baseUrl: DEFAULT_API_URL,
|
|
9579
|
+
source: "unusable_shared_config",
|
|
9580
|
+
issue: normalized.issue
|
|
8890
9581
|
};
|
|
8891
9582
|
} catch {
|
|
8892
9583
|
return {
|
|
8893
|
-
baseUrl:
|
|
8894
|
-
source: "unusable_shared_config"
|
|
9584
|
+
baseUrl: DEFAULT_API_URL,
|
|
9585
|
+
source: "unusable_shared_config",
|
|
9586
|
+
issue: "invalid_url"
|
|
8895
9587
|
};
|
|
8896
9588
|
}
|
|
8897
9589
|
}
|
|
8898
|
-
function sharedConfigStatus(env = process.env, fileSystem =
|
|
9590
|
+
function sharedConfigStatus(env = process.env, fileSystem = nodeFileSystem2) {
|
|
8899
9591
|
const path = remembranceConfigPath(env);
|
|
9592
|
+
const location = env.REMEMBRANCE_PLUGIN_HOST?.trim().toLowerCase() !== "openclaw" && Boolean(env.XDG_CONFIG_HOME) ? "custom_shared_config" : "default_shared_config";
|
|
8900
9593
|
if (!fileSystem.exists(path)) {
|
|
8901
9594
|
return {
|
|
8902
|
-
|
|
9595
|
+
location,
|
|
8903
9596
|
present: false,
|
|
8904
9597
|
valid_json: null,
|
|
8905
9598
|
api_key_present: false,
|
|
@@ -8919,13 +9612,15 @@ function sharedConfigStatus(env = process.env, fileSystem = nodeFileSystem) {
|
|
|
8919
9612
|
let validJson = false;
|
|
8920
9613
|
let config = {};
|
|
8921
9614
|
try {
|
|
8922
|
-
const parsed = JSON.parse(
|
|
8923
|
-
|
|
9615
|
+
const parsed = JSON.parse(
|
|
9616
|
+
fileSystem.read(path, MAX_LOCAL_CONFIG_BYTES)
|
|
9617
|
+
);
|
|
9618
|
+
validJson = isRecord2(parsed);
|
|
8924
9619
|
config = normalizeConfig(parsed);
|
|
8925
9620
|
} catch {
|
|
8926
9621
|
}
|
|
8927
9622
|
return {
|
|
8928
|
-
|
|
9623
|
+
location,
|
|
8929
9624
|
present: true,
|
|
8930
9625
|
valid_json: validJson,
|
|
8931
9626
|
api_key_present: Boolean(config.apiKey),
|
|
@@ -8935,36 +9630,50 @@ function sharedConfigStatus(env = process.env, fileSystem = nodeFileSystem) {
|
|
|
8935
9630
|
};
|
|
8936
9631
|
}
|
|
8937
9632
|
function localConnectionStatus(result, options) {
|
|
8938
|
-
const response =
|
|
9633
|
+
const response = isRecord2(result) ? result : {};
|
|
8939
9634
|
const env = options.env ?? process.env;
|
|
8940
|
-
const fileSystem = options.fileSystem ??
|
|
9635
|
+
const fileSystem = options.fileSystem ?? nodeFileSystem2;
|
|
8941
9636
|
const credential = resolveApiCredential(env, fileSystem);
|
|
9637
|
+
const sharedConfig = sharedConfigStatus(env, fileSystem);
|
|
9638
|
+
const apiDestination = apiDestinationStatus(
|
|
9639
|
+
options.apiBase,
|
|
9640
|
+
options.apiUrlSource
|
|
9641
|
+
);
|
|
8942
9642
|
const pluginHealth = localPluginLifecycleHealth({
|
|
8943
9643
|
env,
|
|
8944
9644
|
fileSystem,
|
|
8945
9645
|
credentialSource: credential.source,
|
|
8946
9646
|
pluginVersion: options.pluginVersion,
|
|
9647
|
+
apiDestinationFingerprint: apiDestination.fingerprint,
|
|
9648
|
+
apiDestinationSource: options.apiUrlSource,
|
|
8947
9649
|
now: options.now
|
|
8948
9650
|
});
|
|
8949
9651
|
const registryReady = response.ok === true;
|
|
9652
|
+
const insecureCredential = sharedConfig.api_key_present && sharedConfig.secure_permissions === false;
|
|
9653
|
+
const unknownCredentialPermissions = sharedConfig.api_key_present && sharedConfig.secure_permissions === null;
|
|
8950
9654
|
return {
|
|
8951
|
-
status: registryReady && pluginHealth.status === "degraded" ? "degraded" : registryReady && pluginHealth.status === "partial" ? "partial" : registryReady ? "ready" : "error",
|
|
9655
|
+
status: registryReady && (pluginHealth.status === "degraded" || insecureCredential) ? "degraded" : registryReady && (pluginHealth.status === "partial" || unknownCredentialPermissions) ? "partial" : registryReady ? "ready" : "error",
|
|
8952
9656
|
http_status: typeof response.status === "number" ? response.status : null,
|
|
8953
9657
|
transport: {
|
|
8954
9658
|
kind: "local_stdio_mcp",
|
|
8955
9659
|
credential_source: credential.source,
|
|
8956
|
-
api_url:
|
|
9660
|
+
api_url: apiDestination.kind === "remembrance_cloud" ? "https://remembrance.dev" : null,
|
|
8957
9661
|
api_url_source: options.apiUrlSource,
|
|
8958
|
-
|
|
8959
|
-
|
|
9662
|
+
api_destination: {
|
|
9663
|
+
kind: apiDestination.kind,
|
|
9664
|
+
source: apiDestination.source
|
|
9665
|
+
},
|
|
9666
|
+
shared_config: sharedConfig,
|
|
9667
|
+
credential_boundary: credential.source === "unusable_shared_config" || options.apiUrlSource === "unusable_shared_config" ? "The shared Remembrance config exists but is unreadable or invalid. Remote tools fail locally until it is fixed or intentionally removed; no request silently falls back to anonymous scope." : credential.source === "unusable_destination_binding" ? "The configured API key is not bound to this registry destination. Remote tools remain paused so a credential cannot be forwarded to an unintended origin." : options.apiUrlSource === "unusable_environment" ? "REMEMBRANCE_API_URL is invalid or unsafe. Remote tools remain paused until it uses HTTPS (except loopback), any private destination is explicitly allowed, and the credential is bound to the same origin." : "This local MCP process resolves REMEMBRANCE_API_KEY first, then the shared config file. Anonymous curl or browser probes do not test this process."
|
|
8960
9668
|
},
|
|
8961
9669
|
registry: response.body ?? (response.error ? { error: String(response.error) } : null),
|
|
9670
|
+
local_signing_identity: localAgentIdentityStatus(env, fileSystem),
|
|
8962
9671
|
plugin_health: pluginHealth
|
|
8963
9672
|
};
|
|
8964
9673
|
}
|
|
8965
9674
|
function localPluginLifecycleHealth(options) {
|
|
8966
9675
|
const env = options.env ?? process.env;
|
|
8967
|
-
const fileSystem = options.fileSystem ??
|
|
9676
|
+
const fileSystem = options.fileSystem ?? nodeFileSystem2;
|
|
8968
9677
|
const rawSurface = String(env.REMEMBRANCE_PLUGIN_HOST ?? "").trim().toLowerCase();
|
|
8969
9678
|
if (!rawSurface) {
|
|
8970
9679
|
return {
|
|
@@ -8991,7 +9700,7 @@ function localPluginLifecycleHealth(options) {
|
|
|
8991
9700
|
}
|
|
8992
9701
|
const surface = rawSurface;
|
|
8993
9702
|
const host = agentHostBySurface(surface);
|
|
8994
|
-
const healthDir = env.REMEMBRANCE_PLUGIN_HEALTH_DIR ? String(env.REMEMBRANCE_PLUGIN_HEALTH_DIR) :
|
|
9703
|
+
const healthDir = env.REMEMBRANCE_PLUGIN_HEALTH_DIR ? String(env.REMEMBRANCE_PLUGIN_HEALTH_DIR) : join3(homedir3(), ".cache", "remembrance", "plugin-health");
|
|
8995
9704
|
const paths = lifecycleMarkerPaths(surface, healthDir, fileSystem);
|
|
8996
9705
|
if (paths.length === 0) {
|
|
8997
9706
|
return missingPluginLifecycleHealth(surface);
|
|
@@ -9000,8 +9709,10 @@ function localPluginLifecycleHealth(options) {
|
|
|
9000
9709
|
let newestMarkerMs = Number.NEGATIVE_INFINITY;
|
|
9001
9710
|
for (const path of paths) {
|
|
9002
9711
|
try {
|
|
9003
|
-
const parsed = JSON.parse(
|
|
9004
|
-
|
|
9712
|
+
const parsed = JSON.parse(
|
|
9713
|
+
fileSystem.read(path, MAX_LOCAL_LIFECYCLE_MARKER_BYTES)
|
|
9714
|
+
);
|
|
9715
|
+
if (!isRecord2(parsed) || parsed.surface !== surface) {
|
|
9005
9716
|
continue;
|
|
9006
9717
|
}
|
|
9007
9718
|
const candidateMs = typeof parsed.last_seen_at === "string" ? Date.parse(parsed.last_seen_at) : Number.NEGATIVE_INFINITY;
|
|
@@ -9023,7 +9734,7 @@ function localPluginLifecycleHealth(options) {
|
|
|
9023
9734
|
]
|
|
9024
9735
|
};
|
|
9025
9736
|
}
|
|
9026
|
-
const componentRecord =
|
|
9737
|
+
const componentRecord = isRecord2(marker.components) ? marker.components : {};
|
|
9027
9738
|
const nowMs = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
9028
9739
|
const sessionStartMs = validLifecycleTimestamp(
|
|
9029
9740
|
componentRecord.session_start,
|
|
@@ -9088,6 +9799,19 @@ function localPluginLifecycleHealth(options) {
|
|
|
9088
9799
|
action: "Use the shared Remembrance config for both hooks and local MCP, or give both processes the same REMEMBRANCE_API_KEY, then restart the host."
|
|
9089
9800
|
});
|
|
9090
9801
|
}
|
|
9802
|
+
const markerDestinationFingerprint = typeof marker.api_destination_fingerprint === "string" && /^[a-f0-9]{16}$/.test(marker.api_destination_fingerprint) ? marker.api_destination_fingerprint : null;
|
|
9803
|
+
const markerDestinationSource = typeof marker.api_destination_source === "string" ? marker.api_destination_source : null;
|
|
9804
|
+
if (options.apiDestinationFingerprint && !markerDestinationFingerprint) {
|
|
9805
|
+
issues.push({
|
|
9806
|
+
code: "api_destination_not_observed",
|
|
9807
|
+
action: "Fully restart the host, submit one prompt, and call run_connection_doctor again so both destinations can be compared."
|
|
9808
|
+
});
|
|
9809
|
+
} else if (options.apiDestinationFingerprint && markerDestinationFingerprint && options.apiDestinationFingerprint !== markerDestinationFingerprint) {
|
|
9810
|
+
issues.push({
|
|
9811
|
+
code: "api_destination_mismatch",
|
|
9812
|
+
action: "Configure native hooks and bundled MCP to use the same Remembrance registry, fully restart the host, and rerun the doctor."
|
|
9813
|
+
});
|
|
9814
|
+
}
|
|
9091
9815
|
const onlyAwaitingEligibleEvents = issues.length > 0 && issues.every(
|
|
9092
9816
|
(issue) => ["tool_observer_not_observed", "completion_hook_not_observed"].includes(
|
|
9093
9817
|
issue.code
|
|
@@ -9101,11 +9825,24 @@ function localPluginLifecycleHealth(options) {
|
|
|
9101
9825
|
host_version: typeof marker.host_version === "string" && marker.host_version ? marker.host_version : null,
|
|
9102
9826
|
last_seen_at: lastSeenAt,
|
|
9103
9827
|
credential_source: markerCredential,
|
|
9828
|
+
api_destination: {
|
|
9829
|
+
source: markerDestinationSource,
|
|
9830
|
+
matches_mcp: markerDestinationFingerprint && options.apiDestinationFingerprint ? markerDestinationFingerprint === options.apiDestinationFingerprint : null,
|
|
9831
|
+
mcp_source: options.apiDestinationSource ?? null
|
|
9832
|
+
},
|
|
9104
9833
|
components,
|
|
9105
9834
|
issues,
|
|
9106
9835
|
explanation: "Each host session is tracked independently. SessionStart and the prompt hook must run first; tool and completion observations appear after those lifecycle events become eligible."
|
|
9107
9836
|
};
|
|
9108
9837
|
}
|
|
9838
|
+
function apiDestinationStatus(baseUrl, source) {
|
|
9839
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
9840
|
+
return {
|
|
9841
|
+
kind: normalized === "https://remembrance.dev" ? "remembrance_cloud" : "custom",
|
|
9842
|
+
source,
|
|
9843
|
+
fingerprint: createHash2("sha256").update(normalized, "utf8").digest("hex").slice(0, 16)
|
|
9844
|
+
};
|
|
9845
|
+
}
|
|
9109
9846
|
function missingPluginLifecycleHealth(surface) {
|
|
9110
9847
|
return {
|
|
9111
9848
|
expected: true,
|
|
@@ -9129,7 +9866,7 @@ function emptyPluginComponents() {
|
|
|
9129
9866
|
);
|
|
9130
9867
|
}
|
|
9131
9868
|
function lifecycleMarkerPaths(surface, healthDir, fileSystem) {
|
|
9132
|
-
const legacyPath =
|
|
9869
|
+
const legacyPath = join3(healthDir, `${surface}.json`);
|
|
9133
9870
|
const listed = fileSystem.list ? (() => {
|
|
9134
9871
|
try {
|
|
9135
9872
|
return fileSystem.list(healthDir);
|
|
@@ -9139,12 +9876,12 @@ function lifecycleMarkerPaths(surface, healthDir, fileSystem) {
|
|
|
9139
9876
|
})() : [];
|
|
9140
9877
|
const sessionPaths = listed.filter(
|
|
9141
9878
|
(name) => name.startsWith(`${surface}.`) && name.endsWith(".json") && /^[a-z_]+\.[a-f0-9]{24}\.json$/.test(name)
|
|
9142
|
-
).map((name) =>
|
|
9879
|
+
).slice(0, MAX_LOCAL_LIFECYCLE_MARKERS).map((name) => join3(healthDir, name));
|
|
9143
9880
|
if (fileSystem.exists(legacyPath)) sessionPaths.push(legacyPath);
|
|
9144
|
-
return [...new Set(sessionPaths)];
|
|
9881
|
+
return [...new Set(sessionPaths)].slice(0, MAX_LOCAL_LIFECYCLE_MARKERS);
|
|
9145
9882
|
}
|
|
9146
9883
|
function normalizeConfig(value) {
|
|
9147
|
-
if (!
|
|
9884
|
+
if (!isRecord2(value)) return {};
|
|
9148
9885
|
const apiKey = typeof value.apiKey === "string" ? value.apiKey.trim() : "";
|
|
9149
9886
|
const apiUrl = typeof value.apiUrl === "string" ? value.apiUrl.trim() : "";
|
|
9150
9887
|
return {
|
|
@@ -9152,19 +9889,81 @@ function normalizeConfig(value) {
|
|
|
9152
9889
|
...apiUrl ? { apiUrl } : {}
|
|
9153
9890
|
};
|
|
9154
9891
|
}
|
|
9155
|
-
function normalizeApiUrl(value) {
|
|
9156
|
-
if (typeof value !== "string" || !value.trim())
|
|
9892
|
+
function normalizeApiUrl(value, env) {
|
|
9893
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
9894
|
+
return { baseUrl: null, issue: "invalid_url" };
|
|
9895
|
+
}
|
|
9157
9896
|
const candidate = value.trim();
|
|
9158
9897
|
try {
|
|
9159
9898
|
const parsed = new URL(candidate);
|
|
9160
9899
|
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
9161
|
-
return null;
|
|
9900
|
+
return { baseUrl: null, issue: "invalid_url" };
|
|
9901
|
+
}
|
|
9902
|
+
const loopback = isLoopbackHostname(parsed.hostname);
|
|
9903
|
+
const privateDestination = isPrivateDestinationHostname(parsed.hostname);
|
|
9904
|
+
if (parsed.protocol === "http:" && !loopback) {
|
|
9905
|
+
return { baseUrl: null, issue: "insecure_remote_http" };
|
|
9162
9906
|
}
|
|
9163
|
-
|
|
9907
|
+
if (!loopback && privateDestination && env.REMEMBRANCE_ALLOW_PRIVATE_REGISTRY !== "true") {
|
|
9908
|
+
return {
|
|
9909
|
+
baseUrl: null,
|
|
9910
|
+
issue: "private_destination_requires_opt_in"
|
|
9911
|
+
};
|
|
9912
|
+
}
|
|
9913
|
+
return { baseUrl: candidate.replace(/\/+$/, ""), issue: null };
|
|
9164
9914
|
} catch {
|
|
9165
|
-
return null;
|
|
9915
|
+
return { baseUrl: null, issue: "invalid_url" };
|
|
9166
9916
|
}
|
|
9167
9917
|
}
|
|
9918
|
+
function credentialForDestination(credential, configuration) {
|
|
9919
|
+
if (configuration.source.startsWith("unusable_")) {
|
|
9920
|
+
return unusableDestinationBinding();
|
|
9921
|
+
}
|
|
9922
|
+
return credential.boundBaseUrl === configuration.baseUrl ? { apiKey: credential.apiKey, source: credential.source } : unusableDestinationBinding();
|
|
9923
|
+
}
|
|
9924
|
+
function unusableSharedConfig() {
|
|
9925
|
+
return {
|
|
9926
|
+
apiKey: "",
|
|
9927
|
+
source: "unusable_shared_config"
|
|
9928
|
+
};
|
|
9929
|
+
}
|
|
9930
|
+
function unusableDestinationBinding() {
|
|
9931
|
+
return {
|
|
9932
|
+
apiKey: "",
|
|
9933
|
+
source: "unusable_destination_binding"
|
|
9934
|
+
};
|
|
9935
|
+
}
|
|
9936
|
+
function isLoopbackHostname(hostname) {
|
|
9937
|
+
const normalized = normalizeHostname(hostname);
|
|
9938
|
+
if (normalized === "localhost" || normalized.endsWith(".localhost")) {
|
|
9939
|
+
return true;
|
|
9940
|
+
}
|
|
9941
|
+
if (normalized === "::1") return true;
|
|
9942
|
+
if (isIP(normalized) === 4) {
|
|
9943
|
+
return Number(normalized.split(".")[0]) === 127;
|
|
9944
|
+
}
|
|
9945
|
+
return false;
|
|
9946
|
+
}
|
|
9947
|
+
function isPrivateDestinationHostname(hostname) {
|
|
9948
|
+
const normalized = normalizeHostname(hostname);
|
|
9949
|
+
if (normalized.endsWith(".local") || normalized.endsWith(".internal") || normalized === "localhost") {
|
|
9950
|
+
return true;
|
|
9951
|
+
}
|
|
9952
|
+
const version = isIP(normalized);
|
|
9953
|
+
if (version === 4) return isNonPublicIpv4(normalized);
|
|
9954
|
+
if (version === 6) {
|
|
9955
|
+
const compact = normalized.toLowerCase();
|
|
9956
|
+
return compact === "::" || compact === "::1" || compact.startsWith("fc") || compact.startsWith("fd") || /^fe[89ab]/.test(compact);
|
|
9957
|
+
}
|
|
9958
|
+
return false;
|
|
9959
|
+
}
|
|
9960
|
+
function isNonPublicIpv4(address) {
|
|
9961
|
+
const [a = -1, b = -1] = address.split(".").map(Number);
|
|
9962
|
+
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a >= 224;
|
|
9963
|
+
}
|
|
9964
|
+
function normalizeHostname(hostname) {
|
|
9965
|
+
return hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
9966
|
+
}
|
|
9168
9967
|
function validLifecycleTimestamp(value, nowMs) {
|
|
9169
9968
|
if (typeof value !== "string") return Number.NaN;
|
|
9170
9969
|
const parsed = Date.parse(value);
|
|
@@ -9173,28 +9972,33 @@ function validLifecycleTimestamp(value, nowMs) {
|
|
|
9173
9972
|
}
|
|
9174
9973
|
return parsed;
|
|
9175
9974
|
}
|
|
9176
|
-
function
|
|
9975
|
+
function isRecord2(value) {
|
|
9177
9976
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
9178
9977
|
}
|
|
9179
9978
|
|
|
9180
9979
|
// src/server.ts
|
|
9181
9980
|
var apiConfiguration = resolveApiConfiguration();
|
|
9182
9981
|
var apiBase = apiConfiguration.baseUrl;
|
|
9183
|
-
var
|
|
9982
|
+
var MAX_REMOTE_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
9983
|
+
var DOCTOR_PROBE_TIMEOUT_MS = 7500;
|
|
9984
|
+
var SERVER_VERSION = true ? "0.1.49" : "0.0.0-dev";
|
|
9184
9985
|
var tools = toolDefinitions;
|
|
9986
|
+
var doctorCliRequested = process.argv[2] === "doctor";
|
|
9185
9987
|
var inputBuffer = Buffer.alloc(0);
|
|
9186
9988
|
var clientFraming = "ndjson";
|
|
9187
9989
|
var cachedEconomicsSession = null;
|
|
9188
9990
|
var cachedValueProofKeys = null;
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9991
|
+
if (!doctorCliRequested) {
|
|
9992
|
+
process.stdin.on("data", (chunk) => {
|
|
9993
|
+
inputBuffer = Buffer.concat([inputBuffer, chunk]);
|
|
9994
|
+
processMessages().catch(() => {
|
|
9995
|
+
writeResponse(null, null, {
|
|
9996
|
+
code: -32603,
|
|
9997
|
+
message: "Internal MCP error."
|
|
9998
|
+
});
|
|
9195
9999
|
});
|
|
9196
10000
|
});
|
|
9197
|
-
}
|
|
10001
|
+
}
|
|
9198
10002
|
async function processMessages() {
|
|
9199
10003
|
const parsed = readJsonRpcMessages(inputBuffer);
|
|
9200
10004
|
inputBuffer = parsed.remaining;
|
|
@@ -9393,6 +10197,9 @@ async function callTool(definition, rawArguments) {
|
|
|
9393
10197
|
if (definition.local === "queue_private_skill_import") {
|
|
9394
10198
|
return queuePrivateSkillHandoff(payload, { uploadBaseUrl: apiBase });
|
|
9395
10199
|
}
|
|
10200
|
+
if (definition.name === "run_connection_doctor") {
|
|
10201
|
+
return runLocalConnectionDoctor(payload);
|
|
10202
|
+
}
|
|
9396
10203
|
if (definition.name === "submit_feedback") {
|
|
9397
10204
|
return submitFeedback(payload);
|
|
9398
10205
|
}
|
|
@@ -9401,7 +10208,8 @@ async function callTool(definition, rawArguments) {
|
|
|
9401
10208
|
}
|
|
9402
10209
|
const localConfigurationError = remoteConfigurationError(
|
|
9403
10210
|
apiConfiguration.source,
|
|
9404
|
-
resolveApiCredential().source
|
|
10211
|
+
resolveApiCredential().source,
|
|
10212
|
+
apiConfiguration.issue
|
|
9405
10213
|
);
|
|
9406
10214
|
const result = definition.name === "get_connection_status" && localConfigurationError ? { ok: false, status: 0, error: localConfigurationError } : await callRemembrance(definition, payload);
|
|
9407
10215
|
if (definition.name === "get_connection_status") {
|
|
@@ -9417,17 +10225,29 @@ async function callTool(definition, rawArguments) {
|
|
|
9417
10225
|
}
|
|
9418
10226
|
return result;
|
|
9419
10227
|
}
|
|
9420
|
-
function remoteConfigurationError(apiUrlSource, credentialSource) {
|
|
10228
|
+
function remoteConfigurationError(apiUrlSource, credentialSource, apiUrlIssue) {
|
|
10229
|
+
if (credentialSource === "unusable_destination_binding") {
|
|
10230
|
+
return "The Remembrance API key is not bound to the configured registry destination. For a custom registry, store apiKey and apiUrl together in the shared config, or set REMEMBRANCE_API_KEY_ORIGIN to the exact REMEMBRANCE_API_URL value. Remote tools remain paused so the key cannot be forwarded elsewhere.";
|
|
10231
|
+
}
|
|
9421
10232
|
if (apiUrlSource === "unusable_environment") {
|
|
9422
|
-
return "REMEMBRANCE_API_URL
|
|
10233
|
+
return apiConfigurationErrorMessage(apiUrlIssue, "REMEMBRANCE_API_URL");
|
|
9423
10234
|
}
|
|
9424
10235
|
if (apiUrlSource === "unusable_shared_config" || credentialSource === "unusable_shared_config") {
|
|
9425
10236
|
return "The shared Remembrance config exists but is unreadable or invalid. Fix or intentionally remove ~/.config/remembrance/config.json before using remote Remembrance tools.";
|
|
9426
10237
|
}
|
|
9427
10238
|
return null;
|
|
9428
10239
|
}
|
|
10240
|
+
function apiConfigurationErrorMessage(issue, setting) {
|
|
10241
|
+
if (issue === "insecure_remote_http") {
|
|
10242
|
+
return `${setting} may use HTTP only for a loopback registry. Use HTTPS for every remote registry before using remote Remembrance tools.`;
|
|
10243
|
+
}
|
|
10244
|
+
if (issue === "private_destination_requires_opt_in") {
|
|
10245
|
+
return `${setting} targets a private or link-local registry. Set REMEMBRANCE_ALLOW_PRIVATE_REGISTRY=true only for an intentionally trusted HTTPS self-hosted registry.`;
|
|
10246
|
+
}
|
|
10247
|
+
return `${setting} is invalid. Set it to an absolute HTTP(S) registry URL without credentials, query parameters, or fragments, or intentionally remove it before using remote Remembrance tools.`;
|
|
10248
|
+
}
|
|
9429
10249
|
function clientHealthReportFromConnectionStatus(status) {
|
|
9430
|
-
const health =
|
|
10250
|
+
const health = isRecord3(status.plugin_health) ? status.plugin_health : null;
|
|
9431
10251
|
if (!health || health.expected !== true || health.status !== "degraded") {
|
|
9432
10252
|
return null;
|
|
9433
10253
|
}
|
|
@@ -9437,9 +10257,9 @@ function clientHealthReportFromConnectionStatus(status) {
|
|
|
9437
10257
|
}
|
|
9438
10258
|
const host = agentHostBySurface(surface);
|
|
9439
10259
|
if (!host || !host.plugin) return null;
|
|
9440
|
-
const components =
|
|
9441
|
-
const observed = (name) =>
|
|
9442
|
-
const
|
|
10260
|
+
const components = isRecord3(health.components) ? health.components : {};
|
|
10261
|
+
const observed = (name) => isRecord3(components[name]) && components[name]?.observed === true ? "active" : "not_observed";
|
|
10262
|
+
const issueCodes2 = Array.isArray(health.issues) ? health.issues.map((issue) => isRecord3(issue) ? issue.code : null).filter((code) => typeof code === "string") : [];
|
|
9443
10263
|
const allowedIssueCodes = /* @__PURE__ */ new Set([
|
|
9444
10264
|
"native_hooks_not_observed",
|
|
9445
10265
|
"session_start_not_observed",
|
|
@@ -9447,12 +10267,14 @@ function clientHealthReportFromConnectionStatus(status) {
|
|
|
9447
10267
|
"completion_hook_not_observed",
|
|
9448
10268
|
"tool_observer_not_observed",
|
|
9449
10269
|
"credential_source_mismatch",
|
|
10270
|
+
"api_destination_mismatch",
|
|
10271
|
+
"api_destination_not_observed",
|
|
9450
10272
|
"plugin_version_mismatch",
|
|
9451
10273
|
"lifecycle_marker_stale",
|
|
9452
10274
|
"invalid_lifecycle_marker",
|
|
9453
10275
|
"unsupported_plugin_host"
|
|
9454
10276
|
]);
|
|
9455
|
-
const boundedIssues =
|
|
10277
|
+
const boundedIssues = issueCodes2.filter(
|
|
9456
10278
|
(code) => allowedIssueCodes.has(code)
|
|
9457
10279
|
);
|
|
9458
10280
|
const candidate = {
|
|
@@ -9547,12 +10369,7 @@ async function submitFeedback(payload) {
|
|
|
9547
10369
|
if (!verifiedAttestation) {
|
|
9548
10370
|
return callRemembrance(mustFindTool("submit_feedback"), request);
|
|
9549
10371
|
}
|
|
9550
|
-
const identity = await
|
|
9551
|
-
if (!identity) {
|
|
9552
|
-
throw new McpPublicError(
|
|
9553
|
-
"No local Remembrance agent identity found. Run bootstrap_agent_identity first."
|
|
9554
|
-
);
|
|
9555
|
-
}
|
|
10372
|
+
const identity = await ensureSigningIdentity();
|
|
9556
10373
|
const agent = request.agent ?? {
|
|
9557
10374
|
provider: agentProviderForIdentity(identity.provider),
|
|
9558
10375
|
agent_id: identity.subject
|
|
@@ -9597,12 +10414,7 @@ async function submitFeedback(payload) {
|
|
|
9597
10414
|
});
|
|
9598
10415
|
}
|
|
9599
10416
|
async function signedRemembranceAttestation(remembrancePayload, skillSlug) {
|
|
9600
|
-
const identity = await
|
|
9601
|
-
if (!identity) {
|
|
9602
|
-
throw new McpPublicError(
|
|
9603
|
-
"No local Remembrance agent identity found. Run bootstrap_agent_identity first."
|
|
9604
|
-
);
|
|
9605
|
-
}
|
|
10417
|
+
const identity = await ensureSigningIdentity();
|
|
9606
10418
|
const evidenceHash = attestationEvidenceHashForRemembrance(remembrancePayload);
|
|
9607
10419
|
const challenge = await callRemembrance(
|
|
9608
10420
|
mustFindTool("request_attestation_challenge"),
|
|
@@ -9623,7 +10435,7 @@ async function signedRemembranceAttestation(remembrancePayload, skillSlug) {
|
|
|
9623
10435
|
const signature = signPayload(
|
|
9624
10436
|
null,
|
|
9625
10437
|
Buffer.from(signingPayload),
|
|
9626
|
-
|
|
10438
|
+
createPrivateKey2(identity.private_key)
|
|
9627
10439
|
).toString("base64url");
|
|
9628
10440
|
return {
|
|
9629
10441
|
version: "v2",
|
|
@@ -9642,8 +10454,13 @@ async function signedRemembranceAttestation(remembrancePayload, skillSlug) {
|
|
|
9642
10454
|
}
|
|
9643
10455
|
async function bootstrapAgentIdentity(args) {
|
|
9644
10456
|
const keyPath = identityPath(args.key_path);
|
|
9645
|
-
|
|
9646
|
-
|
|
10457
|
+
let reusedExistingIdentity = !args.force_rotate && existsSync3(keyPath);
|
|
10458
|
+
let identity = reusedExistingIdentity ? await readIdentity(keyPath) : null;
|
|
10459
|
+
if (!identity) {
|
|
10460
|
+
const created = await createAndPersistIdentity(args, keyPath);
|
|
10461
|
+
identity = created.identity;
|
|
10462
|
+
reusedExistingIdentity = created.reusedExistingIdentity;
|
|
10463
|
+
}
|
|
9647
10464
|
if (!identity) {
|
|
9648
10465
|
throw new McpPublicError(
|
|
9649
10466
|
"Unable to create or read the local Remembrance agent identity. Check the configured key path and file permissions, then retry bootstrap_agent_identity."
|
|
@@ -9652,10 +10469,10 @@ async function bootstrapAgentIdentity(args) {
|
|
|
9652
10469
|
const mismatchedFields = [];
|
|
9653
10470
|
if (reusedExistingIdentity) {
|
|
9654
10471
|
if (typeof args.subject === "string" && args.subject !== identity.subject) {
|
|
9655
|
-
mismatchedFields.push(
|
|
10472
|
+
mismatchedFields.push("subject");
|
|
9656
10473
|
}
|
|
9657
10474
|
if (typeof args.key_id === "string" && args.key_id !== identity.key_id) {
|
|
9658
|
-
mismatchedFields.push(
|
|
10475
|
+
mismatchedFields.push("key_id");
|
|
9659
10476
|
}
|
|
9660
10477
|
}
|
|
9661
10478
|
const signedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9669,7 +10486,7 @@ async function bootstrapAgentIdentity(args) {
|
|
|
9669
10486
|
const proofSignature = signPayload(
|
|
9670
10487
|
null,
|
|
9671
10488
|
Buffer.from(proofPayload),
|
|
9672
|
-
|
|
10489
|
+
createPrivateKey2(identity.private_key)
|
|
9673
10490
|
).toString("base64url");
|
|
9674
10491
|
const registration = await callRemembrance(
|
|
9675
10492
|
mustFindTool("register_agent_key"),
|
|
@@ -9688,14 +10505,18 @@ async function bootstrapAgentIdentity(args) {
|
|
|
9688
10505
|
}
|
|
9689
10506
|
}
|
|
9690
10507
|
);
|
|
10508
|
+
if (registration.ok !== true) {
|
|
10509
|
+
throw new McpPublicError(
|
|
10510
|
+
`The local Remembrance identity is secure, but registration failed${registration.status ? ` (${registration.status})` : ""}. Check get_connection_status, then retry bootstrap_agent_identity.`
|
|
10511
|
+
);
|
|
10512
|
+
}
|
|
9691
10513
|
return {
|
|
9692
|
-
key_path:
|
|
10514
|
+
storage: args.key_path ? "custom_path" : "default_local_config",
|
|
9693
10515
|
provider: identity.provider,
|
|
9694
|
-
subject: identity.subject,
|
|
9695
10516
|
key_id: identity.key_id,
|
|
9696
10517
|
reused_existing_identity: reusedExistingIdentity,
|
|
9697
10518
|
...mismatchedFields.length > 0 ? {
|
|
9698
|
-
warning: `
|
|
10519
|
+
warning: `The existing local identity was reused; the requested ${mismatchedFields.join(" and ")} ${mismatchedFields.length > 1 ? "were" : "was"} ignored. Pass force_rotate: true only when intentionally replacing its trust history.`
|
|
9699
10520
|
} : {},
|
|
9700
10521
|
registration
|
|
9701
10522
|
};
|
|
@@ -9705,28 +10526,79 @@ async function createAndPersistIdentity(args, keyPath) {
|
|
|
9705
10526
|
const publicKeyPem = String(
|
|
9706
10527
|
publicKey.export({ type: "spki", format: "pem" })
|
|
9707
10528
|
);
|
|
10529
|
+
const keyId = args.key_id ?? defaultAgentKeyIdForPublicKey(publicKeyPem);
|
|
9708
10530
|
const identity = {
|
|
9709
10531
|
provider: args.provider ?? "other",
|
|
9710
|
-
subject: args.subject
|
|
9711
|
-
key_id:
|
|
10532
|
+
subject: args.subject ?? `local:${keyId}`,
|
|
10533
|
+
key_id: keyId,
|
|
9712
10534
|
public_key: publicKeyPem,
|
|
9713
10535
|
private_key: String(privateKey.export({ type: "pkcs8", format: "pem" })),
|
|
9714
10536
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9715
10537
|
};
|
|
9716
|
-
await mkdir2(
|
|
9717
|
-
|
|
10538
|
+
await mkdir2(dirname2(keyPath), { recursive: true, mode: 448 });
|
|
10539
|
+
try {
|
|
10540
|
+
await writeFile2(keyPath, `${JSON.stringify(identity, null, 2)}
|
|
9718
10541
|
`, {
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
10542
|
+
mode: 384,
|
|
10543
|
+
flag: args.force_rotate ? "w" : "wx"
|
|
10544
|
+
});
|
|
10545
|
+
return { identity, reusedExistingIdentity: false };
|
|
10546
|
+
} catch (error) {
|
|
10547
|
+
if (!args.force_rotate && isFileAlreadyExistsError(error)) {
|
|
10548
|
+
const existing = await readIdentity(keyPath);
|
|
10549
|
+
if (existing) {
|
|
10550
|
+
return { identity: existing, reusedExistingIdentity: true };
|
|
10551
|
+
}
|
|
10552
|
+
}
|
|
10553
|
+
throw error;
|
|
10554
|
+
}
|
|
9722
10555
|
}
|
|
9723
10556
|
async function readIdentity(path = identityPath()) {
|
|
9724
|
-
if (!
|
|
10557
|
+
if (!existsSync3(path)) {
|
|
9725
10558
|
return null;
|
|
9726
10559
|
}
|
|
9727
|
-
|
|
10560
|
+
try {
|
|
10561
|
+
const identity = parseStoredIdentity(
|
|
10562
|
+
JSON.parse(
|
|
10563
|
+
readSecureLocalText(path, {
|
|
10564
|
+
maxBytes: MAX_LOCAL_IDENTITY_BYTES,
|
|
10565
|
+
repairPermissions: true
|
|
10566
|
+
})
|
|
10567
|
+
)
|
|
10568
|
+
);
|
|
10569
|
+
if (!identity) {
|
|
10570
|
+
throw new Error("invalid identity");
|
|
10571
|
+
}
|
|
10572
|
+
return identity;
|
|
10573
|
+
} catch {
|
|
10574
|
+
throw new McpPublicError(
|
|
10575
|
+
"The local Remembrance agent identity is unreadable, invalid, or cannot be secured. Restore the original key file, or explicitly run bootstrap_agent_identity with force_rotate: true to start a new trust history."
|
|
10576
|
+
);
|
|
10577
|
+
}
|
|
10578
|
+
}
|
|
10579
|
+
var automaticIdentityBootstrap = null;
|
|
10580
|
+
async function ensureSigningIdentity() {
|
|
10581
|
+
if (automaticIdentityBootstrap) return automaticIdentityBootstrap;
|
|
10582
|
+
const existing = await readIdentity();
|
|
10583
|
+
if (automaticIdentityBootstrap) return automaticIdentityBootstrap;
|
|
10584
|
+
if (existing) return existing;
|
|
10585
|
+
if (!automaticIdentityBootstrap) {
|
|
10586
|
+
automaticIdentityBootstrap = (async () => {
|
|
10587
|
+
await bootstrapAgentIdentity({});
|
|
10588
|
+
const created = await readIdentity();
|
|
10589
|
+
if (!created) {
|
|
10590
|
+
throw new McpPublicError(
|
|
10591
|
+
"Remembrance could not initialize the local signing identity. Run bootstrap_agent_identity and retry."
|
|
10592
|
+
);
|
|
10593
|
+
}
|
|
10594
|
+
return created;
|
|
10595
|
+
})().finally(() => {
|
|
10596
|
+
automaticIdentityBootstrap = null;
|
|
10597
|
+
});
|
|
10598
|
+
}
|
|
10599
|
+
return automaticIdentityBootstrap;
|
|
9728
10600
|
}
|
|
9729
|
-
async function callRemembrance(definition, rawArguments) {
|
|
10601
|
+
async function callRemembrance(definition, rawArguments, options = {}) {
|
|
9730
10602
|
const parsed = definition.schema.parse(rawArguments ?? {});
|
|
9731
10603
|
const payload = definition.name === "query_skills" || definition.name === "invoke_skill" ? {
|
|
9732
10604
|
...parsed,
|
|
@@ -9741,7 +10613,8 @@ async function callRemembrance(definition, rawArguments) {
|
|
|
9741
10613
|
const credential = resolveApiCredential();
|
|
9742
10614
|
const configurationError = remoteConfigurationError(
|
|
9743
10615
|
apiConfiguration.source,
|
|
9744
|
-
credential.source
|
|
10616
|
+
credential.source,
|
|
10617
|
+
apiConfiguration.issue
|
|
9745
10618
|
);
|
|
9746
10619
|
if (configurationError && definition.name !== "get_connection_status") {
|
|
9747
10620
|
throw new McpPublicError(configurationError);
|
|
@@ -9750,7 +10623,7 @@ async function callRemembrance(definition, rawArguments) {
|
|
|
9750
10623
|
if (apiKey) {
|
|
9751
10624
|
headers["x-remembrance-api-key"] = apiKey;
|
|
9752
10625
|
}
|
|
9753
|
-
if (definition.name !== "
|
|
10626
|
+
if (!options.skipEconomicsSession && definition.name !== "register_agent_key" && definition.name !== "request_attestation_challenge" && definition.name !== "get_connection_status") {
|
|
9754
10627
|
const economicsSession = await ensureEconomicsSessionToken().catch(
|
|
9755
10628
|
() => null
|
|
9756
10629
|
);
|
|
@@ -9760,8 +10633,12 @@ async function callRemembrance(definition, rawArguments) {
|
|
|
9760
10633
|
}
|
|
9761
10634
|
const endpoint = endpointFor(definition, payload);
|
|
9762
10635
|
const controller = new AbortController();
|
|
9763
|
-
const timeout = setTimeout(
|
|
10636
|
+
const timeout = setTimeout(
|
|
10637
|
+
() => controller.abort(),
|
|
10638
|
+
options.timeoutMs ?? apiTimeoutMs()
|
|
10639
|
+
);
|
|
9764
10640
|
let response;
|
|
10641
|
+
let body;
|
|
9765
10642
|
try {
|
|
9766
10643
|
response = await fetch(`${apiBase}${endpoint}`, {
|
|
9767
10644
|
method: definition.method ?? "POST",
|
|
@@ -9769,6 +10646,20 @@ async function callRemembrance(definition, rawArguments) {
|
|
|
9769
10646
|
body: definition.method === "GET" ? void 0 : JSON.stringify(payload),
|
|
9770
10647
|
signal: controller.signal
|
|
9771
10648
|
});
|
|
10649
|
+
try {
|
|
10650
|
+
body = await readBoundedResponseJson(response);
|
|
10651
|
+
} catch {
|
|
10652
|
+
if (controller.signal.aborted) {
|
|
10653
|
+
return {
|
|
10654
|
+
ok: false,
|
|
10655
|
+
status: 0,
|
|
10656
|
+
error: "Remembrance API request timed out."
|
|
10657
|
+
};
|
|
10658
|
+
}
|
|
10659
|
+
body = {
|
|
10660
|
+
error: response.ok ? "Remembrance API returned an unreadable or oversized response." : "Remembrance API request failed."
|
|
10661
|
+
};
|
|
10662
|
+
}
|
|
9772
10663
|
} catch (error) {
|
|
9773
10664
|
return {
|
|
9774
10665
|
ok: false,
|
|
@@ -9778,9 +10669,6 @@ async function callRemembrance(definition, rawArguments) {
|
|
|
9778
10669
|
} finally {
|
|
9779
10670
|
clearTimeout(timeout);
|
|
9780
10671
|
}
|
|
9781
|
-
const body = await response.json().catch(() => ({
|
|
9782
|
-
error: response.ok ? "Remembrance API returned an unreadable response." : "Remembrance API request failed."
|
|
9783
|
-
}));
|
|
9784
10672
|
const verifiedBody = definition.name === "get_value_proof" && response.ok ? await verifySignedValueProofWithKeyRefresh(body, fetchValueProofKeySet) : body;
|
|
9785
10673
|
return {
|
|
9786
10674
|
ok: response.ok,
|
|
@@ -9791,6 +10679,115 @@ async function callRemembrance(definition, rawArguments) {
|
|
|
9791
10679
|
body: verifiedBody
|
|
9792
10680
|
};
|
|
9793
10681
|
}
|
|
10682
|
+
async function runLocalConnectionDoctor(options, execution = "mcp_tool") {
|
|
10683
|
+
const connectionDefinition = mustFindTool("get_connection_status");
|
|
10684
|
+
const localConfigurationError = remoteConfigurationError(
|
|
10685
|
+
apiConfiguration.source,
|
|
10686
|
+
resolveApiCredential().source,
|
|
10687
|
+
apiConfiguration.issue
|
|
10688
|
+
);
|
|
10689
|
+
const connectionPromise = localConfigurationError ? Promise.resolve({
|
|
10690
|
+
ok: false,
|
|
10691
|
+
status: 0,
|
|
10692
|
+
error: localConfigurationError
|
|
10693
|
+
}) : callDoctorReadProbe(connectionDefinition, {});
|
|
10694
|
+
const catalogPromise = options.active_read_probe && !localConfigurationError ? callDoctorReadProbe(mustFindTool("list_skills"), { limit: 1 }) : Promise.resolve(null);
|
|
10695
|
+
const [connectionResult, catalogResult] = await Promise.all([
|
|
10696
|
+
connectionPromise,
|
|
10697
|
+
catalogPromise
|
|
10698
|
+
]);
|
|
10699
|
+
const connectionStatus = localConnectionStatus(connectionResult, {
|
|
10700
|
+
apiBase,
|
|
10701
|
+
apiUrlSource: apiConfiguration.source,
|
|
10702
|
+
pluginVersion: SERVER_VERSION
|
|
10703
|
+
});
|
|
10704
|
+
const activeReadProbe = {
|
|
10705
|
+
requested: options.active_read_probe,
|
|
10706
|
+
succeeded: null,
|
|
10707
|
+
item_count: null
|
|
10708
|
+
};
|
|
10709
|
+
if (options.active_read_probe && !localConfigurationError) {
|
|
10710
|
+
const resultRecord = isRecord3(catalogResult) ? catalogResult : {};
|
|
10711
|
+
const body = isRecord3(resultRecord.body) ? resultRecord.body : {};
|
|
10712
|
+
const skills = Array.isArray(body.skills) ? body.skills : null;
|
|
10713
|
+
activeReadProbe.succeeded = resultRecord.ok === true && skills !== null;
|
|
10714
|
+
activeReadProbe.item_count = skills ? skills.length : null;
|
|
10715
|
+
} else if (options.active_read_probe) {
|
|
10716
|
+
activeReadProbe.succeeded = false;
|
|
10717
|
+
}
|
|
10718
|
+
return buildConnectionDoctorReport({
|
|
10719
|
+
connection_status: connectionStatus,
|
|
10720
|
+
transport: execution === "standalone_cli" ? "standalone_cli" : "local_stdio_mcp",
|
|
10721
|
+
host_registration_observed: execution === "mcp_tool",
|
|
10722
|
+
active_read_probe: activeReadProbe,
|
|
10723
|
+
check_organization_write_authorization: options.check_organization_write_authorization
|
|
10724
|
+
});
|
|
10725
|
+
}
|
|
10726
|
+
async function callDoctorReadProbe(definition, payload) {
|
|
10727
|
+
const first = await callRemembrance(definition, payload, {
|
|
10728
|
+
skipEconomicsSession: true,
|
|
10729
|
+
timeoutMs: DOCTOR_PROBE_TIMEOUT_MS
|
|
10730
|
+
});
|
|
10731
|
+
return isTransientDoctorProbeFailure(first) ? callRemembrance(definition, payload, {
|
|
10732
|
+
skipEconomicsSession: true,
|
|
10733
|
+
timeoutMs: DOCTOR_PROBE_TIMEOUT_MS
|
|
10734
|
+
}) : first;
|
|
10735
|
+
}
|
|
10736
|
+
function isTransientDoctorProbeFailure(result) {
|
|
10737
|
+
if (!isRecord3(result) || result.ok === true) return false;
|
|
10738
|
+
const status = typeof result.status === "number" ? result.status : 0;
|
|
10739
|
+
return status === 0 || status === 408 || status === 425 || status >= 500;
|
|
10740
|
+
}
|
|
10741
|
+
function formatConnectionDoctorReport(report) {
|
|
10742
|
+
const lines = [
|
|
10743
|
+
`Remembrance connection doctor: ${report.status.toUpperCase()}`,
|
|
10744
|
+
`Transport: ${report.transport}`,
|
|
10745
|
+
`Scope: ${report.scope}`,
|
|
10746
|
+
`Destination: ${report.destination.kind}`,
|
|
10747
|
+
`Query ready: ${report.safe_to_query ? "yes" : "no"}`,
|
|
10748
|
+
`Organization submissions: ${report.organization_write_authorized ? "authorized" : "not authorized"}`,
|
|
10749
|
+
`Signed contributions: ${report.signed_contributions_ready === null ? "not observable" : report.signed_contributions_ready ? "ready" : "not ready"}`,
|
|
10750
|
+
""
|
|
10751
|
+
];
|
|
10752
|
+
for (const check of report.checks) {
|
|
10753
|
+
lines.push(`[${check.status.toUpperCase()}] ${check.id}: ${check.summary}`);
|
|
10754
|
+
if (check.remediation) {
|
|
10755
|
+
lines.push(` Fix: ${check.remediation.action}`);
|
|
10756
|
+
if (check.remediation.command) {
|
|
10757
|
+
lines.push(` Command: ${check.remediation.command}`);
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
return `${lines.join("\n")}
|
|
10762
|
+
`;
|
|
10763
|
+
}
|
|
10764
|
+
async function runConnectionDoctorCli(args = process.argv.slice(3)) {
|
|
10765
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
10766
|
+
process.stdout.write(
|
|
10767
|
+
"Usage: remembrance-mcp doctor [--json] [--no-probe] [--skip-write-check]\n"
|
|
10768
|
+
);
|
|
10769
|
+
return 0;
|
|
10770
|
+
}
|
|
10771
|
+
const allowed = /* @__PURE__ */ new Set(["--json", "--no-probe", "--skip-write-check"]);
|
|
10772
|
+
const unknown = args.find((argument) => !allowed.has(argument));
|
|
10773
|
+
if (unknown) {
|
|
10774
|
+
process.stderr.write(`Unknown doctor option: ${unknown}
|
|
10775
|
+
`);
|
|
10776
|
+
return 2;
|
|
10777
|
+
}
|
|
10778
|
+
const report = await runLocalConnectionDoctor(
|
|
10779
|
+
{
|
|
10780
|
+
active_read_probe: !args.includes("--no-probe"),
|
|
10781
|
+
check_organization_write_authorization: !args.includes("--skip-write-check")
|
|
10782
|
+
},
|
|
10783
|
+
"standalone_cli"
|
|
10784
|
+
);
|
|
10785
|
+
process.stdout.write(
|
|
10786
|
+
args.includes("--json") ? `${JSON.stringify(report, null, 2)}
|
|
10787
|
+
` : formatConnectionDoctorReport(report)
|
|
10788
|
+
);
|
|
10789
|
+
return report.status === "blocked" ? 1 : 0;
|
|
10790
|
+
}
|
|
9794
10791
|
async function fetchValueProofKeySet(forceRefresh) {
|
|
9795
10792
|
const now = Date.now();
|
|
9796
10793
|
if (!forceRefresh && cachedValueProofKeys && cachedValueProofKeys.expiresAt > now) {
|
|
@@ -9811,7 +10808,7 @@ async function fetchValueProofKeySet(forceRefresh) {
|
|
|
9811
10808
|
`Value proof verification keys are unavailable (${response.status}).`
|
|
9812
10809
|
);
|
|
9813
10810
|
}
|
|
9814
|
-
const value = await response
|
|
10811
|
+
const value = await readBoundedResponseJson(response);
|
|
9815
10812
|
cachedValueProofKeys = {
|
|
9816
10813
|
value,
|
|
9817
10814
|
expiresAt: now + valueProofKeyCacheTtlMs(response.headers)
|
|
@@ -9834,7 +10831,34 @@ function apiTimeoutMs() {
|
|
|
9834
10831
|
process.env.REMEMBRANCE_API_TIMEOUT_MS ?? "",
|
|
9835
10832
|
10
|
|
9836
10833
|
);
|
|
9837
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 3e4;
|
|
10834
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 6e4) : 3e4;
|
|
10835
|
+
}
|
|
10836
|
+
async function readBoundedResponseJson(response) {
|
|
10837
|
+
if (!response.body) return {};
|
|
10838
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
10839
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_REMOTE_RESPONSE_BYTES) {
|
|
10840
|
+
throw new Error("Remembrance API response exceeded its size limit.");
|
|
10841
|
+
}
|
|
10842
|
+
const reader = response.body.getReader();
|
|
10843
|
+
const decoder = new TextDecoder();
|
|
10844
|
+
let total = 0;
|
|
10845
|
+
let text = "";
|
|
10846
|
+
try {
|
|
10847
|
+
while (true) {
|
|
10848
|
+
const { done, value } = await reader.read();
|
|
10849
|
+
if (done) break;
|
|
10850
|
+
total += value.byteLength;
|
|
10851
|
+
if (total > MAX_REMOTE_RESPONSE_BYTES) {
|
|
10852
|
+
await reader.cancel();
|
|
10853
|
+
throw new Error("Remembrance API response exceeded its size limit.");
|
|
10854
|
+
}
|
|
10855
|
+
text += decoder.decode(value, { stream: true });
|
|
10856
|
+
}
|
|
10857
|
+
text += decoder.decode();
|
|
10858
|
+
return text ? JSON.parse(text) : {};
|
|
10859
|
+
} finally {
|
|
10860
|
+
reader.releaseLock();
|
|
10861
|
+
}
|
|
9838
10862
|
}
|
|
9839
10863
|
function endpointFor(definition, payload) {
|
|
9840
10864
|
if (!definition.endpoint) {
|
|
@@ -9922,7 +10946,7 @@ async function ensureEconomicsSessionToken() {
|
|
|
9922
10946
|
const signature = signPayload(
|
|
9923
10947
|
null,
|
|
9924
10948
|
Buffer.from(challenge.signing_payload),
|
|
9925
|
-
|
|
10949
|
+
createPrivateKey2(identity.private_key)
|
|
9926
10950
|
).toString("base64url");
|
|
9927
10951
|
const exchangeResponse = await directEconomicsSessionRequest({
|
|
9928
10952
|
action: "exchange",
|
|
@@ -9963,21 +10987,14 @@ function mustFindTool(name) {
|
|
|
9963
10987
|
}
|
|
9964
10988
|
return tool2;
|
|
9965
10989
|
}
|
|
9966
|
-
function
|
|
10990
|
+
function isRecord3(value) {
|
|
9967
10991
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
9968
10992
|
}
|
|
9969
10993
|
function identityPath(explicit) {
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
return process.env.REMEMBRANCE_AGENT_KEY_PATH;
|
|
9975
|
-
}
|
|
9976
|
-
return join3(
|
|
9977
|
-
process.env.XDG_CONFIG_HOME ?? join3(homedir3(), ".config"),
|
|
9978
|
-
"remembrance",
|
|
9979
|
-
"agent-key.json"
|
|
9980
|
-
);
|
|
10994
|
+
return explicit?.trim() || localAgentIdentityPath();
|
|
10995
|
+
}
|
|
10996
|
+
function isFileAlreadyExistsError(error) {
|
|
10997
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
9981
10998
|
}
|
|
9982
10999
|
function agentProviderForIdentity(provider) {
|
|
9983
11000
|
if (provider === "codex" || provider === "cursor") {
|
|
@@ -10005,13 +11022,26 @@ ${body}`;
|
|
|
10005
11022
|
return `${body}
|
|
10006
11023
|
`;
|
|
10007
11024
|
}
|
|
11025
|
+
if (doctorCliRequested) {
|
|
11026
|
+
void runConnectionDoctorCli().then((code) => {
|
|
11027
|
+
process.exitCode = code;
|
|
11028
|
+
}).catch(() => {
|
|
11029
|
+
process.stderr.write(
|
|
11030
|
+
"Remembrance connection doctor could not complete safely.\n"
|
|
11031
|
+
);
|
|
11032
|
+
process.exitCode = 1;
|
|
11033
|
+
});
|
|
11034
|
+
}
|
|
10008
11035
|
export {
|
|
10009
11036
|
callTool,
|
|
10010
11037
|
clientHealthReportFromConnectionStatus,
|
|
10011
11038
|
dispatchJsonRpcRequest,
|
|
11039
|
+
formatConnectionDoctorReport,
|
|
10012
11040
|
formatJsonRpcResponse,
|
|
10013
11041
|
readJsonRpcMessages,
|
|
10014
11042
|
remoteConfigurationError,
|
|
10015
11043
|
reportDegradedClientHealth,
|
|
10016
|
-
resetValueProofKeyCacheForTests
|
|
11044
|
+
resetValueProofKeyCacheForTests,
|
|
11045
|
+
runConnectionDoctorCli,
|
|
11046
|
+
runLocalConnectionDoctor
|
|
10017
11047
|
};
|