@remembrance-ai/opencode-plugin 0.1.53 → 0.1.60

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.
@@ -329,6 +329,7 @@ enabled_tools = [
329
329
  "query_skills",
330
330
  "list_skills",
331
331
  "invoke_skill",
332
+ "get_effective_preferences",
332
333
  "get_skill",
333
334
  "get_resource",
334
335
  "get_value_proof",
@@ -338,6 +339,8 @@ enabled_tools = [
338
339
  "submit_remembrance",
339
340
  "propose_private_skill",
340
341
  "submit_suggestion",
342
+ "record_preference",
343
+ "link_current_installation",
341
344
  ]
342
345
  default_tools_approval_mode = "writes"
343
346
  ~~~
@@ -351,7 +354,8 @@ The MCP annotation for `propose_private_skill` is non-read-only and
351
354
  closed-world: it makes a network request, but can change only the authenticated
352
355
  organization's private review queue and cannot change publicly visible internet
353
356
  state. Closed-world does not mean zero-network. Only
354
- `queue_private_skill_import` is the local, zero-network fallback.
357
+ `queue_private_skill_import` is a local, zero-network handoff tool, and it
358
+ should run only when an organization admin explicitly requests that handoff.
355
359
 
356
360
  Example text to merge into the tenant-specific guardian policy:
357
361
 
@@ -386,12 +390,14 @@ over a user's local `[auto_review].policy`. See the official Codex
386
390
  [managed configuration](https://learn.chatgpt.com/docs/enterprise/managed-configuration#configure-automatic-review-policy)
387
391
  documentation for the current schema and deployment options.
388
392
 
389
- If the organization does not approve direct egress, keep the host denial. Use
390
- `queue_private_skill_import` locally, or run the bundled
391
- `scripts/queue-private-skill-import.mjs` helper, then have an organization
392
- admin upload the mode-0600 JSON at **Dashboard > Skills > Import**. The handoff
393
- never contains an API key or organization id, never contacts Remembrance, and
394
- does not count as submitted until the dashboard returns an import batch receipt.
393
+ If the organization does not approve direct egress, keep the host denial. The
394
+ plugin reports one content-free local alert, does not retry, and does not create
395
+ a handoff automatically. Only when an organization admin explicitly requests a
396
+ handoff, use `queue_private_skill_import` locally or run the bundled
397
+ `scripts/queue-private-skill-import.mjs` helper, then have the admin upload the
398
+ mode-0600 JSON at **Dashboard > Skills > Import**. The handoff never contains an
399
+ API key or organization id, never contacts Remembrance, and does not count as
400
+ submitted until the dashboard returns an import batch receipt.
395
401
 
396
402
  If Codex still sees `<your org key>` after restart, remove stale
397
403
  `REMEMBRANCE_API_KEY` exports from shell profiles such as `~/.zshrc` and
@@ -427,6 +433,7 @@ run_connection_doctor
427
433
  query_skills
428
434
  list_skills
429
435
  invoke_skill
436
+ get_effective_preferences
430
437
  get_skill
431
438
  get_resource
432
439
  get_value_proof
@@ -436,6 +443,8 @@ report_task_outcome
436
443
  submit_remembrance
437
444
  propose_private_skill
438
445
  submit_suggestion
446
+ record_preference
447
+ link_current_installation
439
448
  ~~~
440
449
 
441
450
  It intentionally omits `propose_skill_idea`, `submit_resource`,
@@ -493,6 +502,7 @@ Merge this into managed settings:
493
502
  "mcp__remembrance__query_skills",
494
503
  "mcp__remembrance__list_skills",
495
504
  "mcp__remembrance__invoke_skill",
505
+ "mcp__remembrance__get_effective_preferences",
496
506
  "mcp__remembrance__get_skill",
497
507
  "mcp__remembrance__get_resource",
498
508
  "mcp__remembrance__get_value_proof",
@@ -502,6 +512,8 @@ Merge this into managed settings:
502
512
  "mcp__remembrance__submit_remembrance",
503
513
  "mcp__remembrance__propose_private_skill",
504
514
  "mcp__remembrance__submit_suggestion",
515
+ "mcp__remembrance__record_preference",
516
+ "mcp__remembrance__link_current_installation",
505
517
  ]
506
518
  },
507
519
  "sandbox": {
@@ -548,6 +560,7 @@ narrow server/tool set:
548
560
  "query_skills",
549
561
  "list_skills",
550
562
  "invoke_skill",
563
+ "get_effective_preferences",
551
564
  "get_skill",
552
565
  "get_resource",
553
566
  "get_value_proof",
@@ -557,6 +570,8 @@ narrow server/tool set:
557
570
  "submit_remembrance",
558
571
  "propose_private_skill",
559
572
  "submit_suggestion",
573
+ "record_preference",
574
+ "link_current_installation",
560
575
  ],
561
576
  "trust": false
562
577
  }
@@ -601,6 +616,7 @@ tool profile hides MCP tools, and `tools.deny: ["bundle-mcp"]` disables them:
601
616
  "query_skills",
602
617
  "list_skills",
603
618
  "invoke_skill",
619
+ "get_effective_preferences",
604
620
  "get_skill",
605
621
  "get_resource",
606
622
  "get_value_proof",
@@ -610,6 +626,8 @@ tool profile hides MCP tools, and `tools.deny: ["bundle-mcp"]` disables them:
610
626
  "submit_remembrance",
611
627
  "propose_private_skill",
612
628
  "submit_suggestion",
629
+ "record_preference",
630
+ "link_current_installation",
613
631
  ]
614
632
  }
615
633
  }
@@ -780,6 +798,17 @@ npx skills add dreamarkinc/remembrance-skills --skill remembrancer
780
798
  The entry skill is REST-only and self-contained. The same skill directory can
781
799
  be copied to ".agents/skills/remembrancer/SKILL.md" for compatible providers.
782
800
 
801
+ ## Verified client updates
802
+
803
+ Native plugins and local MCP check the credential-free public release manifest
804
+ at startup and through `run_connection_doctor`. The check sends no API key,
805
+ fails open when the registry is unavailable, and never runs a command returned
806
+ by the network. When a newer verified release exists, the agent must ask before
807
+ running the update command bundled with its installed client, then tell the user
808
+ which Codex, Claude Code, Cursor, OpenClaw, VS Code, OpenCode, or MCP host must
809
+ be reloaded, reopened, or fully restarted. Set
810
+ `REMEMBRANCE_CLIENT_UPDATE_CHECK=0` to disable only this advisory check.
811
+
783
812
  ## Validate after setup
784
813
 
785
814
  1. Start a fresh agent session.
@@ -834,10 +863,12 @@ be copied to ".agents/skills/remembrancer/SKILL.md" for compatible providers.
834
863
  that should not appear anonymously.
835
864
  13. With an organization submission key, submit one disposable redacted skill
836
865
  through propose_private_skill and verify it appears only in that
837
- organization's review queue. If host policy denies the export, run
838
- queue_private_skill_import instead, upload its JSON in the dashboard, and
839
- verify the agent reports a local queue receipt separately from the server
840
- import receipt.
866
+ organization's review queue. In a separate negative scenario, simulate a
867
+ host-policy denial and verify the plugin reports the fixed content-free alert
868
+ once, persists no blocked content, performs no retry, and creates no handoff.
869
+ Test queue_private_skill_import only as a separate administrator-requested
870
+ manual handoff, with its local receipt distinguished from a server import
871
+ receipt.
841
872
  14. If using local MCP, verify local_signing_identity in get_connection_status.
842
873
  A missing opaque identity initializes automatically on the first signed
843
874
  contribution; bootstrap_agent_identity with no arguments is an optional
package/src/index.mjs CHANGED
@@ -37,6 +37,8 @@
37
37
  import { readFileSync } from "node:fs";
38
38
  import process from "node:process";
39
39
  import {
40
+ HOST_POLICY_ALERT_TEXT,
41
+ checkForClientUpdate,
40
42
  clearHighMatchSurfaceForExplicitSelection,
41
43
  clearHighMatchSurfaceIfOpened,
42
44
  debugLog,
@@ -44,11 +46,13 @@ import {
44
46
  directSelectionFromResponse,
45
47
  highMatchFromResponse,
46
48
  markCurrentEngagementHandled,
49
+ markHostPolicyAlertDelivered,
47
50
  recordDirectiveFollowThroughForTool,
48
51
  recordDirectSelectionSurface,
49
52
  recordDirectiveSurface,
50
53
  recordHighMatchSurface,
51
54
  recordPluginLifecycleHealth,
55
+ recordHostPolicyDenial,
52
56
  recordRegistryUse,
53
57
  recordTaskEligibility,
54
58
  recordValueEpisodeSurface,
@@ -61,6 +65,7 @@ import {
61
65
  toolResponseIndicatesFailure,
62
66
  valueEpisodeFromResponse,
63
67
  writePromptedCount,
68
+ warmPrincipalSession,
64
69
  } from "../scripts/hook-core.mjs";
65
70
 
66
71
  const SURFACE = "opencode";
@@ -182,6 +187,12 @@ export function toolNameFromEvent(input, output) {
182
187
  input?.toolName,
183
188
  output?.tool,
184
189
  output?.toolName,
190
+ input?.properties?.tool,
191
+ input?.properties?.toolName,
192
+ input?.properties?.permission,
193
+ output?.properties?.tool,
194
+ output?.properties?.toolName,
195
+ output?.properties?.permission,
185
196
  ]) {
186
197
  if (typeof candidate === "string" && candidate.trim()) {
187
198
  return candidate;
@@ -221,11 +232,36 @@ export const Remembrance = async (context = {}) => {
221
232
  const queriedMessages = new Set();
222
233
  const messageSessions = new Map();
223
234
  const pendingGuidance = new Map();
235
+ const pendingClientUpdates = new Map();
224
236
  const userMessages = new Map();
225
237
  const pendingTextParts = new Map();
226
238
 
239
+ async function observeHostPolicyDenial({
240
+ eventType,
241
+ sessionId,
242
+ toolName,
243
+ value,
244
+ }) {
245
+ const observation = recordHostPolicyDenial(
246
+ {
247
+ surface: SURFACE,
248
+ sessionId,
249
+ eventType,
250
+ toolName,
251
+ value,
252
+ pluginVersion: version,
253
+ },
254
+ env,
255
+ );
256
+ if (!observation) return false;
257
+ await notify(client, HOST_POLICY_ALERT_TEXT, "error");
258
+ markHostPolicyAlertDelivered(SURFACE, sessionId, observation.id, env);
259
+ return true;
260
+ }
261
+
227
262
  function clearSessionState(sessionId) {
228
263
  pendingGuidance.delete(sessionId);
264
+ pendingClientUpdates.delete(sessionId);
229
265
  for (const [messageId, trackedSessionId] of messageSessions) {
230
266
  if (trackedSessionId !== sessionId) continue;
231
267
  messageSessions.delete(messageId);
@@ -265,10 +301,33 @@ export const Remembrance = async (context = {}) => {
265
301
  try {
266
302
  const sessionId = sessionIdFromEvent(event, event);
267
303
  recordLifecycle("session_start", sessionId);
304
+ const [, clientUpdate] = await Promise.all([
305
+ (context.warmSession ?? warmPrincipalSession)(
306
+ {
307
+ runtime: SURFACE,
308
+ hostSurface: "cli",
309
+ clientVersion: version,
310
+ hostVersion: "",
311
+ },
312
+ env,
313
+ ).catch(() => null),
314
+ (context.clientUpdateCheck ?? checkForClientUpdate)(
315
+ {
316
+ surface: SURFACE,
317
+ currentVersion: version,
318
+ },
319
+ env,
320
+ ).catch(() => null),
321
+ ]);
322
+ if (clientUpdate?.notice) {
323
+ pendingClientUpdates.set(sessionId, clientUpdate.notice);
324
+ }
268
325
  await notify(
269
326
  client,
270
- `Remembrance ${version} is active. Relevant memory is added before eligible turns; ` +
271
- "run_connection_doctor verifies the active connection and gives one exact next step without exposing the key.",
327
+ clientUpdate?.latest_version
328
+ ? `Remembrance ${version} is active; ${clientUpdate.latest_version} is available. The agent will ask before updating, and the host must be restarted afterward.`
329
+ : `Remembrance ${version} is active. Relevant memory is added before eligible turns; ` +
330
+ "run_connection_doctor verifies the active connection and gives one exact next step without exposing the key.",
272
331
  );
273
332
  } catch (error) {
274
333
  debugLog(env, "opencode_session_created_failed", {
@@ -334,7 +393,13 @@ export const Remembrance = async (context = {}) => {
334
393
  try {
335
394
  const sessionId = sessionIdFromEvent(input, output);
336
395
  const guidance = pendingGuidance.get(sessionId);
337
- if (!guidance || !Array.isArray(output?.system)) return;
396
+ const updateNotice = pendingClientUpdates.get(sessionId);
397
+ if (!Array.isArray(output?.system)) return;
398
+ if (updateNotice) {
399
+ output.system.push(updateNotice);
400
+ pendingClientUpdates.delete(sessionId);
401
+ }
402
+ if (!guidance) return;
338
403
  output.system.push(guidance.text);
339
404
  pendingGuidance.delete(sessionId);
340
405
  // Delivery happened: only now is the guidance actually in the model's
@@ -453,6 +518,22 @@ export const Remembrance = async (context = {}) => {
453
518
  await handleSessionIdle(event);
454
519
  return;
455
520
  }
521
+ if (
522
+ event.type === "permission.replied" ||
523
+ event.type === "session.error"
524
+ ) {
525
+ const serialized = JSON.stringify(event);
526
+ const inferredTool = /remembrance/i.test(serialized)
527
+ ? toolNameFromEvent(event, event) || "remembrance"
528
+ : toolNameFromEvent(event, event);
529
+ await observeHostPolicyDenial({
530
+ eventType: event.type,
531
+ sessionId: sessionIdFromEvent(event, event),
532
+ toolName: inferredTool,
533
+ value: event,
534
+ });
535
+ return;
536
+ }
456
537
  if (event.type === "session.deleted") {
457
538
  clearSessionState(sessionIdFromEvent(event, event));
458
539
  }
@@ -468,11 +549,19 @@ export const Remembrance = async (context = {}) => {
468
549
  "tool.execute.after": async (input, output) => {
469
550
  try {
470
551
  const sessionId = sessionIdFromEvent(input, output);
471
- recordLifecycle("tool_observer", sessionId);
472
552
  const tool = toolNameFromEvent(input, output);
473
553
  if (!tool) return;
474
554
  const normalizedTool = tool.toLowerCase();
475
- if (toolResponseIndicatesFailure(output)) return;
555
+ if (toolResponseIndicatesFailure(output)) {
556
+ await observeHostPolicyDenial({
557
+ eventType: "tool.execute.after",
558
+ sessionId,
559
+ toolName: tool,
560
+ value: output,
561
+ });
562
+ return;
563
+ }
564
+ recordLifecycle("tool_observer", sessionId);
476
565
  if (normalizedTool.endsWith("query_skills")) {
477
566
  if (queryResponseHasMatches(output)) {
478
567
  recordRegistryUse(sessionId, env);