@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.
- package/README.md +43 -8
- package/opencode.json +4 -9
- package/package.json +1 -1
- package/scripts/hook-core.mjs +1839 -175
- package/servers/remembrance-mcp.mjs +1431 -274
- package/skills/remembrancer/SKILL.md +64 -39
- package/skills/remembrancer/references/attestation-rest.md +43 -5
- package/skills/remembrancer/references/identity-preferences-topology.md +221 -0
- package/skills/remembrancer/references/remembrance-payloads.md +19 -12
- package/skills/remembrancer/references/remembrance-setup.md +42 -11
- package/src/index.mjs +94 -5
|
@@ -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
|
|
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.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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))
|
|
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);
|