@remembrance-ai/opencode-plugin 0.1.46 → 0.1.48

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.
@@ -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 REMEMBRANCE_MCP_SERVER_INSTRUCTIONS = "Remembrance is shared operational memory for agents. Before claiming that Remembrance is anonymous, unconfigured, or missing an organization key, call get_connection_status for the transport you will actually use. 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.";
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.5",
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.5",
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, call MCP \`get_connection_status\`. It names the active transport,
5926
- credential source, verified organization/public scope, and config permission
5927
- status without exposing the key. An anonymous curl or browser probe describes
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 call
5942
- \`get_connection_status\`. A healthy install reports \`local_stdio_mcp\`, the
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 \`get_connection_status\` is absent, or if
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 call
6082
- \`get_connection_status\` and confirm \`shared_config\`, \`local_stdio_mcp\`,
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 call
6173
- \`get_connection_status\` and confirm organization scope before contribution
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 call \`get_connection_status\` and confirm
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, call
6273
- \`get_connection_status\` and confirm organization scope. See the official
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. Call \`get_connection_status\` and confirm
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 call
6309
- \`get_connection_status\` and confirm organization scope before private writes.
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 get_connection_status and confirm the active transport, credential
6411
- source, and expected public/organization scope before inspecting environment
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, run bootstrap_agent_identity once when verified TOFU
6457
- contributions are needed.
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 existsSync2 } from "node:fs";
7259
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
7260
- import { homedir as homedir3 } from "node:os";
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("Stable agent identity, for example codex:user@example.com."),
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 if present."
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 if present. Requires skill.slug."
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. Use once per agent installation; rerun to re-bootstrap if the local agent-key.json was lost.",
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, readFileSync, readdirSync, statSync } from "node:fs";
8816
- import { homedir as homedir2, tmpdir } from "node:os";
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 nodeFileSystem = {
8827
- exists: existsSync,
8828
- read: (path) => readFileSync(path, "utf8"),
8829
- mode: (path) => statSync(path).mode,
8830
- list: (path) => readdirSync(path)
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 = homedir2()) {
8833
- const configRoot = env.REMEMBRANCE_PLUGIN_HOST?.trim().toLowerCase() === "openclaw" ? join2(homeDirectory, ".config") : env.XDG_CONFIG_HOME ?? join2(homeDirectory, ".config");
8834
- return join2(configRoot, "remembrance", "config.json");
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 = nodeFileSystem) {
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
- return { apiKey: environmentKey, source: "environment" };
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(fileSystem.read(path));
8847
- if (!isRecord(parsed)) {
8848
- return { apiKey: "", source: "unusable_shared_config" };
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 { apiKey: "", source: "unusable_shared_config" };
9517
+ return unusableSharedConfig();
8852
9518
  }
8853
- const apiKey = normalizeConfig(parsed).apiKey;
8854
- return apiKey ? { apiKey, source: "shared_config" } : { apiKey: "", source: "none" };
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 { apiKey: "", source: "unusable_shared_config" };
9536
+ return unusableSharedConfig();
8857
9537
  }
8858
9538
  }
8859
- function resolveApiKey(env = process.env, fileSystem = nodeFileSystem) {
9539
+ function resolveApiKey(env = process.env, fileSystem = nodeFileSystem2) {
8860
9540
  return resolveApiCredential(env, fileSystem).apiKey;
8861
9541
  }
8862
- function resolveApiConfiguration(env = process.env, fileSystem = nodeFileSystem) {
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
- return normalized ? { baseUrl: normalized, source: "environment" } : {
8867
- baseUrl: "https://remembrance.dev",
8868
- source: "unusable_environment"
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: "https://remembrance.dev", source: "default" };
9557
+ return { baseUrl: DEFAULT_API_URL, source: "default" };
8874
9558
  }
8875
9559
  try {
8876
- const parsed = JSON.parse(fileSystem.read(path));
8877
- if (!isRecord(parsed)) {
9560
+ const parsed = JSON.parse(
9561
+ fileSystem.read(path, MAX_LOCAL_CONFIG_BYTES)
9562
+ );
9563
+ if (!isRecord2(parsed)) {
8878
9564
  return {
8879
- baseUrl: "https://remembrance.dev",
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: "https://remembrance.dev", source: "default" };
9571
+ return { baseUrl: DEFAULT_API_URL, source: "default" };
8885
9572
  }
8886
- const normalized = normalizeApiUrl(parsed.apiUrl);
8887
- return normalized ? { baseUrl: normalized, source: "shared_config" } : {
8888
- baseUrl: "https://remembrance.dev",
8889
- source: "unusable_shared_config"
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: "https://remembrance.dev",
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 = nodeFileSystem) {
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
- path,
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(fileSystem.read(path));
8923
- validJson = isRecord(parsed);
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
- path,
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 = isRecord(result) ? result : {};
9633
+ const response = isRecord2(result) ? result : {};
8939
9634
  const env = options.env ?? process.env;
8940
- const fileSystem = options.fileSystem ?? nodeFileSystem;
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: options.apiBase,
9660
+ api_url: apiDestination.kind === "remembrance_cloud" ? "https://remembrance.dev" : null,
8957
9661
  api_url_source: options.apiUrlSource,
8958
- shared_config: sharedConfigStatus(env, fileSystem),
8959
- 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." : options.apiUrlSource === "unusable_environment" ? "REMEMBRANCE_API_URL is invalid. Remote tools fail locally until it is set to an absolute HTTP(S) registry URL or intentionally removed." : "This local MCP process resolves REMEMBRANCE_API_KEY first, then the shared config file. Anonymous curl or browser probes do not test this process."
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 ?? nodeFileSystem;
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) : join2(tmpdir(), "remembrance-plugin-health");
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(fileSystem.read(path));
9004
- if (!isRecord(parsed) || parsed.surface !== surface) {
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 = isRecord(marker.components) ? marker.components : {};
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 = join2(healthDir, `${surface}.json`);
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) => join2(healthDir, 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 (!isRecord(value)) return {};
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()) return null;
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
- return candidate.replace(/\/+$/, "");
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 isRecord(value) {
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 SERVER_VERSION = true ? "0.1.46" : "0.0.0-dev";
9982
+ var MAX_REMOTE_RESPONSE_BYTES = 4 * 1024 * 1024;
9983
+ var DOCTOR_PROBE_TIMEOUT_MS = 7500;
9984
+ var SERVER_VERSION = true ? "0.1.48" : "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
- process.stdin.on("data", (chunk) => {
9190
- inputBuffer = Buffer.concat([inputBuffer, chunk]);
9191
- processMessages().catch(() => {
9192
- writeResponse(null, null, {
9193
- code: -32603,
9194
- message: "Internal MCP error."
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 is invalid. Set it to an absolute HTTP(S) registry URL or intentionally remove it before using remote Remembrance tools.";
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 = isRecord2(status.plugin_health) ? status.plugin_health : null;
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 = isRecord2(health.components) ? health.components : {};
9441
- const observed = (name) => isRecord2(components[name]) && components[name]?.observed === true ? "active" : "not_observed";
9442
- const issueCodes = Array.isArray(health.issues) ? health.issues.map((issue) => isRecord2(issue) ? issue.code : null).filter((code) => typeof code === "string") : [];
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 = issueCodes.filter(
10277
+ const boundedIssues = issueCodes2.filter(
9456
10278
  (code) => allowedIssueCodes.has(code)
9457
10279
  );
9458
10280
  const candidate = {
@@ -9520,7 +10342,7 @@ async function submitRemembrance(payload) {
9520
10342
  }
9521
10343
  const skillSlug = request.skill?.slug;
9522
10344
  if (!skillSlug) {
9523
- throw new Error("verified_attestation requires skill.slug.");
10345
+ throw new McpPublicError("verified_attestation requires skill.slug.");
9524
10346
  }
9525
10347
  const evidence = {
9526
10348
  trace_hash: request.evidence.trace_hash ?? null,
@@ -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 readIdentity();
9551
- if (!identity) {
9552
- throw new Error(
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 readIdentity();
9601
- if (!identity) {
9602
- throw new Error(
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
- createPrivateKey(identity.private_key)
10438
+ createPrivateKey2(identity.private_key)
9627
10439
  ).toString("base64url");
9628
10440
  return {
9629
10441
  version: "v2",
@@ -9642,18 +10454,25 @@ async function signedRemembranceAttestation(remembrancePayload, skillSlug) {
9642
10454
  }
9643
10455
  async function bootstrapAgentIdentity(args) {
9644
10456
  const keyPath = identityPath(args.key_path);
9645
- const reusedExistingIdentity = !args.force_rotate && existsSync2(keyPath);
9646
- const identity = reusedExistingIdentity ? await readIdentity(keyPath) : await createAndPersistIdentity(args, keyPath);
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
- throw new Error("Unable to create or read Remembrance agent identity.");
10465
+ throw new McpPublicError(
10466
+ "Unable to create or read the local Remembrance agent identity. Check the configured key path and file permissions, then retry bootstrap_agent_identity."
10467
+ );
9649
10468
  }
9650
10469
  const mismatchedFields = [];
9651
10470
  if (reusedExistingIdentity) {
9652
10471
  if (typeof args.subject === "string" && args.subject !== identity.subject) {
9653
- mismatchedFields.push(`subject "${args.subject}"`);
10472
+ mismatchedFields.push("subject");
9654
10473
  }
9655
10474
  if (typeof args.key_id === "string" && args.key_id !== identity.key_id) {
9656
- mismatchedFields.push(`key_id "${args.key_id}"`);
10475
+ mismatchedFields.push("key_id");
9657
10476
  }
9658
10477
  }
9659
10478
  const signedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -9667,7 +10486,7 @@ async function bootstrapAgentIdentity(args) {
9667
10486
  const proofSignature = signPayload(
9668
10487
  null,
9669
10488
  Buffer.from(proofPayload),
9670
- createPrivateKey(identity.private_key)
10489
+ createPrivateKey2(identity.private_key)
9671
10490
  ).toString("base64url");
9672
10491
  const registration = await callRemembrance(
9673
10492
  mustFindTool("register_agent_key"),
@@ -9686,14 +10505,18 @@ async function bootstrapAgentIdentity(args) {
9686
10505
  }
9687
10506
  }
9688
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
+ }
9689
10513
  return {
9690
- key_path: keyPath,
10514
+ storage: args.key_path ? "custom_path" : "default_local_config",
9691
10515
  provider: identity.provider,
9692
- subject: identity.subject,
9693
10516
  key_id: identity.key_id,
9694
10517
  reused_existing_identity: reusedExistingIdentity,
9695
10518
  ...mismatchedFields.length > 0 ? {
9696
- warning: `An existing identity (subject "${identity.subject}", key_id "${identity.key_id}") was reused; the requested ${mismatchedFields.join(" and ")} ${mismatchedFields.length > 1 ? "were" : "was"} ignored. Pass force_rotate: true to mint a new identity.`
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.`
9697
10520
  } : {},
9698
10521
  registration
9699
10522
  };
@@ -9703,28 +10526,79 @@ async function createAndPersistIdentity(args, keyPath) {
9703
10526
  const publicKeyPem = String(
9704
10527
  publicKey.export({ type: "spki", format: "pem" })
9705
10528
  );
10529
+ const keyId = args.key_id ?? defaultAgentKeyIdForPublicKey(publicKeyPem);
9706
10530
  const identity = {
9707
10531
  provider: args.provider ?? "other",
9708
- subject: args.subject,
9709
- key_id: args.key_id ?? defaultAgentKeyIdForPublicKey(publicKeyPem),
10532
+ subject: args.subject ?? `local:${keyId}`,
10533
+ key_id: keyId,
9710
10534
  public_key: publicKeyPem,
9711
10535
  private_key: String(privateKey.export({ type: "pkcs8", format: "pem" })),
9712
10536
  created_at: (/* @__PURE__ */ new Date()).toISOString()
9713
10537
  };
9714
- await mkdir2(dirname(keyPath), { recursive: true, mode: 448 });
9715
- await writeFile2(keyPath, `${JSON.stringify(identity, null, 2)}
10538
+ await mkdir2(dirname2(keyPath), { recursive: true, mode: 448 });
10539
+ try {
10540
+ await writeFile2(keyPath, `${JSON.stringify(identity, null, 2)}
9716
10541
  `, {
9717
- mode: 384
9718
- });
9719
- return identity;
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
+ }
9720
10555
  }
9721
10556
  async function readIdentity(path = identityPath()) {
9722
- if (!existsSync2(path)) {
10557
+ if (!existsSync3(path)) {
9723
10558
  return null;
9724
10559
  }
9725
- return JSON.parse(await readFile2(path, "utf8"));
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;
9726
10600
  }
9727
- async function callRemembrance(definition, rawArguments) {
10601
+ async function callRemembrance(definition, rawArguments, options = {}) {
9728
10602
  const parsed = definition.schema.parse(rawArguments ?? {});
9729
10603
  const payload = definition.name === "query_skills" || definition.name === "invoke_skill" ? {
9730
10604
  ...parsed,
@@ -9739,7 +10613,8 @@ async function callRemembrance(definition, rawArguments) {
9739
10613
  const credential = resolveApiCredential();
9740
10614
  const configurationError = remoteConfigurationError(
9741
10615
  apiConfiguration.source,
9742
- credential.source
10616
+ credential.source,
10617
+ apiConfiguration.issue
9743
10618
  );
9744
10619
  if (configurationError && definition.name !== "get_connection_status") {
9745
10620
  throw new McpPublicError(configurationError);
@@ -9748,7 +10623,7 @@ async function callRemembrance(definition, rawArguments) {
9748
10623
  if (apiKey) {
9749
10624
  headers["x-remembrance-api-key"] = apiKey;
9750
10625
  }
9751
- if (definition.name !== "bootstrap_agent_identity" && definition.name !== "get_connection_status") {
10626
+ if (!options.skipEconomicsSession && definition.name !== "register_agent_key" && definition.name !== "request_attestation_challenge" && definition.name !== "get_connection_status") {
9752
10627
  const economicsSession = await ensureEconomicsSessionToken().catch(
9753
10628
  () => null
9754
10629
  );
@@ -9758,8 +10633,12 @@ async function callRemembrance(definition, rawArguments) {
9758
10633
  }
9759
10634
  const endpoint = endpointFor(definition, payload);
9760
10635
  const controller = new AbortController();
9761
- const timeout = setTimeout(() => controller.abort(), apiTimeoutMs());
10636
+ const timeout = setTimeout(
10637
+ () => controller.abort(),
10638
+ options.timeoutMs ?? apiTimeoutMs()
10639
+ );
9762
10640
  let response;
10641
+ let body;
9763
10642
  try {
9764
10643
  response = await fetch(`${apiBase}${endpoint}`, {
9765
10644
  method: definition.method ?? "POST",
@@ -9767,6 +10646,20 @@ async function callRemembrance(definition, rawArguments) {
9767
10646
  body: definition.method === "GET" ? void 0 : JSON.stringify(payload),
9768
10647
  signal: controller.signal
9769
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
+ }
9770
10663
  } catch (error) {
9771
10664
  return {
9772
10665
  ok: false,
@@ -9776,9 +10669,6 @@ async function callRemembrance(definition, rawArguments) {
9776
10669
  } finally {
9777
10670
  clearTimeout(timeout);
9778
10671
  }
9779
- const body = await response.json().catch(() => ({
9780
- error: response.ok ? "Remembrance API returned an unreadable response." : "Remembrance API request failed."
9781
- }));
9782
10672
  const verifiedBody = definition.name === "get_value_proof" && response.ok ? await verifySignedValueProofWithKeyRefresh(body, fetchValueProofKeySet) : body;
9783
10673
  return {
9784
10674
  ok: response.ok,
@@ -9789,6 +10679,115 @@ async function callRemembrance(definition, rawArguments) {
9789
10679
  body: verifiedBody
9790
10680
  };
9791
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
+ }
9792
10791
  async function fetchValueProofKeySet(forceRefresh) {
9793
10792
  const now = Date.now();
9794
10793
  if (!forceRefresh && cachedValueProofKeys && cachedValueProofKeys.expiresAt > now) {
@@ -9809,7 +10808,7 @@ async function fetchValueProofKeySet(forceRefresh) {
9809
10808
  `Value proof verification keys are unavailable (${response.status}).`
9810
10809
  );
9811
10810
  }
9812
- const value = await response.json();
10811
+ const value = await readBoundedResponseJson(response);
9813
10812
  cachedValueProofKeys = {
9814
10813
  value,
9815
10814
  expiresAt: now + valueProofKeyCacheTtlMs(response.headers)
@@ -9832,7 +10831,34 @@ function apiTimeoutMs() {
9832
10831
  process.env.REMEMBRANCE_API_TIMEOUT_MS ?? "",
9833
10832
  10
9834
10833
  );
9835
- 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
+ }
9836
10862
  }
9837
10863
  function endpointFor(definition, payload) {
9838
10864
  if (!definition.endpoint) {
@@ -9920,7 +10946,7 @@ async function ensureEconomicsSessionToken() {
9920
10946
  const signature = signPayload(
9921
10947
  null,
9922
10948
  Buffer.from(challenge.signing_payload),
9923
- createPrivateKey(identity.private_key)
10949
+ createPrivateKey2(identity.private_key)
9924
10950
  ).toString("base64url");
9925
10951
  const exchangeResponse = await directEconomicsSessionRequest({
9926
10952
  action: "exchange",
@@ -9961,21 +10987,14 @@ function mustFindTool(name) {
9961
10987
  }
9962
10988
  return tool2;
9963
10989
  }
9964
- function isRecord2(value) {
10990
+ function isRecord3(value) {
9965
10991
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
9966
10992
  }
9967
10993
  function identityPath(explicit) {
9968
- if (explicit) {
9969
- return explicit;
9970
- }
9971
- if (process.env.REMEMBRANCE_AGENT_KEY_PATH) {
9972
- return process.env.REMEMBRANCE_AGENT_KEY_PATH;
9973
- }
9974
- return join3(
9975
- process.env.XDG_CONFIG_HOME ?? join3(homedir3(), ".config"),
9976
- "remembrance",
9977
- "agent-key.json"
9978
- );
10994
+ return explicit?.trim() || localAgentIdentityPath();
10995
+ }
10996
+ function isFileAlreadyExistsError(error) {
10997
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
9979
10998
  }
9980
10999
  function agentProviderForIdentity(provider) {
9981
11000
  if (provider === "codex" || provider === "cursor") {
@@ -10003,13 +11022,26 @@ ${body}`;
10003
11022
  return `${body}
10004
11023
  `;
10005
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
+ }
10006
11035
  export {
10007
11036
  callTool,
10008
11037
  clientHealthReportFromConnectionStatus,
10009
11038
  dispatchJsonRpcRequest,
11039
+ formatConnectionDoctorReport,
10010
11040
  formatJsonRpcResponse,
10011
11041
  readJsonRpcMessages,
10012
11042
  remoteConfigurationError,
10013
11043
  reportDegradedClientHealth,
10014
- resetValueProofKeyCacheForTests
11044
+ resetValueProofKeyCacheForTests,
11045
+ runConnectionDoctorCli,
11046
+ runLocalConnectionDoctor
10015
11047
  };