@remembrance-ai/pi-plugin 0.0.0-stage → 0.2.25

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.
@@ -0,0 +1,200 @@
1
+ import { PRIVATE_EGRESS_TOOL_LANES } from "./private-egress-lanes.mjs";
2
+
3
+ const PRIVATE_LESSON_LANE = "private_lesson";
4
+ const PRIVATE_SKILL_LANE = "private_skill";
5
+ const PRIVATE_LESSON_SUBMIT_TOOL = "submit_private_lesson_candidate";
6
+ const PER_OPERATION_ADMIN_LANE = "admin_control";
7
+ const PRIVATE_EGRESS_TOOLS = new Set(Object.keys(PRIVATE_EGRESS_TOOL_LANES));
8
+ const PRIVATE_EGRESS_LANES = new Set(Object.values(PRIVATE_EGRESS_TOOL_LANES));
9
+
10
+ export function handlePrivateLessonSubmitApproval(event, options = {}) {
11
+ const toolName = String(
12
+ event?.toolName ?? event?.tool_name ?? "",
13
+ ).toLowerCase();
14
+ const exactTool = [...PRIVATE_EGRESS_TOOLS].find((candidate) =>
15
+ toolName.endsWith(candidate),
16
+ );
17
+ const lane = exactTool ? PRIVATE_EGRESS_TOOL_LANES[exactTool] : undefined;
18
+ if (
19
+ lane === "conditional_feedback" &&
20
+ !feedbackContainsReusableContent(event)
21
+ ) {
22
+ return undefined;
23
+ }
24
+ const approvedLanes = options.approvedLanes ?? options.approvedTools;
25
+ if (!exactTool || !lane || (lane !== PER_OPERATION_ADMIN_LANE && approvedLanes?.has?.(lane))) {
26
+ return undefined;
27
+ }
28
+ const approval = approvalCopy(lane);
29
+ return {
30
+ requireApproval: {
31
+ title: approval.title,
32
+ description: approval.description,
33
+ severity: "warning",
34
+ allowedDecisions: lane === PER_OPERATION_ADMIN_LANE
35
+ ? ["allow-once", "deny"]
36
+ : ["allow-once", "allow-always", "deny"],
37
+ timeoutMs: 120_000,
38
+ onResolution(decision) {
39
+ if (decision === "allow-always" && lane !== PER_OPERATION_ADMIN_LANE) options.onAllowAlways?.(lane);
40
+ },
41
+ },
42
+ };
43
+ }
44
+
45
+ function feedbackContainsReusableContent(event) {
46
+ const payload =
47
+ event?.arguments ??
48
+ event?.input ??
49
+ event?.params?.arguments ??
50
+ event?.params;
51
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
52
+ return false;
53
+ }
54
+ const lesson = payload.lesson;
55
+ return typeof lesson === "string" && lesson.trim().length > 0;
56
+ }
57
+
58
+ function approvalCopy(lane) {
59
+ if (lane === PER_OPERATION_ADMIN_LANE) {
60
+ return {
61
+ title: "Approve Remembrance administrator operation",
62
+ description:
63
+ "Review this exact admin_write operation and its complete arguments. Approval authorizes this invocation only; it does not approve future administrator writes or change the request_id used to identify the same logical attempt. Saved private-learning approvals do not authorize administrator operations.",
64
+ };
65
+ }
66
+ if (lane === "admin_review_repair") {
67
+ return {
68
+ title: "Create protected Remembrance review replacement",
69
+ description:
70
+ "Organization admin:review authority required. Send one complete new protected skill replacement to this organization's normal verification and review queue. The original draft is retained unchanged. This does not approve the replacement or close the original; any original disposition requires a separate normal explicit decision after proving an accepted, tenant-readable active replacement. Private-learning approval does not authorize this administrator action.",
71
+ };
72
+ }
73
+ if (lane === PRIVATE_SKILL_LANE) {
74
+ return {
75
+ title: "Submit private Remembrance skill",
76
+ description:
77
+ "Send one encrypted complete-skill draft to this organization's private verification and review queue. The submission action cannot publish public content.",
78
+ };
79
+ }
80
+ if (lane === "remembrance_submission") {
81
+ return {
82
+ title: "Submit broad Remembrance content",
83
+ description:
84
+ "Send reusable content through a legacy broad contribution action. Review the complete payload and destination before allowing it.",
85
+ };
86
+ }
87
+ if (lane === "private_skill_proposal") {
88
+ return {
89
+ title: "Propose private Remembrance skill",
90
+ description:
91
+ "Send a complete skill only to this organization's private review queue. This approval does not authorize public-capable candidate actions.",
92
+ };
93
+ }
94
+ if (lane === "skill_candidate") {
95
+ return {
96
+ title: "Propose Remembrance skill candidate",
97
+ description:
98
+ "Send a complete skill candidate whose destination depends on the active credential. Review the payload and resulting visibility before allowing it.",
99
+ };
100
+ }
101
+ if (lane === "skill_suggestion") {
102
+ return {
103
+ title: "Suggest a Remembrance skill change",
104
+ description:
105
+ "Send one proposed change to an existing skill. This approval covers suggestions only.",
106
+ };
107
+ }
108
+ if (lane === "resource_submission") {
109
+ return {
110
+ title: "Submit a Remembrance resource",
111
+ description:
112
+ "Send one reusable resource candidate. This approval does not authorize skill or review submissions.",
113
+ };
114
+ }
115
+ if (lane === "resource_review") {
116
+ return {
117
+ title: "Submit a Remembrance resource review",
118
+ description:
119
+ "Send one resource review with bounded redacted evidence. This approval covers reviews only.",
120
+ };
121
+ }
122
+ if (lane === "conditional_feedback") {
123
+ return {
124
+ title: "Submit reusable feedback content",
125
+ description:
126
+ "Send the reusable lesson attached to this legacy feedback action. Categorical query-fit and post-use feedback do not require this approval.",
127
+ };
128
+ }
129
+ return {
130
+ title: "Submit private Remembrance lesson",
131
+ description:
132
+ "Send one already-redacted canonical lesson to this organization's private verification queue. The submission action cannot publish public content.",
133
+ };
134
+ }
135
+
136
+ export function privateLessonSubmitApproved(api) {
137
+ return privateLearningSubmitApprovals(api).has(PRIVATE_LESSON_LANE);
138
+ }
139
+
140
+ export function privateLearningSubmitApprovals(api) {
141
+ const config = asRecord(api?.pluginConfig);
142
+ const configuredLanes = asRecord(config.privateLearningEgressApprovals);
143
+ const configuredTools = asRecord(config.privateLearningSubmitApprovals);
144
+ const approved = new Set(
145
+ [...PRIVATE_EGRESS_LANES].filter((lane) => lane !== PER_OPERATION_ADMIN_LANE && configuredLanes[lane] === true),
146
+ );
147
+ for (const [toolName, lane] of Object.entries(PRIVATE_EGRESS_TOOL_LANES)) {
148
+ if (lane !== PER_OPERATION_ADMIN_LANE && configuredTools[toolName] === true) approved.add(lane);
149
+ }
150
+ if (config.privateLessonSubmitApproval === true) {
151
+ approved.add(PRIVATE_LESSON_LANE);
152
+ }
153
+ return approved;
154
+ }
155
+
156
+ export async function persistPrivateLessonSubmitApproval(api) {
157
+ return persistPrivateLearningSubmitApproval(api, PRIVATE_LESSON_SUBMIT_TOOL);
158
+ }
159
+
160
+ export async function persistPrivateLearningSubmitApproval(api, toolName) {
161
+ const lane = PRIVATE_EGRESS_LANES.has(toolName)
162
+ ? toolName
163
+ : PRIVATE_EGRESS_TOOL_LANES[toolName];
164
+ if (!lane) {
165
+ throw new Error("Unsupported private Remembrance submission action.");
166
+ }
167
+ if (lane === PER_OPERATION_ADMIN_LANE) {
168
+ throw new Error("Administrator operations require per-operation approval and cannot persist allow-always consent.");
169
+ }
170
+ const mutateConfigFile = api?.runtime?.config?.mutateConfigFile;
171
+ if (typeof mutateConfigFile !== "function") {
172
+ throw new Error("OpenClaw config mutation is unavailable.");
173
+ }
174
+ await mutateConfigFile({
175
+ afterWrite: { mode: "auto" },
176
+ mutate: (draft) => {
177
+ const plugins = asRecord(draft.plugins);
178
+ const entries = asRecord(plugins.entries);
179
+ const entry = asRecord(entries.remembrance);
180
+ entries.remembrance = {
181
+ ...entry,
182
+ config: {
183
+ ...asRecord(entry.config),
184
+ privateLearningEgressApprovals: {
185
+ ...asRecord(asRecord(entry.config).privateLearningEgressApprovals),
186
+ [lane]: true,
187
+ },
188
+ },
189
+ };
190
+ plugins.entries = entries;
191
+ draft.plugins = plugins;
192
+ },
193
+ });
194
+ }
195
+
196
+ function asRecord(value) {
197
+ return value && typeof value === "object" && !Array.isArray(value)
198
+ ? value
199
+ : {};
200
+ }