@remembrance-ai/opencode-plugin 0.1.52 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -263,11 +263,13 @@ privately, then let an organization admin use the reviewed public-propagation
263
263
  flow for a redacted, public-safe version when appropriate.
264
264
 
265
265
  If host policy blocks repository-derived content before the request reaches
266
- Remembrance, do not retry it through another network transport. Use local MCP
266
+ Remembrance, do not retry it through another network transport. Report the fixed
267
+ content-free denial alert and do not create a handoff automatically. Only when
268
+ an organization admin explicitly requests one, use local MCP
267
269
  `queue_private_skill_import` or the bundled
268
- `scripts/queue-private-skill-import.mjs` helper, then have an organization admin
269
- upload the mode-0600 JSON at **Dashboard > Skills > Import**. A local handoff is
270
- not a server submission receipt.
270
+ `scripts/queue-private-skill-import.mjs` helper, then have the admin upload the
271
+ mode-0600 JSON at **Dashboard > Skills > Import**. A local handoff is not a
272
+ server submission receipt.
271
273
 
272
274
  ## New skill idea
273
275
 
@@ -121,15 +121,18 @@ CODEX_CLI="${CODEX_CLI:-$(command -v codex || true)}"
121
121
  This command handles both first install and update. If zsh says
122
122
  "codex: command not found", it discovers the current ChatGPT desktop bundle or
123
123
  the legacy Codex app bundle without requiring a shell alias. The final command
124
- opens Codex CLI so its secure hook review can be completed immediately.
124
+ opens Codex CLI so its secure hook review can be completed immediately when
125
+ Codex requires it.
125
126
 
126
127
  Codex will not execute plugin hooks until their exact definitions are trusted.
127
- In the Codex window opened by the installer, choose **Review hooks** on the
128
- automatic **Hooks need review** screen and trust only the Remembrance
128
+ In the Codex window opened by the installer, if Codex shows a **Hooks need
129
+ review** screen, choose **Review hooks** and trust only the Remembrance
129
130
  `SessionStart`, `UserPromptSubmit`, `PostToolUse`, and `Stop` hooks.
130
- Changed hook definitions show the same review screen again; never use the
131
- automation-only trust bypass for normal installation. Exit that window, then
132
- fully restart Codex.
131
+ If no review screen appears, continue: Codex may be reusing an existing valid
132
+ trust decision. Changed hook definitions show the same review screen again;
133
+ never use the automation-only trust bypass for normal installation. Fully
134
+ restart Codex, submit one prompt, use one Remembrance tool, complete one turn,
135
+ and run `run_connection_doctor`.
133
136
 
134
137
  OpenClaw:
135
138
 
@@ -187,10 +190,11 @@ Integrations & MCP**, and verify query, invocation, feedback, and contribution
187
190
  receipts in both local and cloud runs.
188
191
 
189
192
  After installing any native plugin, restart the agent app/session and approve
190
- the runtime's trust request. For Codex, complete the automatic hook review
191
- above; changed hook definitions show the same review screen again. A currently
192
- running Codex or Claude thread usually cannot hot-load newly installed plugin
193
- tools.
193
+ the runtime's trust request when one appears. For Codex, complete the hook
194
+ review above if Codex requests it; unchanged, previously trusted definitions
195
+ may not show another review screen. Changed hook definitions show the review
196
+ screen again. A currently running Codex or Claude thread usually cannot
197
+ hot-load newly installed plugin tools.
194
198
 
195
199
  ## Enterprise/org key setup
196
200
 
@@ -347,7 +351,8 @@ The MCP annotation for `propose_private_skill` is non-read-only and
347
351
  closed-world: it makes a network request, but can change only the authenticated
348
352
  organization's private review queue and cannot change publicly visible internet
349
353
  state. Closed-world does not mean zero-network. Only
350
- `queue_private_skill_import` is the local, zero-network fallback.
354
+ `queue_private_skill_import` is a local, zero-network handoff tool, and it
355
+ should run only when an organization admin explicitly requests that handoff.
351
356
 
352
357
  Example text to merge into the tenant-specific guardian policy:
353
358
 
@@ -382,12 +387,14 @@ over a user's local `[auto_review].policy`. See the official Codex
382
387
  [managed configuration](https://learn.chatgpt.com/docs/enterprise/managed-configuration#configure-automatic-review-policy)
383
388
  documentation for the current schema and deployment options.
384
389
 
385
- If the organization does not approve direct egress, keep the host denial. Use
386
- `queue_private_skill_import` locally, or run the bundled
387
- `scripts/queue-private-skill-import.mjs` helper, then have an organization
388
- admin upload the mode-0600 JSON at **Dashboard > Skills > Import**. The handoff
389
- never contains an API key or organization id, never contacts Remembrance, and
390
- does not count as submitted until the dashboard returns an import batch receipt.
390
+ If the organization does not approve direct egress, keep the host denial. The
391
+ plugin reports one content-free local alert, does not retry, and does not create
392
+ a handoff automatically. Only when an organization admin explicitly requests a
393
+ handoff, use `queue_private_skill_import` locally or run the bundled
394
+ `scripts/queue-private-skill-import.mjs` helper, then have the admin upload the
395
+ mode-0600 JSON at **Dashboard > Skills > Import**. The handoff never contains an
396
+ API key or organization id, never contacts Remembrance, and does not count as
397
+ submitted until the dashboard returns an import batch receipt.
391
398
 
392
399
  If Codex still sees `<your org key>` after restart, remove stale
393
400
  `REMEMBRANCE_API_KEY` exports from shell profiles such as `~/.zshrc` and
@@ -776,6 +783,17 @@ npx skills add dreamarkinc/remembrance-skills --skill remembrancer
776
783
  The entry skill is REST-only and self-contained. The same skill directory can
777
784
  be copied to ".agents/skills/remembrancer/SKILL.md" for compatible providers.
778
785
 
786
+ ## Verified client updates
787
+
788
+ Native plugins and local MCP check the credential-free public release manifest
789
+ at startup and through `run_connection_doctor`. The check sends no API key,
790
+ fails open when the registry is unavailable, and never runs a command returned
791
+ by the network. When a newer verified release exists, the agent must ask before
792
+ running the update command bundled with its installed client, then tell the user
793
+ which Codex, Claude Code, Cursor, OpenClaw, VS Code, OpenCode, or MCP host must
794
+ be reloaded, reopened, or fully restarted. Set
795
+ `REMEMBRANCE_CLIENT_UPDATE_CHECK=0` to disable only this advisory check.
796
+
779
797
  ## Validate after setup
780
798
 
781
799
  1. Start a fresh agent session.
@@ -830,10 +848,12 @@ be copied to ".agents/skills/remembrancer/SKILL.md" for compatible providers.
830
848
  that should not appear anonymously.
831
849
  13. With an organization submission key, submit one disposable redacted skill
832
850
  through propose_private_skill and verify it appears only in that
833
- organization's review queue. If host policy denies the export, run
834
- queue_private_skill_import instead, upload its JSON in the dashboard, and
835
- verify the agent reports a local queue receipt separately from the server
836
- import receipt.
851
+ organization's review queue. In a separate negative scenario, simulate a
852
+ host-policy denial and verify the plugin reports the fixed content-free alert
853
+ once, persists no blocked content, performs no retry, and creates no handoff.
854
+ Test queue_private_skill_import only as a separate administrator-requested
855
+ manual handoff, with its local receipt distinguished from a server import
856
+ receipt.
837
857
  14. If using local MCP, verify local_signing_identity in get_connection_status.
838
858
  A missing opaque identity initializes automatically on the first signed
839
859
  contribution; bootstrap_agent_identity with no arguments is an optional
@@ -843,10 +863,13 @@ be copied to ".agents/skills/remembrancer/SKILL.md" for compatible providers.
843
863
 
844
864
  - "Plugin installed, but no tools": restart the agent app/session; confirm the
845
865
  plugin is enabled and contains the runtime-specific manifest. For Codex,
846
- launch the installer-provided command, choose **Review hooks** on the
847
- automatic review screen, and trust only the listed Remembrance hooks. Exit
848
- that window and fully restart Codex; updates that change hooks show the same
849
- review screen again.
866
+ launch the installer-provided command. If Codex shows a hook review, choose
867
+ **Review hooks** and trust only the listed Remembrance hooks; if it does not,
868
+ continue because Codex may be reusing an existing valid trust decision.
869
+ Fully restart Codex, submit one prompt, use one Remembrance tool, complete one
870
+ turn, and run `run_connection_doctor`. If the lifecycle remains incomplete,
871
+ update or reinstall the plugin and repeat the check. Updates that change
872
+ hooks show the review screen again.
850
873
  - "Agent has tools but does not use them": first verify a concrete query receipt,
851
874
  then test a short contextual follow-up such as "fix these issues". Native
852
875
  prompt hooks should inject a full-conversation query reminder, and completion
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,
@@ -182,6 +186,12 @@ export function toolNameFromEvent(input, output) {
182
186
  input?.toolName,
183
187
  output?.tool,
184
188
  output?.toolName,
189
+ input?.properties?.tool,
190
+ input?.properties?.toolName,
191
+ input?.properties?.permission,
192
+ output?.properties?.tool,
193
+ output?.properties?.toolName,
194
+ output?.properties?.permission,
185
195
  ]) {
186
196
  if (typeof candidate === "string" && candidate.trim()) {
187
197
  return candidate;
@@ -221,11 +231,36 @@ export const Remembrance = async (context = {}) => {
221
231
  const queriedMessages = new Set();
222
232
  const messageSessions = new Map();
223
233
  const pendingGuidance = new Map();
234
+ const pendingClientUpdates = new Map();
224
235
  const userMessages = new Map();
225
236
  const pendingTextParts = new Map();
226
237
 
238
+ async function observeHostPolicyDenial({
239
+ eventType,
240
+ sessionId,
241
+ toolName,
242
+ value,
243
+ }) {
244
+ const observation = recordHostPolicyDenial(
245
+ {
246
+ surface: SURFACE,
247
+ sessionId,
248
+ eventType,
249
+ toolName,
250
+ value,
251
+ pluginVersion: version,
252
+ },
253
+ env,
254
+ );
255
+ if (!observation) return false;
256
+ await notify(client, HOST_POLICY_ALERT_TEXT, "error");
257
+ markHostPolicyAlertDelivered(SURFACE, sessionId, observation.id, env);
258
+ return true;
259
+ }
260
+
227
261
  function clearSessionState(sessionId) {
228
262
  pendingGuidance.delete(sessionId);
263
+ pendingClientUpdates.delete(sessionId);
229
264
  for (const [messageId, trackedSessionId] of messageSessions) {
230
265
  if (trackedSessionId !== sessionId) continue;
231
266
  messageSessions.delete(messageId);
@@ -265,10 +300,24 @@ export const Remembrance = async (context = {}) => {
265
300
  try {
266
301
  const sessionId = sessionIdFromEvent(event, event);
267
302
  recordLifecycle("session_start", sessionId);
303
+ const clientUpdate = await (
304
+ context.clientUpdateCheck ?? checkForClientUpdate
305
+ )(
306
+ {
307
+ surface: SURFACE,
308
+ currentVersion: version,
309
+ },
310
+ env,
311
+ ).catch(() => null);
312
+ if (clientUpdate?.notice) {
313
+ pendingClientUpdates.set(sessionId, clientUpdate.notice);
314
+ }
268
315
  await notify(
269
316
  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.",
317
+ clientUpdate?.latest_version
318
+ ? `Remembrance ${version} is active; ${clientUpdate.latest_version} is available. The agent will ask before updating, and the host must be restarted afterward.`
319
+ : `Remembrance ${version} is active. Relevant memory is added before eligible turns; ` +
320
+ "run_connection_doctor verifies the active connection and gives one exact next step without exposing the key.",
272
321
  );
273
322
  } catch (error) {
274
323
  debugLog(env, "opencode_session_created_failed", {
@@ -334,7 +383,13 @@ export const Remembrance = async (context = {}) => {
334
383
  try {
335
384
  const sessionId = sessionIdFromEvent(input, output);
336
385
  const guidance = pendingGuidance.get(sessionId);
337
- if (!guidance || !Array.isArray(output?.system)) return;
386
+ const updateNotice = pendingClientUpdates.get(sessionId);
387
+ if (!Array.isArray(output?.system)) return;
388
+ if (updateNotice) {
389
+ output.system.push(updateNotice);
390
+ pendingClientUpdates.delete(sessionId);
391
+ }
392
+ if (!guidance) return;
338
393
  output.system.push(guidance.text);
339
394
  pendingGuidance.delete(sessionId);
340
395
  // Delivery happened: only now is the guidance actually in the model's
@@ -453,6 +508,22 @@ export const Remembrance = async (context = {}) => {
453
508
  await handleSessionIdle(event);
454
509
  return;
455
510
  }
511
+ if (
512
+ event.type === "permission.replied" ||
513
+ event.type === "session.error"
514
+ ) {
515
+ const serialized = JSON.stringify(event);
516
+ const inferredTool = /remembrance/i.test(serialized)
517
+ ? toolNameFromEvent(event, event) || "remembrance"
518
+ : toolNameFromEvent(event, event);
519
+ await observeHostPolicyDenial({
520
+ eventType: event.type,
521
+ sessionId: sessionIdFromEvent(event, event),
522
+ toolName: inferredTool,
523
+ value: event,
524
+ });
525
+ return;
526
+ }
456
527
  if (event.type === "session.deleted") {
457
528
  clearSessionState(sessionIdFromEvent(event, event));
458
529
  }
@@ -468,11 +539,19 @@ export const Remembrance = async (context = {}) => {
468
539
  "tool.execute.after": async (input, output) => {
469
540
  try {
470
541
  const sessionId = sessionIdFromEvent(input, output);
471
- recordLifecycle("tool_observer", sessionId);
472
542
  const tool = toolNameFromEvent(input, output);
473
543
  if (!tool) return;
474
544
  const normalizedTool = tool.toLowerCase();
475
- if (toolResponseIndicatesFailure(output)) return;
545
+ if (toolResponseIndicatesFailure(output)) {
546
+ await observeHostPolicyDenial({
547
+ eventType: "tool.execute.after",
548
+ sessionId,
549
+ toolName: tool,
550
+ value: output,
551
+ });
552
+ return;
553
+ }
554
+ recordLifecycle("tool_observer", sessionId);
476
555
  if (normalizedTool.endsWith("query_skills")) {
477
556
  if (queryResponseHasMatches(output)) {
478
557
  recordRegistryUse(sessionId, env);