@openviking/openclaw-plugin 2026.7.11 → 2026.8.6

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.
Files changed (48) hide show
  1. package/INSTALL-ZH.md +39 -0
  2. package/INSTALL.md +39 -0
  3. package/README.md +5 -0
  4. package/auto-recall.ts +5 -4
  5. package/config.ts +145 -6
  6. package/context-engine.ts +0 -1
  7. package/dist/auto-recall.js +4 -4
  8. package/dist/config.js +101 -4
  9. package/dist/context-engine.js +0 -1
  10. package/dist/index.js +13 -4
  11. package/dist/plugin/openviking-archive-tools.js +1 -2
  12. package/dist/plugin/openviking-command-definitions.js +3 -3
  13. package/dist/plugin/openviking-experience-tools.js +105 -0
  14. package/dist/plugin/openviking-import-tools.js +2 -2
  15. package/dist/plugin/openviking-memory-recall-tools.js +4 -4
  16. package/dist/plugin/openviking-memory-tools.js +11 -6
  17. package/dist/plugin/openviking-query-runtime.js +1 -1
  18. package/dist/plugin/openviking-query-tools.js +4 -4
  19. package/dist/plugin/openviking-recall-trace-runtime.js +3 -3
  20. package/dist/plugin/openviking-session-routing-runtime.js +9 -2
  21. package/dist/plugin/openviking-tool-result-tools.js +3 -3
  22. package/dist/routing/identity-routing.js +27 -0
  23. package/dist/routing/memory-uri.js +1 -0
  24. package/dist/services/context-lifecycle-service.js +21 -16
  25. package/dist/services/context-message-adapter.js +2 -9
  26. package/index.ts +13 -3
  27. package/install-manifest.json +1 -0
  28. package/openclaw.plugin.json +46 -3
  29. package/package.json +1 -1
  30. package/plugin/command-registration.ts +2 -0
  31. package/plugin/openviking-archive-tools.ts +1 -4
  32. package/plugin/openviking-command-definitions.ts +4 -3
  33. package/plugin/openviking-experience-tools.ts +160 -0
  34. package/plugin/openviking-import-tools.ts +4 -2
  35. package/plugin/openviking-memory-recall-tools.ts +6 -4
  36. package/plugin/openviking-memory-tools.ts +18 -10
  37. package/plugin/openviking-query-runtime.ts +1 -1
  38. package/plugin/openviking-query-tools.ts +6 -4
  39. package/plugin/openviking-recall-trace-runtime.ts +4 -3
  40. package/plugin/openviking-recall-trace-tools.ts +2 -0
  41. package/plugin/openviking-session-routing-runtime.ts +15 -1
  42. package/plugin/openviking-tool-result-tools.ts +0 -6
  43. package/registries/openviking-tools.ts +6 -0
  44. package/routing/identity-routing.ts +41 -0
  45. package/routing/memory-uri.ts +1 -0
  46. package/services/context-lifecycle-service.ts +27 -18
  47. package/services/context-message-adapter.ts +2 -9
  48. package/skills/ov-experience-memory/SKILL.md +156 -0
package/INSTALL-ZH.md CHANGED
@@ -203,6 +203,45 @@ openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --forc
203
203
  openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role assistant --peer-prefix <PREFIX> --json
204
204
  ```
205
205
 
206
+ #### 无法执行 CLI 时直接配置文件
207
+
208
+ 如果容器内无法执行 `openclaw` CLI,可以把以下字段合并到 OpenClaw 实际读取的配置文件。设置了 `OPENCLAW_CONFIG_PATH` 时使用该路径;否则通常是 `$OPENCLAW_STATE_DIR/openclaw.json`(默认 `~/.openclaw/openclaw.json`)。
209
+
210
+ ```json
211
+ {
212
+ "plugins": {
213
+ "entries": {
214
+ "openviking": {
215
+ "enabled": true,
216
+ "config": {
217
+ "mode": "remote",
218
+ "baseUrl": "http://openviking:1933",
219
+ "apiKey": "<API_KEY>",
220
+ "peer_role": "assistant"
221
+ }
222
+ }
223
+ },
224
+ "slots": {
225
+ "contextEngine": "openviking"
226
+ }
227
+ }
228
+ }
229
+ ```
230
+
231
+ - 插件必须已经安装;编辑前请备份配置,并把以上字段合并到现有 `plugins` 配置。
232
+ - 如果配置中已有 `plugins.allow`,把 `openviking` 追加进去;如果没有,不要仅为本插件新建 allowlist。
233
+ - `contextEngine` 是独占 slot;若已有其他 context engine,只有确认替换后再修改该字段。使用 root API key 时,还需在 `config` 中设置 `accountId` 和 `userId`。
234
+ - 容器连接其他服务时,`baseUrl` 应使用容器内可访问的服务地址,而不是 `127.0.0.1`。
235
+ - 推荐使用 `SecretRef` 对象作为 `apiKey`(而不是明文字符串),避免密钥直接落盘写入 `openclaw.json`。支持的形式与 OpenClaw 核心中 LLM/TTS/MCP 等 provider 配置使用的标准 `SecretRef` 一致:
236
+
237
+ | 类型 | 示例 | 说明 |
238
+ | --- | --- | --- |
239
+ | `env` | `{ "source": "env", "id": "OPENVIKING_API_KEY" }` | 启动时读取同名环境变量。 |
240
+ | `file` | `{ "source": "file", "id": "/etc/secrets/openviking.key" }` | 以 UTF-8 读取并去除首尾空白;`~` 可展开,适配 Kubernetes `secretKeyRef` 卷挂载、0600 权限文件。 |
241
+ | `exec` | `{ "source": "exec", "provider": "op", "id": "op://vault/openviking/credential" }` | 运行 `<provider> <id>`,去除 stdout 首尾空白(1Password `op`、Vault、gopass 等均符合此形态)。 |
242
+
243
+ 仍可使用纯字符串形式(含 `${ENV_VAR}` 插值)作为向后兼容路径;此时请限制文件权限或通过受控 Secret 卷提供,并在修改后重启 Gateway、容器或 Pod。
244
+
206
245
  ### 3. 重启 OpenClaw Gateway
207
246
 
208
247
  ```bash
package/INSTALL.md CHANGED
@@ -147,6 +147,45 @@ If you want assistant messages to carry a prefixed `peer_id` and data-plane reca
147
147
  openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role assistant --peer-prefix <PREFIX> --json
148
148
  ```
149
149
 
150
+ #### Configure The File Directly When The CLI Is Unavailable
151
+
152
+ If the `openclaw` CLI cannot run inside the container, merge the following fields into the config file that OpenClaw actually reads. Use `OPENCLAW_CONFIG_PATH` when it is set; otherwise the file is usually `$OPENCLAW_STATE_DIR/openclaw.json` (default: `~/.openclaw/openclaw.json`).
153
+
154
+ ```json
155
+ {
156
+ "plugins": {
157
+ "entries": {
158
+ "openviking": {
159
+ "enabled": true,
160
+ "config": {
161
+ "mode": "remote",
162
+ "baseUrl": "http://openviking:1933",
163
+ "apiKey": "<API_KEY>",
164
+ "peer_role": "assistant"
165
+ }
166
+ }
167
+ },
168
+ "slots": {
169
+ "contextEngine": "openviking"
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ - The plugin must already be installed. Back up the file and merge these fields into the existing `plugins` config.
176
+ - If `plugins.allow` already exists, append `openviking`; otherwise, do not create an allowlist only for this plugin.
177
+ - `contextEngine` is an exclusive slot. If another context engine is configured, change it only after confirming the replacement. Root API keys also require `accountId` and `userId` in `config`.
178
+ - When connecting to another service from a container, use a `baseUrl` that is reachable from the container rather than `127.0.0.1`.
179
+ - Prefer a `SecretRef` for `apiKey` instead of a plaintext string so the key is never stored inside `openclaw.json`. Supported shapes match OpenClaw's standard `SecretRef` used by LLM/TTS/MCP provider configs:
180
+
181
+ | Shape | Example | Notes |
182
+ | --- | --- | --- |
183
+ | `env` | `{ "source": "env", "id": "OPENVIKING_API_KEY" }` | Reads the named env var at plugin load time. |
184
+ | `file` | `{ "source": "file", "id": "/etc/secrets/openviking.key" }` | Reads UTF-8, trims whitespace. `~` is expanded; ideal for Kubernetes `secretKeyRef` volumes and 0600-managed files. |
185
+ | `exec` | `{ "source": "exec", "provider": "op", "id": "op://vault/openviking/credential" }` | Runs `<provider> <id>` and trims stdout (1Password `op`, Vault, gopass, etc. all match this shape). |
186
+
187
+ A plain `string` (including `${ENV_VAR}` interpolation) is still accepted, but only as a backward-compatibility path; in that case, restrict file permissions or provide the file through a managed Secret volume, then restart the Gateway, container, or Pod.
188
+
150
189
  ### 3. Restart OpenClaw Gateway
151
190
 
152
191
  ```bash
package/README.md CHANGED
@@ -46,6 +46,8 @@ Once installed, the plugin provides these agent tools:
46
46
  | `ov_read` | Read the full original content of one exact OpenViking URI |
47
47
  | `ov_multi_read` | Read the full original content of multiple OpenViking URIs |
48
48
  | `ov_list` | List OpenViking directories after search to inspect sibling chunks and overview files |
49
+ | `search_experience` | Search reusable execution experiences for the current OpenViking user |
50
+ | `read_experience` | Read one exact Experience returned by `search_experience` |
49
51
  | `openviking_tool_result_read` | Restore the full original content of an externalized tool result |
50
52
  | `openviking_tool_result_search` | Search inside an externalized tool result by keyword |
51
53
  | `openviking_tool_result_list` | List externalized tool results in the current session |
@@ -248,6 +250,8 @@ Beyond automatic behavior, the plugin exposes these tools directly:
248
250
  - `ov_read`: read one exact `viking://` URI returned by `ov_search` or `ov_list`
249
251
  - `ov_multi_read`: read multiple exact `viking://` URIs, useful for an overview plus sibling chunks
250
252
  - `ov_list`: list a hit's parent directory after `ov_search` to recover sibling chunks, `.overview.md`, and related split-document context
253
+ - `search_experience`: search reusable execution experiences for the current OpenViking user
254
+ - `read_experience`: read one exact Experience returned by `search_experience`
251
255
 
252
256
  They serve different roles:
253
257
 
@@ -261,6 +265,7 @@ They serve different roles:
261
265
  - `ov_read` turns a ranked hit into original evidence before answering precise documentation, codebase, configuration, or procedural questions
262
266
  - `ov_multi_read` reads overview and sibling chunks together when a split document needs more context than a single hit
263
267
  - `ov_list` complements `ov_search` when a ranked hit is only one chunk of a larger procedure or document
268
+ - `search_experience` and `read_experience` provide the exact tool-call contract used by the bundled Experience Memory skill and usage reporting
264
269
 
265
270
  `ov_archive_expand` is especially important because `assemble()` normally returns archive summaries and indexes, not the full raw transcript.
266
271
 
package/auto-recall.ts CHANGED
@@ -313,6 +313,7 @@ export async function buildAutoRecallContext(params: {
313
313
  queryConfig?: EffectiveQueryConfig;
314
314
  client: OpenVikingClient;
315
315
  agentId: string;
316
+ actorPeerId?: string;
316
317
  queryText: string;
317
318
  logger: Logger;
318
319
  verbose?: (message: string) => void;
@@ -324,14 +325,14 @@ export async function buildAutoRecallContext(params: {
324
325
  queryTruncated?: boolean;
325
326
  resourceTypes?: RecallResourceType[];
326
327
  }): Promise<{ block?: string; memoryCount: number; estimatedTokens: number }> {
327
- const { cfg, client, agentId, queryText, logger, verbose } = params;
328
+ const { cfg, client, agentId, actorPeerId, queryText, logger, verbose } = params;
328
329
  const queryConfig = params.queryConfig;
329
330
 
330
331
  if (!cfg.autoRecall || queryText.length < 5) {
331
332
  return { memoryCount: 0, estimatedTokens: 0 };
332
333
  }
333
334
 
334
- const precheck = await quickRecallPrecheck(client, agentId);
335
+ const precheck = await quickRecallPrecheck(client, actorPeerId);
335
336
  if (!precheck.ok) {
336
337
  verbose?.(`openviking: skipping auto-recall because precheck failed (${precheck.reason})`);
337
338
  return { memoryCount: 0, estimatedTokens: 0 };
@@ -369,7 +370,7 @@ export async function buildAutoRecallContext(params: {
369
370
  limit: candidateLimit,
370
371
  scoreThreshold: 0,
371
372
  contextType: search.contextType,
372
- actorPeerId: agentId,
373
+ actorPeerId,
373
374
  });
374
375
  return {
375
376
  resourceType: search.resourceType,
@@ -484,7 +485,7 @@ export async function buildAutoRecallContext(params: {
484
485
 
485
486
  const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(
486
487
  memories,
487
- (uri) => client.read(uri, agentId),
488
+ (uri) => client.read(uri, actorPeerId),
488
489
  {
489
490
  recallPreferAbstract,
490
491
  recallMaxInjectedChars: maxInjectedChars,
package/config.ts CHANGED
@@ -1,13 +1,38 @@
1
1
  import { homedir } from "node:os";
2
+ import { readFileSync } from "node:fs";
2
3
 
3
4
  import { getEnv } from "./runtime-utils.js";
4
5
 
6
+ /**
7
+ * Subset of OpenClaw's standard `SecretRef` shape supported by the OpenViking
8
+ * plugin. The full runtime SDK also recognises `source: "exec"` with a
9
+ * provider plugin, but the plugin self-hosts its own env/file resolution so
10
+ * users on pluginSdkVersion < 2026.6.x (no host-level secret-resolver) still
11
+ * get vault-free secret management.
12
+ *
13
+ * source: "env" -> `process.env[id]` (same as the bare `${ENV}` string
14
+ * interpolation, but declarative and visible to UI hints).
15
+ * source: "file" -> contents of `id` path (``~`` expansion supported),
16
+ * leading/trailing whitespace trimmed — convenient for
17
+ * Kubernetes `secretKeyRef` mount volumes and
18
+ * file-permission-only deployments.
19
+ * source: "exec" -> delegate to host `openclaw` secret resolver when the
20
+ * runtime exposes it; today the plugin performs a
21
+ * best-effort `provider + id` CLI exec call so that
22
+ * 1Password / Vault / gopass users can share OpenViking
23
+ * credentials with their other providers.
24
+ */
25
+ export type OpenVikingSecretRef =
26
+ | { source: "env"; id: string }
27
+ | { source: "file"; id: string }
28
+ | { source: "exec"; provider: string; id: string };
29
+
5
30
  export type MemoryOpenVikingConfig = {
6
31
  mode?: "remote";
7
32
  baseUrl?: string;
8
33
  peer_role?: "none" | "assistant" | "person";
9
34
  peer_prefix?: string;
10
- apiKey?: string;
35
+ apiKey?: string | OpenVikingSecretRef;
11
36
  /** Optional HTTP headers merged into every OpenViking request. */
12
37
  headers?: Record<string, string>;
13
38
  /** Advanced option. Only needed when explicitly sending tenant identity headers. With a user key the server derives identity from the key. */
@@ -102,8 +127,10 @@ export type MemoryOpenVikingConfig = {
102
127
 
103
128
  /** Runtime config after memoryOpenVikingConfigSchema.parse() has applied defaults. */
104
129
  export type ParsedMemoryOpenVikingConfig = Required<
105
- Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes">
130
+ Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes" | "apiKey">
106
131
  > & {
132
+ /** parse() resolves SecretRef values, so the runtime shape is always a plain string. */
133
+ apiKey: string;
107
134
  agentExperience: Required<NonNullable<MemoryOpenVikingConfig["agentExperience"]>>;
108
135
  recallTargetTypes: Array<"resource" | "user" | "agent">;
109
136
  };
@@ -143,6 +170,8 @@ export const OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES = [
143
170
  "ov_read",
144
171
  "ov_multi_read",
145
172
  "ov_list",
173
+ "search_experience",
174
+ "read_experience",
146
175
  "memory_recall",
147
176
  "ov_recall_trace",
148
177
  "memory_store",
@@ -163,6 +192,7 @@ export const OPENVIKING_TOOL_GROUPS: Record<string, readonly OpenVikingToolName[
163
192
  default: OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES,
164
193
  memory: ["memory_recall", "memory_store", "memory_forget"],
165
194
  resource_query: ["ov_search", "ov_read", "ov_multi_read", "ov_list"],
195
+ experience: ["search_experience", "read_experience"],
166
196
  import: ["add_resource", "add_skill"],
167
197
  recall_trace: ["ov_recall_trace"],
168
198
  archive: ["ov_archive_search", "ov_archive_expand"],
@@ -188,6 +218,105 @@ function resolvePeerPrefix(configured: unknown): string {
188
218
  return DEFAULT_PEER_PREFIX;
189
219
  }
190
220
 
221
+ /**
222
+ * Resolve an {@link OpenVikingSecretRef} to the actual secret string. Plain
223
+ * strings pass through untouched so callers can compose this with the legacy
224
+ * `resolveEnvVars` pipeline without branching.
225
+ *
226
+ * Intentional behaviours:
227
+ * * `env`: uses `getEnv` (process.env but exposed for vitest overrides). An
228
+ * unset / empty env var is an error — the user asked for a named secret
229
+ * so silent empty-string fallback would mask misconfigurations.
230
+ * * `file`: ~ expanded, then read as UTF-8, then leading/trailing whitespace
231
+ * stripped (de-facto standard for files written with `echo xxx > key`).
232
+ * Missing / unreadable files propagate the original node error with an
233
+ * error prefix that names the OpenViking field, so the user knows which
234
+ * secretRef failed to load.
235
+ * * `exec`: shells out `<provider> <id>`. Providers commonly expose this
236
+ * shape (1Password CLI, gopass, HashiCorp Vault), which matches how
237
+ * OpenClaw's own `@transmitt0r/openclaw-plugin-onepassword` provider
238
+ * resolves `{source:"exec", provider:"onepassword", id:"..."}`. Exec is
239
+ * best-effort; failures surface with an error prefix so users can
240
+ * distinguish `provider not installed` from `provider configured but id
241
+ * missing`.
242
+ */
243
+ function resolveSecret(
244
+ value: string | OpenVikingSecretRef | undefined | null,
245
+ label: string,
246
+ ): string {
247
+ if (value === undefined || value === null) return "";
248
+ if (typeof value === "string") return value;
249
+ if (!value || typeof value !== "object") {
250
+ throw new Error(
251
+ `OpenViking ${label} must be a plain string or a SecretRef object ` +
252
+ `({source:"env"|"file"|"exec", id})`,
253
+ );
254
+ }
255
+ const obj = value as Record<string, unknown>;
256
+ if (typeof obj.id !== "string" || !obj.id) {
257
+ throw new Error(`OpenViking ${label} SecretRef requires a non-empty string "id"`);
258
+ }
259
+ const id = obj.id;
260
+ switch (obj.source) {
261
+ case "env": {
262
+ const envValue = getEnv(id);
263
+ if (!envValue) {
264
+ throw new Error(
265
+ `OpenViking ${label} SecretRef env source: environment variable ${id} is not set or empty`,
266
+ );
267
+ }
268
+ return envValue;
269
+ }
270
+ case "file": {
271
+ const resolvedPath = expandHomeDir(id);
272
+ try {
273
+ const contents = readFileSync(resolvedPath, "utf8");
274
+ return contents.trim();
275
+ } catch (err) {
276
+ const msg = err instanceof Error ? err.message : String(err);
277
+ throw new Error(
278
+ `OpenViking ${label} SecretRef file source: cannot read ${resolvedPath} (${msg})`,
279
+ );
280
+ }
281
+ }
282
+ case "exec": {
283
+ const provider = typeof (obj as Record<string, unknown>).provider === "string"
284
+ ? (obj as Record<string, unknown>).provider as string
285
+ : "";
286
+ if (!provider) {
287
+ throw new Error(
288
+ `OpenViking ${label} SecretRef exec source requires a "provider" field naming the secret-manager CLI on PATH`,
289
+ );
290
+ }
291
+ try {
292
+ // Avoid top-level `import "node:child_process"` — this branch is rare
293
+ // and keeping the require lazy makes startup marginally faster for
294
+ // the 95% of users who pick env/file.
295
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
296
+ const { execFileSync } = require("node:child_process") as typeof import("node:child_process");
297
+ const out = execFileSync(provider, [id], {
298
+ encoding: "utf8",
299
+ stdio: ["ignore", "pipe", "pipe"],
300
+ timeout: 15_000,
301
+ env: process.env,
302
+ }) as string;
303
+ return out.trim();
304
+ } catch (err) {
305
+ const msg = err instanceof Error ? err.message : String(err);
306
+ throw new Error(
307
+ `OpenViking ${label} SecretRef exec source: provider "${provider}" failed for id "${id}" (${msg})`,
308
+ );
309
+ }
310
+ }
311
+ default:
312
+ throw new Error(
313
+ `OpenViking ${label} SecretRef has unknown source "${String(
314
+ (obj as Record<string, unknown>).source,
315
+ )}". Supported: "env" | "file" | "exec".`,
316
+ );
317
+ }
318
+ }
319
+
191
320
  function resolvePeerRole(configured: unknown) {
192
321
  if (typeof configured === "string") {
193
322
  const role = configured.trim().toLowerCase();
@@ -465,7 +594,14 @@ export const memoryOpenVikingConfigSchema = {
465
594
  const peerPrefix = resolvePeerPrefix(cfg.peer_prefix);
466
595
  const rawBaseUrl = typeof cfg.baseUrl === "string" ? cfg.baseUrl : resolveDefaultBaseUrl();
467
596
  const resolvedBaseUrl = resolveEnvVars(rawBaseUrl).replace(/\/+$/, "");
468
- const rawApiKey = typeof cfg.apiKey === "string" ? cfg.apiKey : getEnv("OPENVIKING_API_KEY");
597
+ // Support plain string, SecretRef object, and OPENVIKING_API_KEY fallback.
598
+ // A user writing a SecretRef has explicitly opted out of the bare env
599
+ // interpolation path, so the fallback kicks in only when nothing is
600
+ // configured at all (matches existing behaviour).
601
+ const rawApiKey: string | OpenVikingSecretRef | undefined =
602
+ cfg.apiKey !== undefined && cfg.apiKey !== null
603
+ ? (cfg.apiKey as string | OpenVikingSecretRef)
604
+ : getEnv("OPENVIKING_API_KEY") || undefined;
469
605
  const captureMode = cfg.captureMode;
470
606
  if (
471
607
  typeof captureMode !== "undefined" &&
@@ -508,7 +644,7 @@ export const memoryOpenVikingConfigSchema = {
508
644
  baseUrl: resolvedBaseUrl,
509
645
  peer_role: peerRole,
510
646
  peer_prefix: peerPrefix,
511
- apiKey: rawApiKey ? resolveEnvVars(rawApiKey) : "",
647
+ apiKey: rawApiKey ? resolveEnvVars(resolveSecret(rawApiKey, "config.apiKey")) : "",
512
648
  headers: toStringRecord(cfg.headers, "openviking config headers"),
513
649
  accountId,
514
650
  userId,
@@ -678,7 +814,10 @@ export const memoryOpenVikingConfigSchema = {
678
814
  label: "OpenViking API Key",
679
815
  sensitive: true,
680
816
  placeholder: "${OPENVIKING_API_KEY}",
681
- help: "Optional API key for OpenViking server",
817
+ help:
818
+ "Optional API key for OpenViking server. Accepts a plain string, " +
819
+ "${ENV_VAR} interpolation, or a SecretRef object ({source: env/file/exec, id}). " +
820
+ "Prefer the SecretRef shapes so the key never sits as plaintext in openclaw.json.",
682
821
  },
683
822
  headers: {
684
823
  label: "Headers",
@@ -836,7 +975,7 @@ export const memoryOpenVikingConfigSchema = {
836
975
  enabledTools: {
837
976
  label: "Enabled Tools",
838
977
  placeholder: "default",
839
- help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
978
+ help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, experience, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
840
979
  advanced: true,
841
980
  },
842
981
  disabledTools: {
package/context-engine.ts CHANGED
@@ -283,7 +283,6 @@ export function createMemoryOpenVikingContextEngine(params: {
283
283
  getClient,
284
284
  logger,
285
285
  rememberSessionAgentId,
286
- resolveAgentId,
287
286
  isBypassedSession,
288
287
  });
289
288
  }
@@ -200,12 +200,12 @@ export function shouldRecallAgentExperience(input) {
200
200
  return { recall: false, score, reason: score < 0 ? "non_execution" : "below_threshold" };
201
201
  }
202
202
  export async function buildAutoRecallContext(params) {
203
- const { cfg, client, agentId, queryText, logger, verbose } = params;
203
+ const { cfg, client, agentId, actorPeerId, queryText, logger, verbose } = params;
204
204
  const queryConfig = params.queryConfig;
205
205
  if (!cfg.autoRecall || queryText.length < 5) {
206
206
  return { memoryCount: 0, estimatedTokens: 0 };
207
207
  }
208
- const precheck = await quickRecallPrecheck(client, agentId);
208
+ const precheck = await quickRecallPrecheck(client, actorPeerId);
209
209
  if (!precheck.ok) {
210
210
  verbose?.(`openviking: skipping auto-recall because precheck failed (${precheck.reason})`);
211
211
  return { memoryCount: 0, estimatedTokens: 0 };
@@ -236,7 +236,7 @@ export async function buildAutoRecallContext(params) {
236
236
  limit: candidateLimit,
237
237
  scoreThreshold: 0,
238
238
  contextType: search.contextType,
239
- actorPeerId: agentId,
239
+ actorPeerId,
240
240
  });
241
241
  return {
242
242
  resourceType: search.resourceType,
@@ -343,7 +343,7 @@ export async function buildAutoRecallContext(params) {
343
343
  await recordTrace([], 0, 0);
344
344
  return { memoryCount: 0, estimatedTokens: 0 };
345
345
  }
346
- const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(memories, (uri) => client.read(uri, agentId), {
346
+ const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(memories, (uri) => client.read(uri, actorPeerId), {
347
347
  recallPreferAbstract,
348
348
  recallMaxInjectedChars: maxInjectedChars,
349
349
  includeUri: true,
package/dist/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { homedir } from "node:os";
2
+ import { readFileSync } from "node:fs";
2
3
  import { getEnv } from "./runtime-utils.js";
3
4
  const DEFAULT_BASE_URL = "http://127.0.0.1:1933";
4
5
  const DEFAULT_TARGET_URI = "viking://user/memories";
@@ -34,6 +35,8 @@ export const OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES = [
34
35
  "ov_read",
35
36
  "ov_multi_read",
36
37
  "ov_list",
38
+ "search_experience",
39
+ "read_experience",
37
40
  "memory_recall",
38
41
  "ov_recall_trace",
39
42
  "memory_store",
@@ -53,6 +56,7 @@ export const OPENVIKING_TOOL_GROUPS = {
53
56
  default: OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES,
54
57
  memory: ["memory_recall", "memory_store", "memory_forget"],
55
58
  resource_query: ["ov_search", "ov_read", "ov_multi_read", "ov_list"],
59
+ experience: ["search_experience", "read_experience"],
56
60
  import: ["add_resource", "add_skill"],
57
61
  recall_trace: ["ov_recall_trace"],
58
62
  archive: ["ov_archive_search", "ov_archive_expand"],
@@ -76,6 +80,91 @@ function resolvePeerPrefix(configured) {
76
80
  }
77
81
  return DEFAULT_PEER_PREFIX;
78
82
  }
83
+ /**
84
+ * Resolve an {@link OpenVikingSecretRef} to the actual secret string. Plain
85
+ * strings pass through untouched so callers can compose this with the legacy
86
+ * `resolveEnvVars` pipeline without branching.
87
+ *
88
+ * Intentional behaviours:
89
+ * * `env`: uses `getEnv` (process.env but exposed for vitest overrides). An
90
+ * unset / empty env var is an error — the user asked for a named secret
91
+ * so silent empty-string fallback would mask misconfigurations.
92
+ * * `file`: ~ expanded, then read as UTF-8, then leading/trailing whitespace
93
+ * stripped (de-facto standard for files written with `echo xxx > key`).
94
+ * Missing / unreadable files propagate the original node error with an
95
+ * error prefix that names the OpenViking field, so the user knows which
96
+ * secretRef failed to load.
97
+ * * `exec`: shells out `<provider> <id>`. Providers commonly expose this
98
+ * shape (1Password CLI, gopass, HashiCorp Vault), which matches how
99
+ * OpenClaw's own `@transmitt0r/openclaw-plugin-onepassword` provider
100
+ * resolves `{source:"exec", provider:"onepassword", id:"..."}`. Exec is
101
+ * best-effort; failures surface with an error prefix so users can
102
+ * distinguish `provider not installed` from `provider configured but id
103
+ * missing`.
104
+ */
105
+ function resolveSecret(value, label) {
106
+ if (value === undefined || value === null)
107
+ return "";
108
+ if (typeof value === "string")
109
+ return value;
110
+ if (!value || typeof value !== "object") {
111
+ throw new Error(`OpenViking ${label} must be a plain string or a SecretRef object ` +
112
+ `({source:"env"|"file"|"exec", id})`);
113
+ }
114
+ const obj = value;
115
+ if (typeof obj.id !== "string" || !obj.id) {
116
+ throw new Error(`OpenViking ${label} SecretRef requires a non-empty string "id"`);
117
+ }
118
+ const id = obj.id;
119
+ switch (obj.source) {
120
+ case "env": {
121
+ const envValue = getEnv(id);
122
+ if (!envValue) {
123
+ throw new Error(`OpenViking ${label} SecretRef env source: environment variable ${id} is not set or empty`);
124
+ }
125
+ return envValue;
126
+ }
127
+ case "file": {
128
+ const resolvedPath = expandHomeDir(id);
129
+ try {
130
+ const contents = readFileSync(resolvedPath, "utf8");
131
+ return contents.trim();
132
+ }
133
+ catch (err) {
134
+ const msg = err instanceof Error ? err.message : String(err);
135
+ throw new Error(`OpenViking ${label} SecretRef file source: cannot read ${resolvedPath} (${msg})`);
136
+ }
137
+ }
138
+ case "exec": {
139
+ const provider = typeof obj.provider === "string"
140
+ ? obj.provider
141
+ : "";
142
+ if (!provider) {
143
+ throw new Error(`OpenViking ${label} SecretRef exec source requires a "provider" field naming the secret-manager CLI on PATH`);
144
+ }
145
+ try {
146
+ // Avoid top-level `import "node:child_process"` — this branch is rare
147
+ // and keeping the require lazy makes startup marginally faster for
148
+ // the 95% of users who pick env/file.
149
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
150
+ const { execFileSync } = require("node:child_process");
151
+ const out = execFileSync(provider, [id], {
152
+ encoding: "utf8",
153
+ stdio: ["ignore", "pipe", "pipe"],
154
+ timeout: 15_000,
155
+ env: process.env,
156
+ });
157
+ return out.trim();
158
+ }
159
+ catch (err) {
160
+ const msg = err instanceof Error ? err.message : String(err);
161
+ throw new Error(`OpenViking ${label} SecretRef exec source: provider "${provider}" failed for id "${id}" (${msg})`);
162
+ }
163
+ }
164
+ default:
165
+ throw new Error(`OpenViking ${label} SecretRef has unknown source "${String(obj.source)}". Supported: "env" | "file" | "exec".`);
166
+ }
167
+ }
79
168
  function resolvePeerRole(configured) {
80
169
  if (typeof configured === "string") {
81
170
  const role = configured.trim().toLowerCase();
@@ -320,7 +409,13 @@ export const memoryOpenVikingConfigSchema = {
320
409
  const peerPrefix = resolvePeerPrefix(cfg.peer_prefix);
321
410
  const rawBaseUrl = typeof cfg.baseUrl === "string" ? cfg.baseUrl : resolveDefaultBaseUrl();
322
411
  const resolvedBaseUrl = resolveEnvVars(rawBaseUrl).replace(/\/+$/, "");
323
- const rawApiKey = typeof cfg.apiKey === "string" ? cfg.apiKey : getEnv("OPENVIKING_API_KEY");
412
+ // Support plain string, SecretRef object, and OPENVIKING_API_KEY fallback.
413
+ // A user writing a SecretRef has explicitly opted out of the bare env
414
+ // interpolation path, so the fallback kicks in only when nothing is
415
+ // configured at all (matches existing behaviour).
416
+ const rawApiKey = cfg.apiKey !== undefined && cfg.apiKey !== null
417
+ ? cfg.apiKey
418
+ : getEnv("OPENVIKING_API_KEY") || undefined;
324
419
  const captureMode = cfg.captureMode;
325
420
  if (typeof captureMode !== "undefined" &&
326
421
  captureMode !== "semantic" &&
@@ -342,7 +437,7 @@ export const memoryOpenVikingConfigSchema = {
342
437
  baseUrl: resolvedBaseUrl,
343
438
  peer_role: peerRole,
344
439
  peer_prefix: peerPrefix,
345
- apiKey: rawApiKey ? resolveEnvVars(rawApiKey) : "",
440
+ apiKey: rawApiKey ? resolveEnvVars(resolveSecret(rawApiKey, "config.apiKey")) : "",
346
441
  headers: toStringRecord(cfg.headers, "openviking config headers"),
347
442
  accountId,
348
443
  userId,
@@ -423,7 +518,9 @@ export const memoryOpenVikingConfigSchema = {
423
518
  label: "OpenViking API Key",
424
519
  sensitive: true,
425
520
  placeholder: "${OPENVIKING_API_KEY}",
426
- help: "Optional API key for OpenViking server",
521
+ help: "Optional API key for OpenViking server. Accepts a plain string, " +
522
+ "${ENV_VAR} interpolation, or a SecretRef object ({source: env/file/exec, id}). " +
523
+ "Prefer the SecretRef shapes so the key never sits as plaintext in openclaw.json.",
427
524
  },
428
525
  headers: {
429
526
  label: "Headers",
@@ -579,7 +676,7 @@ export const memoryOpenVikingConfigSchema = {
579
676
  enabledTools: {
580
677
  label: "Enabled Tools",
581
678
  placeholder: "default",
582
- help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
679
+ help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, experience, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
583
680
  advanced: true,
584
681
  },
585
682
  disabledTools: {
@@ -135,7 +135,6 @@ export function createMemoryOpenVikingContextEngine(params) {
135
135
  getClient,
136
136
  logger,
137
137
  rememberSessionAgentId,
138
- resolveAgentId,
139
138
  isBypassedSession,
140
139
  });
141
140
  }
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { registerOpenVikingCommands } from "./plugin/command-registration.js";
15
15
  import { registerRecallTraceRoutes as registerRecallTraceRouteAdapters } from "./plugin/recall-trace-routes.js";
16
16
  import { createOpenVikingService } from "./plugin/openviking-services.js";
17
17
  import { registerOpenVikingArchiveTools } from "./plugin/openviking-archive-tools.js";
18
+ import { registerOpenVikingExperienceTools } from "./plugin/openviking-experience-tools.js";
18
19
  import { createOpenVikingImportRuntime } from "./plugin/openviking-import-runtime.js";
19
20
  import { registerOpenVikingImportTools } from "./plugin/openviking-import-tools.js";
20
21
  import { registerOpenVikingLifecycleHooks } from "./plugin/openviking-lifecycle-hooks.js";
@@ -25,8 +26,8 @@ import { registerOpenVikingRecallTraceTools } from "./plugin/openviking-recall-t
25
26
  import { createOpenVikingRuntimeState } from "./plugin/openviking-runtime-state.js";
26
27
  import { createOpenVikingSessionRoutingRuntime } from "./plugin/openviking-session-routing-runtime.js";
27
28
  import { registerOpenVikingToolResultTools } from "./plugin/openviking-tool-result-tools.js";
28
- import { boundTraceQuery, createMemoryStoreTempSessionId, createTraceId, extractToolSenderId, inferRecallResourceType, makeBypassedToolResult, previewText, } from "./plugin/openviking-runtime-utils.js";
29
- import { formatMessageFaithful, toRoleId } from "./services/context-message-adapter.js";
29
+ import { boundTraceQuery, createMemoryStoreTempSessionId, createTraceId, inferRecallResourceType, makeBypassedToolResult, previewText, } from "./plugin/openviking-runtime-utils.js";
30
+ import { formatMessageFaithful } from "./services/context-message-adapter.js";
30
31
  import { clampScore, postProcessMemories, pickMemoriesForInjection, } from "./memory-ranking.js";
31
32
  import { createMemoryOpenVikingContextEngine, } from "./context-engine.js";
32
33
  import { openClawSessionRefToOvStorageId, openClawSessionToOvStorageId, } from "./routing/identity-routing.js";
@@ -80,6 +81,7 @@ const contextEnginePlugin = {
80
81
  logger: api.logger,
81
82
  });
82
83
  const { rememberSessionAgentId, resolveAgentId, resolvePluginSessionRouting, toQueryConfigContext, } = createOpenVikingSessionRoutingRuntime({
84
+ peerRole: cfg.peer_role,
83
85
  peerPrefix: cfg.peer_prefix,
84
86
  logFindRequests: cfg.logFindRequests,
85
87
  logger: api.logger,
@@ -135,6 +137,14 @@ const contextEnginePlugin = {
135
137
  isBypassedSession,
136
138
  makeBypassedToolResult,
137
139
  });
140
+ registerOpenVikingExperienceTools({
141
+ registerTool: registerOpenVikingTool,
142
+ getClient,
143
+ resolvePluginSessionRouting,
144
+ isBypassedSession,
145
+ makeBypassedToolResult,
146
+ userId: cfg.userId,
147
+ });
138
148
  registerOpenVikingMemoryRecallTools({
139
149
  registerTool: registerOpenVikingTool,
140
150
  getClient,
@@ -182,8 +192,7 @@ const contextEnginePlugin = {
182
192
  getClient,
183
193
  normalizeSessionId: openClawSessionRefToOvStorageId,
184
194
  createTempSessionId: createMemoryStoreTempSessionId,
185
- extractSenderId: extractToolSenderId,
186
- toRoleId,
195
+ peerRole: cfg.peer_role,
187
196
  resolvePluginSessionRouting,
188
197
  isBypassedSession,
189
198
  makeBypassedToolResult,
@@ -55,7 +55,6 @@ export function registerOpenVikingArchiveTools(deps) {
55
55
  const result = await client.grepSessionArchives(ovSessionId, escapedQuery, {
56
56
  archiveId,
57
57
  caseInsensitive: true,
58
- agentId,
59
58
  });
60
59
  const traceResults = (result.matches ?? []).slice(0, deps.traceRecallMaxResultsPerSearch).map((match) => ({
61
60
  uri: match.uri,
@@ -175,7 +174,7 @@ export function registerOpenVikingArchiveTools(deps) {
175
174
  }
176
175
  try {
177
176
  const client = await deps.getClient();
178
- const detail = await client.getSessionArchive(session.ovSessionId, archiveId, session.agentId);
177
+ const detail = await client.getSessionArchive(session.ovSessionId, archiveId);
179
178
  const header = [
180
179
  `## ${detail.archive_id}`,
181
180
  detail.abstract ? `**Summary**: ${detail.abstract}` : "",