Files
openclaw/extensions/onepassword/index.ts
Peter Steinberger 6bb85f177f feat(onepassword): optional 1Password secrets broker plugin (#106133)
* feat(onepassword): add optional 1Password secrets broker plugin

Curated slug registry with per-item auto/approve/deny policy, plugin-approval
gating with expiring allow-always grants, SQLite audit history, onepassword
status/audit CLI, and a single-attempt op client (--cache=false, minimal env).

Closes #105924

* docs(plugins): refresh generated inventory count after rebase

* fix(onepassword): scope grants and field reads

* fix(onepassword): bound grant retention

* fix(onepassword): satisfy deadcode ratchet and hook allowlist contract

* fix(onepassword): honor live policy reloads

* refactor(onepassword): trim private exports

* test(onepassword): satisfy plugin boundaries

* test(onepassword): document temp directory boundary
2026-07-13 03:12:47 -07:00

120 lines
4.1 KiB
TypeScript

import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
normalizePluginsConfig,
resolveEffectiveEnableState,
resolveLivePluginConfigObject,
} from "openclaw/plugin-sdk/plugin-config-runtime";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import type { AuditRow, StandingGrant } from "./src/broker.js";
import { OnePasswordBroker } from "./src/broker.js";
import { MAX_REGISTERED_ITEMS, parseOnePasswordConfig } from "./src/config.js";
import { OpClient } from "./src/op-client.js";
import { createOnePasswordTool, redactPersistedOnePasswordResult } from "./src/tool.js";
const MAX_AUDIT_ROWS = 40_000;
const MAX_STANDING_GRANTS = MAX_REGISTERED_ITEMS * 32;
export default definePluginEntry({
id: "onepassword",
name: "1Password",
description: "Curated 1Password secrets broker with approval policy and SQLite audit history.",
register(api) {
const startupConfig = parseOnePasswordConfig(api.pluginConfig);
const resolveCurrentConfig = () => {
const liveConfig = api.runtime.config?.current
? (api.runtime.config.current() as OpenClawConfig)
: undefined;
if (!liveConfig) {
return startupConfig;
}
const livePluginConfig = resolveLivePluginConfigObject(
() => liveConfig,
"onepassword",
api.pluginConfig as Record<string, unknown> | undefined,
);
const enabled = resolveEffectiveEnableState({
id: "onepassword",
origin: "bundled",
config: normalizePluginsConfig(liveConfig.plugins),
rootConfig: liveConfig,
enabledByDefault: livePluginConfig !== undefined,
}).enabled;
return enabled ? parseOnePasswordConfig(livePluginConfig) : undefined;
};
const grants = api.runtime.state.openKeyedStore<StandingGrant>({
namespace: "grants",
// Evicting the oldest grant is fail-closed: that agent must approve again.
// Keep enough room for 32 agents holding every registered slug.
maxEntries: MAX_STANDING_GRANTS,
overflowPolicy: "evict-oldest",
});
const audit = api.runtime.state.openKeyedStore<AuditRow>({
namespace: "audit",
maxEntries: MAX_AUDIT_ROWS,
overflowPolicy: "evict-oldest",
});
const tokenFile = path.join(
api.runtime.state.resolveStateDir(process.env),
"credentials",
"onepassword",
"service-account-token",
);
let cachedOpClient: { key: string; client: OpClient } | undefined;
const resolveCurrentOpClient = () => {
const config = resolveCurrentConfig();
const key = JSON.stringify([config?.opBin ?? null, config?.opTimeoutMs ?? 15_000]);
if (cachedOpClient?.key === key) {
return cachedOpClient.client;
}
const client = new OpClient({
opBin: config?.opBin,
tokenFile,
timeoutMs: config?.opTimeoutMs ?? 15_000,
warn: (message) => api.logger.warn(message),
});
cachedOpClient = { key, client };
return client;
};
const broker = startupConfig
? new OnePasswordBroker({
resolveConfig: resolveCurrentConfig,
opClient: {
getItem: (params) => resolveCurrentOpClient().getItem(params),
},
stores: { audit, grants },
})
: undefined;
api.registerCli(
async ({ program }) => {
const { registerOnePasswordCommands } = await import("./src/cli.js");
registerOnePasswordCommands({
program,
resolveConfig: resolveCurrentConfig,
resolveOpClient: resolveCurrentOpClient,
auditStore: audit,
});
},
{
descriptors: [
{
name: "onepassword",
description: "Inspect the 1Password secrets broker",
hasSubcommands: true,
},
],
},
);
if (!broker) {
return;
}
api.registerTool((context) => createOnePasswordTool(broker, context), {
name: "onepassword",
});
api.on("before_tool_call", (event, ctx) => broker.beforeToolCall(event, ctx));
api.on("tool_result_persist", redactPersistedOnePasswordResult);
},
});