@seekrit/openclaw-plugin 0.1.0

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 ADDED
@@ -0,0 +1,80 @@
1
+ # @seekrit/openclaw-plugin
2
+
3
+ seekrit as an [OpenClaw](https://docs.openclaw.ai) **SecretRef provider**. Your
4
+ gateway config names credentials instead of holding them; the values are
5
+ decrypted on your machine, by your credential, when OpenClaw starts.
6
+
7
+ ```bash
8
+ openclaw plugins install npm:@seekrit/openclaw-plugin
9
+ openclaw plugins enable seekrit
10
+ seekrit openclaw init --write
11
+ ```
12
+
13
+ Then any credential OpenClaw accepts a SecretRef for becomes a reference:
14
+
15
+ ```json5
16
+ { source: "exec", provider: "seekrit", id: "OPENAI_API_KEY" }
17
+ ```
18
+
19
+ Full guide: **<https://seekrit.dev/docs/guides/ai-agents/openclaw>**
20
+
21
+ ## What this package is
22
+
23
+ A manifest and a resolver — no runtime code, no tools, no channels, and no
24
+ capability prompts at install time. OpenClaw reads
25
+ `secretProviderIntegrations` out of `openclaw.plugin.json` without executing
26
+ anything, and spawns `seekrit-secret-ref-resolver.js` when it needs values.
27
+
28
+ The resolver is a stdio bridge onto `seekrit openclaw resolve`, so the protocol
29
+ has exactly one implementation. Decryption happens in a short-lived child
30
+ process that exits when the batch is answered: the gateway never holds a token,
31
+ a data key, or a plaintext, and the seekrit API never sees one either.
32
+
33
+ ## Ids
34
+
35
+ Two shapes, mirroring `op://vault/item/field`:
36
+
37
+ | id | resolves against |
38
+ | --- | --- |
39
+ | `OPENAI_API_KEY` | the environment your credential already points at |
40
+ | `billing-api/production/STRIPE_SECRET_KEY` | that application and environment |
41
+
42
+ A bare name is the common case, and the only one a service token needs — the
43
+ token is already bound to an environment.
44
+
45
+ ## Credentials
46
+
47
+ The resolver runs with the environment allowlist the manifest declares, and
48
+ finds its credential there. Any of these work:
49
+
50
+ - `SEEKRIT_TOKEN` — a service token (`skt_`), scoped to one environment.
51
+ - `SEEKRIT_CLIENT_ID` + `SEEKRIT_CLIENT_SECRET` — machine credentials, from
52
+ which the CLI mints and caches an admin token.
53
+ - a `seekrit login` session in `$HOME`, for a workstation gateway.
54
+
55
+ `SEEKRIT_CLI` overrides which CLI the resolver runs, and must be an absolute
56
+ path.
57
+
58
+ ## Two things that will surprise you
59
+
60
+ **Resolution is eager.** OpenClaw reads every active ref once at startup and
61
+ config reload, into one in-memory snapshot — it does not re-resolve per request.
62
+ After you rotate a secret, run `openclaw secrets reload`, or the gateway keeps
63
+ serving the old value until it restarts.
64
+
65
+ **A `--link`ed install will not work.** OpenClaw only honours
66
+ `secretProviderIntegrations` from plugins whose origin is `bundled` or `global`,
67
+ so a locally linked development copy is installed but inert. Install from npm.
68
+
69
+ ## Not a process boundary
70
+
71
+ A SecretRef keeps plaintext out of `openclaw.json`, `models.json`, and the
72
+ gateway's SQLite. It does not stop the agent from reading a resolved value —
73
+ OpenClaw says as much about its own egress sentinels. If the point is that the
74
+ agent must *never* hold the key, put it behind the
75
+ [egress proxy](https://seekrit.dev/docs/guides/agent-proxy) and give the agent a
76
+ `{{seekrit:NAME}}` placeholder instead.
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,49 @@
1
+ {
2
+ "id": "seekrit",
3
+ "name": "seekrit",
4
+ "description": "Resolve OpenClaw exec SecretRefs from seekrit — end-to-end encrypted secrets, decrypted locally so the gateway config holds no plaintext and the seekrit API never sees one.",
5
+ "activation": {
6
+ "onStartup": false
7
+ },
8
+ "secretProviderIntegrations": {
9
+ "seekrit": {
10
+ "providerAlias": "seekrit",
11
+ "displayName": "seekrit",
12
+ "description": "Resolve OpenClaw exec SecretRefs from a seekrit environment using the seekrit CLI.",
13
+ "source": "exec",
14
+ "command": "${node}",
15
+ "args": ["./seekrit-secret-ref-resolver.js"],
16
+ "timeoutMs": 30000,
17
+ "noOutputTimeoutMs": 30000,
18
+ "maxOutputBytes": 1048576,
19
+ "passEnv": [
20
+ "PATH",
21
+ "HOME",
22
+ "USERPROFILE",
23
+ "APPDATA",
24
+ "LOCALAPPDATA",
25
+ "TEMP",
26
+ "TMP",
27
+ "SYSTEMROOT",
28
+ "WINDIR",
29
+ "XDG_CONFIG_HOME",
30
+ "XDG_CACHE_HOME",
31
+ "NODE_EXTRA_CA_CERTS",
32
+ "SEEKRIT_CLI",
33
+ "SEEKRIT_TOKEN",
34
+ "SEEKRIT_CLIENT_ID",
35
+ "SEEKRIT_CLIENT_SECRET",
36
+ "SEEKRIT_API_URL",
37
+ "SEEKRIT_ORG",
38
+ "SEEKRIT_APP",
39
+ "SEEKRIT_ENV",
40
+ "SEEKRIT_BRANCH"
41
+ ]
42
+ }
43
+ },
44
+ "configSchema": {
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "properties": {}
48
+ }
49
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@seekrit/openclaw-plugin",
3
+ "version": "0.1.0",
4
+ "description": "seekrit for OpenClaw — resolve exec SecretRefs from end-to-end encrypted secrets, so the gateway config holds no plaintext and the seekrit API never sees one.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "files": [
10
+ "openclaw.plugin.json",
11
+ "seekrit-secret-ref-resolver.js",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "keywords": [
18
+ "openclaw",
19
+ "openclaw-plugin",
20
+ "secretref",
21
+ "secrets",
22
+ "api-keys",
23
+ "encryption",
24
+ "zero-knowledge"
25
+ ],
26
+ "homepage": "https://seekrit.dev/docs/guides/ai-agents/openclaw",
27
+ "license": "MIT",
28
+ "scripts": {
29
+ "test": "vitest run",
30
+ "typecheck": "tsc --noEmit"
31
+ },
32
+ "dependencies": {
33
+ "@seekrit/cli": "workspace:^"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^26.1.0",
37
+ "vitest": "^4.1.9"
38
+ }
39
+ }
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenClaw exec SecretRef resolver for seekrit.
4
+ *
5
+ * OpenClaw runs this file — `${node} ./seekrit-secret-ref-resolver.js`, declared
6
+ * in `openclaw.plugin.json` — with the request on stdin and a narrow environment
7
+ * allowlist, and reads the response from stdout. All this file does is hand that
8
+ * request to `seekrit openclaw resolve` and pass the answer back.
9
+ *
10
+ * It is a bridge on purpose, for three reasons.
11
+ *
12
+ * **One implementation of the protocol.** `seekrit openclaw resolve` is also the
13
+ * command a plain CLI install points an exec provider straight at. If the
14
+ * protocol lived here as well, a fix would land in one of the two shapes only.
15
+ *
16
+ * **The gateway process never holds key material.** The decryption happens in a
17
+ * child that exits when the batch is answered — the same reason
18
+ * `@seekrit/paperclip-plugin` shells out rather than importing the crypto into a
19
+ * long-lived worker.
20
+ *
21
+ * **Zero dependencies, and a real path on disk.** OpenClaw resolves the
22
+ * entrypoint OpenClaw declared, then refuses it if it is a symlink, a hardlink,
23
+ * or inside a world-writable directory. A plain committed `.js` file at the
24
+ * plugin root satisfies that; a bundler output behind a symlinked `dist` does
25
+ * not.
26
+ *
27
+ * Failure is fail-closed and quiet. A resolver that cannot reach seekrit answers
28
+ * with a per-id error code and exits 0, so OpenClaw refuses to start with a
29
+ * clear "unresolved ref" rather than a resolver crash — and never with a message,
30
+ * because resolver output may contain credentials and OpenClaw declines to
31
+ * display it.
32
+ */
33
+
34
+ import { spawn } from "node:child_process";
35
+ import { createRequire } from "node:module";
36
+ import path from "node:path";
37
+
38
+ const PROTOCOL_VERSION = 1;
39
+
40
+ /** Under the manifest's 30s, so a hung child still produces a clean response. */
41
+ const CHILD_TIMEOUT_MS = 25_000;
42
+
43
+ function readStdin() {
44
+ return new Promise((resolve, reject) => {
45
+ let input = "";
46
+ process.stdin.setEncoding("utf8");
47
+ process.stdin.on("data", (chunk) => {
48
+ input += String(chunk);
49
+ });
50
+ process.stdin.on("error", reject);
51
+ process.stdin.on("end", () => resolve(input));
52
+ });
53
+ }
54
+
55
+ function writeResponse(response) {
56
+ process.stdout.write(`${JSON.stringify(response)}\n`);
57
+ }
58
+
59
+ /** The ids we owe an answer for, so a failure can be reported against each. */
60
+ function requestedIds(input) {
61
+ const parsed = JSON.parse(input);
62
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.ids)) {
63
+ throw new Error("invalid exec SecretRef request");
64
+ }
65
+ // `trim()`, not just `length`, so this list matches the one
66
+ // `seekrit openclaw resolve` builds — a blank id must not become an error key
67
+ // here that the CLI never reports.
68
+ return parsed.ids.filter((id) => typeof id === "string" && id.trim().length > 0);
69
+ }
70
+
71
+ function errorResponse(ids, code) {
72
+ const errors = {};
73
+ for (const id of ids) errors[id] = { code };
74
+ return { protocolVersion: PROTOCOL_VERSION, values: {}, errors };
75
+ }
76
+
77
+ /**
78
+ * Locate the seekrit CLI's entry file.
79
+ *
80
+ * `SEEKRIT_CLI` first, for an install this package cannot see — but only as an
81
+ * absolute path: a bare name would be looked up on `PATH`, and `PATH` is
82
+ * attacker-influenced in a way an explicit absolute path is not.
83
+ *
84
+ * Otherwise resolve `@seekrit/cli` out of this package's own dependency tree.
85
+ * That is deliberately not a `PATH` lookup either: the `seekrit` on `PATH` is a
86
+ * package-manager shim that symlinks into `node_modules`, and its version is
87
+ * whatever the machine happens to have rather than the one this plugin was
88
+ * published against.
89
+ */
90
+ function resolveCliEntry() {
91
+ const override = process.env.SEEKRIT_CLI?.trim();
92
+ if (override) {
93
+ if (!path.isAbsolute(override)) {
94
+ throw new Error("SEEKRIT_CLI must be an absolute path");
95
+ }
96
+ return override;
97
+ }
98
+ const require = createRequire(import.meta.url);
99
+ return require.resolve("@seekrit/cli/cli-entry");
100
+ }
101
+
102
+ /** Run the CLI on the request, resolving to its stdout. */
103
+ function runCli(entry, input) {
104
+ return new Promise((resolve, reject) => {
105
+ const child = spawn(process.execPath, [entry, "openclaw", "resolve"], {
106
+ stdio: ["pipe", "pipe", "pipe"],
107
+ env: process.env,
108
+ });
109
+ let stdout = "";
110
+ let settled = false;
111
+ const finish = (fn, arg) => {
112
+ if (settled) return;
113
+ settled = true;
114
+ clearTimeout(timer);
115
+ fn(arg);
116
+ };
117
+ const timer = setTimeout(() => {
118
+ child.kill("SIGKILL");
119
+ finish(reject, new Error("seekrit CLI timed out"));
120
+ }, CHILD_TIMEOUT_MS);
121
+
122
+ child.stdout.setEncoding("utf8");
123
+ child.stdout.on("data", (chunk) => {
124
+ stdout += String(chunk);
125
+ });
126
+ // Drained and dropped. The CLI writes diagnostics here, and a full pipe
127
+ // would deadlock a child that is otherwise about to answer — but nothing
128
+ // from it may reach our own stderr, which OpenClaw captures.
129
+ child.stderr.resume();
130
+ child.on("error", (err) => finish(reject, err));
131
+ child.on("close", (code) => {
132
+ if (code === 0) finish(resolve, stdout);
133
+ else finish(reject, new Error(`seekrit CLI exited ${code}`));
134
+ });
135
+
136
+ child.stdin.on("error", (err) => finish(reject, err));
137
+ child.stdin.end(input);
138
+ });
139
+ }
140
+
141
+ /** Accept only a well-formed protocol response, so junk cannot look like values. */
142
+ function parseCliResponse(stdout) {
143
+ const parsed = JSON.parse(stdout);
144
+ if (!parsed || typeof parsed !== "object" || typeof parsed.values !== "object") {
145
+ throw new Error("seekrit CLI returned an unexpected shape");
146
+ }
147
+ return {
148
+ protocolVersion: PROTOCOL_VERSION,
149
+ values: parsed.values ?? {},
150
+ ...(parsed.errors ? { errors: parsed.errors } : {}),
151
+ };
152
+ }
153
+
154
+ async function main() {
155
+ const input = await readStdin();
156
+
157
+ let ids;
158
+ try {
159
+ ids = requestedIds(input);
160
+ } catch (err) {
161
+ // Nothing to answer: no ids to attach an error to. Exit non-zero so OpenClaw
162
+ // reports a resolver fault rather than a silently empty batch.
163
+ process.stderr.write(`seekrit: ${err.message}\n`);
164
+ process.exitCode = 2;
165
+ return;
166
+ }
167
+
168
+ let entry;
169
+ try {
170
+ entry = resolveCliEntry();
171
+ } catch {
172
+ writeResponse(errorResponse(ids, "UNAVAILABLE"));
173
+ return;
174
+ }
175
+
176
+ try {
177
+ writeResponse(parseCliResponse(await runCli(entry, input)));
178
+ } catch {
179
+ writeResponse(errorResponse(ids, "UNAVAILABLE"));
180
+ }
181
+ }
182
+
183
+ await main();