@clawkeepers/kernel 0.1.0-beta.1
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/LICENSE +21 -0
- package/NOTICE +206 -0
- package/README.md +20 -0
- package/config.schema.json +14 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +1205 -0
- package/openclaw.plugin.json +137 -0
- package/package.json +68 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1205 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Type as Type2 } from "typebox";
|
|
3
|
+
|
|
4
|
+
// src/upstream/sdk.ts
|
|
5
|
+
import { definePluginEntry, buildJsonPluginConfigSchema } from "openclaw/plugin-sdk/plugin-entry";
|
|
6
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
7
|
+
|
|
8
|
+
// src/config-schema.ts
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
var schema = JSON.parse(readFileSync(new URL("../config.schema.json", import.meta.url), "utf8"));
|
|
11
|
+
|
|
12
|
+
// src/upstream/cli.ts
|
|
13
|
+
import { spawnSync } from "child_process";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join, resolve } from "path";
|
|
16
|
+
function operatorCell(env, home = homedir()) {
|
|
17
|
+
const cell2 = env.CLAWOS_CELL ?? env.OPENCLAW_PROFILE ?? "default";
|
|
18
|
+
if (!/^[a-z](?:[a-z0-9-]{0,30}[a-z0-9])?$/.test(cell2)) throw new Error("Invalid operator cell.");
|
|
19
|
+
if (env.CLAWOS_CELL && env.OPENCLAW_PROFILE && env.CLAWOS_CELL !== env.OPENCLAW_PROFILE) throw new Error("Conflicting operator cell selectors.");
|
|
20
|
+
const state = join(home, cell2 === "default" ? ".openclaw" : `.openclaw-${cell2}`);
|
|
21
|
+
if (env.OPENCLAW_STATE_DIR && resolve(env.OPENCLAW_STATE_DIR) !== state || env.OPENCLAW_CONFIG_PATH && resolve(env.OPENCLAW_CONFIG_PATH) !== join(state, "openclaw.json")) {
|
|
22
|
+
throw new Error("Operator CLI requires a registered canonical cell; explicit state/config does not match.");
|
|
23
|
+
}
|
|
24
|
+
return cell2;
|
|
25
|
+
}
|
|
26
|
+
function runOperatorCli(cell2, args) {
|
|
27
|
+
const result = spawnSync("clawos", [...args, "--cell", cell2, "--json"], {
|
|
28
|
+
env: process.env,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
timeout: 9e4,
|
|
31
|
+
maxBuffer: 4 * 1024 * 1024
|
|
32
|
+
});
|
|
33
|
+
if (result.status !== 0 || result.error) throw new Error("Kernel CLI unavailable or unauthorized; verify clawos installation and cell pairing.");
|
|
34
|
+
try {
|
|
35
|
+
process.stdout.write(`${JSON.stringify(JSON.parse(result.stdout))}
|
|
36
|
+
`);
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error("Kernel CLI returned an invalid response.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function mountOperatorCli(program) {
|
|
42
|
+
const cell2 = operatorCell(process.env);
|
|
43
|
+
const os = program.command("os").description("OpenClaw OS kernel administration");
|
|
44
|
+
os.command("status").option("--json").action(() => runOperatorCli(cell2, ["kernel", "status"]));
|
|
45
|
+
os.command("grants").option("--json").action(() => runOperatorCli(cell2, ["grant", "list"]));
|
|
46
|
+
os.command("approvals").option("--json").action(() => runOperatorCli(cell2, ["approvals", "list"]));
|
|
47
|
+
os.command("audit").option("--json").option("--limit <n>").action((o) => runOperatorCli(cell2, ["audit", "tail", "--limit", o.limit ?? "100"]));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/kernel.ts
|
|
51
|
+
import { randomBytes } from "crypto";
|
|
52
|
+
import { join as join4 } from "path";
|
|
53
|
+
import { existsSync, readFileSync as readFileSync4, writeFileSync, renameSync, unlinkSync } from "fs";
|
|
54
|
+
import { evaluateInstall, ApprovalDecisionParams, ConnectGatekeeperParams, IntroduceParams, ListGrantsParams, RevokeGrantParams } from "@clawkeepers/shared";
|
|
55
|
+
import { Type } from "typebox";
|
|
56
|
+
import { Value as Value5 } from "typebox/value";
|
|
57
|
+
|
|
58
|
+
// src/store.ts
|
|
59
|
+
import { mkdirSync } from "fs";
|
|
60
|
+
import { createRequire } from "module";
|
|
61
|
+
import { dirname } from "path";
|
|
62
|
+
var { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
63
|
+
var SCHEMA = `
|
|
64
|
+
CREATE TABLE IF NOT EXISTS meta(k TEXT PRIMARY KEY,v TEXT NOT NULL);
|
|
65
|
+
CREATE TABLE IF NOT EXISTS grants(handle TEXT PRIMARY KEY,agentId TEXT NOT NULL,cellId TEXT NOT NULL,vendor TEXT NOT NULL,resourceType TEXT NOT NULL,resourceKey TEXT NOT NULL,operatorId TEXT NOT NULL,scope TEXT NOT NULL,audience TEXT NOT NULL,status TEXT NOT NULL,createdAt INTEGER NOT NULL,createdBy TEXT NOT NULL,expiresAt INTEGER,title TEXT);
|
|
66
|
+
CREATE UNIQUE INDEX IF NOT EXISTS grant_identity ON grants(agentId,vendor,resourceType,resourceKey,operatorId,scope);
|
|
67
|
+
CREATE TABLE IF NOT EXISTS instances(id TEXT PRIMARY KEY,vendor TEXT NOT NULL,resourceKey TEXT NOT NULL,operatorId TEXT NOT NULL,observerStrategy TEXT NOT NULL,lockdown INTEGER NOT NULL DEFAULT 0);
|
|
68
|
+
CREATE TABLE IF NOT EXISTS actions(id INTEGER PRIMARY KEY AUTOINCREMENT,gatekeeperInstance TEXT NOT NULL,actionId INTEGER NOT NULL,descriptionJson TEXT NOT NULL,status TEXT NOT NULL,submittedAt INTEGER NOT NULL,decidedBy TEXT,decidedAt INTEGER,appliedAt INTEGER,error TEXT,UNIQUE(gatekeeperInstance,actionId));
|
|
69
|
+
CREATE TABLE IF NOT EXISTS notifications(runId TEXT PRIMARY KEY,createdAt INTEGER NOT NULL);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS action_bindings(id INTEGER PRIMARY KEY,handle TEXT NOT NULL,agentId TEXT NOT NULL,sessionKey TEXT NOT NULL);
|
|
71
|
+
CREATE TABLE IF NOT EXISTS introductions(id INTEGER PRIMARY KEY AUTOINCREMENT,agentId TEXT NOT NULL,sessionKey TEXT,url TEXT NOT NULL,reason TEXT,status TEXT NOT NULL,createdAt INTEGER NOT NULL,requestedBy TEXT);
|
|
72
|
+
CREATE TABLE IF NOT EXISTS observers(sessionKey TEXT NOT NULL,observerId TEXT NOT NULL,tainted INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(sessionKey,observerId));
|
|
73
|
+
CREATE TABLE IF NOT EXISTS approval_decisions(toolCallId TEXT PRIMARY KEY,tool TEXT NOT NULL,paramsJson TEXT NOT NULL,decision TEXT NOT NULL,operatorId TEXT,decidedAt INTEGER NOT NULL);
|
|
74
|
+
CREATE TABLE IF NOT EXISTS audit_index(ts TEXT NOT NULL,kind TEXT NOT NULL,agentId TEXT,handle TEXT,file TEXT NOT NULL,line INTEGER NOT NULL);
|
|
75
|
+
INSERT OR REPLACE INTO meta(k,v) VALUES('schema','1');`;
|
|
76
|
+
var Store = class {
|
|
77
|
+
db;
|
|
78
|
+
constructor(path) {
|
|
79
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
80
|
+
this.db = new DatabaseSync(path);
|
|
81
|
+
this.db.exec("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;");
|
|
82
|
+
}
|
|
83
|
+
/** Refuse forward-schema rollback; an existing current schema is never rewritten at startup. */
|
|
84
|
+
migrate() {
|
|
85
|
+
const exists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='meta'").get();
|
|
86
|
+
if (exists) {
|
|
87
|
+
const schema2 = this.db.prepare("SELECT v FROM meta WHERE k='schema'").get();
|
|
88
|
+
if (schema2?.v !== "1") throw new Error("Kernel schema incompatible.");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.db.exec(SCHEMA);
|
|
92
|
+
}
|
|
93
|
+
close() {
|
|
94
|
+
this.db.close();
|
|
95
|
+
}
|
|
96
|
+
getGrant(handle) {
|
|
97
|
+
return grantRow(this.db.prepare("SELECT * FROM grants WHERE handle=?").get(handle));
|
|
98
|
+
}
|
|
99
|
+
isActiveHandle(handle, now = Date.now()) {
|
|
100
|
+
if (typeof handle !== "string") return false;
|
|
101
|
+
const g = this.getGrant(handle);
|
|
102
|
+
return g?.status === "active" && (g.expiresAt === void 0 || g.expiresAt > now);
|
|
103
|
+
}
|
|
104
|
+
authorizeGrant(expected, agentId, sessionKey, cellId, now = Date.now()) {
|
|
105
|
+
const current = this.getGrant(expected.handle);
|
|
106
|
+
if (!current || current.status !== "active" || current.audience !== "owner-only" || current.expiresAt !== void 0 && current.expiresAt <= now || current.agentId !== agentId || current.cellId !== cellId || current.scope !== "agent" && current.scope !== `session:${sessionKey}` || current.vendor !== expected.vendor || current.resourceType !== expected.resourceType || current.resourceKey !== expected.resourceKey || current.operatorId !== expected.operatorId) return null;
|
|
107
|
+
return current;
|
|
108
|
+
}
|
|
109
|
+
listGrants(agentId, activeOnly = true) {
|
|
110
|
+
return this.db.prepare(`SELECT * FROM grants WHERE agentId=?${activeOnly ? " AND status='active'" : ""} ORDER BY createdAt,handle`).all(agentId).map((r) => grantRow(r));
|
|
111
|
+
}
|
|
112
|
+
allGrants(agentId) {
|
|
113
|
+
const rows = agentId ? this.db.prepare("SELECT * FROM grants WHERE agentId=? ORDER BY createdAt").all(agentId) : this.db.prepare("SELECT * FROM grants ORDER BY createdAt").all();
|
|
114
|
+
return rows.map((r) => grantRow(r));
|
|
115
|
+
}
|
|
116
|
+
insertGrant(g) {
|
|
117
|
+
this.db.prepare(`INSERT OR IGNORE INTO grants(handle,agentId,cellId,vendor,resourceType,resourceKey,operatorId,scope,audience,status,createdAt,createdBy,expiresAt,title) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(g.handle, g.agentId, g.cellId, g.vendor, g.resourceType, g.resourceKey, g.operatorId, g.scope, g.audience, g.status, g.createdAt, g.createdBy, g.expiresAt ?? null, g.title ?? null);
|
|
118
|
+
return grantRow(this.db.prepare("SELECT * FROM grants WHERE agentId=? AND vendor=? AND resourceType=? AND resourceKey=? AND operatorId=? AND scope=?").get(g.agentId, g.vendor, g.resourceType, g.resourceKey, g.operatorId, g.scope));
|
|
119
|
+
}
|
|
120
|
+
setGrantStatus(handle, status) {
|
|
121
|
+
return Number(this.db.prepare("UPDATE grants SET status=? WHERE handle=?").run(status, handle).changes) === 1;
|
|
122
|
+
}
|
|
123
|
+
upsertInstance(r) {
|
|
124
|
+
this.db.prepare(`INSERT INTO instances(id,vendor,resourceKey,operatorId,observerStrategy,lockdown) VALUES(?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET observerStrategy=excluded.observerStrategy,lockdown=MAX(instances.lockdown,excluded.lockdown)`).run(r.id, r.vendor, r.resourceKey, r.operatorId, r.observerStrategy, r.lockdown);
|
|
125
|
+
}
|
|
126
|
+
getInstance(id) {
|
|
127
|
+
return this.db.prepare("SELECT * FROM instances WHERE id=?").get(id) ?? null;
|
|
128
|
+
}
|
|
129
|
+
lockdownInstance(id) {
|
|
130
|
+
this.db.prepare("UPDATE instances SET lockdown=1 WHERE id=?").run(id);
|
|
131
|
+
}
|
|
132
|
+
addIntroduction(v) {
|
|
133
|
+
return Number(this.db.prepare("INSERT INTO introductions(agentId,sessionKey,url,reason,status,createdAt,requestedBy) VALUES(?,?,?,?,?,?,?)").run(v.agentId, v.sessionKey ?? null, v.url, v.reason ?? null, "pending", Date.now(), v.requestedBy ?? null).lastInsertRowid);
|
|
134
|
+
}
|
|
135
|
+
/** Operator-visible pending resource introductions, bounded by the caller. */
|
|
136
|
+
listIntroductions() {
|
|
137
|
+
return this.db.prepare("SELECT id,agentId,sessionKey,url,reason FROM introductions WHERE status='pending' ORDER BY id LIMIT 100").all();
|
|
138
|
+
}
|
|
139
|
+
/** Claim before awaiting grant creation, so overlapping operator decisions cannot duplicate it. */
|
|
140
|
+
claimIntroduction(id) {
|
|
141
|
+
return Number(this.db.prepare("UPDATE introductions SET status='resolving' WHERE id=? AND status='pending'").run(id).changes) === 1;
|
|
142
|
+
}
|
|
143
|
+
/** Complete an already-claimed introduction without changing its bound agent or resource. */
|
|
144
|
+
finishIntroduction(id, status) {
|
|
145
|
+
this.db.prepare("UPDATE introductions SET status=? WHERE id=? AND status='resolving'").run(status, id);
|
|
146
|
+
}
|
|
147
|
+
/** Claim a run digest before delivery: uncertain sends are not repeated. */
|
|
148
|
+
claimNotification(runId) {
|
|
149
|
+
return Number(this.db.prepare("INSERT OR IGNORE INTO notifications(runId,createdAt) VALUES(?,?)").run(runId, Date.now()).changes) === 1;
|
|
150
|
+
}
|
|
151
|
+
countPendingRequests() {
|
|
152
|
+
return this.count("introductions");
|
|
153
|
+
}
|
|
154
|
+
addAction(instance, actionId, d) {
|
|
155
|
+
this.db.prepare("INSERT OR IGNORE INTO actions(gatekeeperInstance,actionId,descriptionJson,status,submittedAt) VALUES(?,?,?,?,?)").run(instance, actionId, JSON.stringify(d), "pending", Date.now());
|
|
156
|
+
return this.db.prepare("SELECT * FROM actions WHERE gatekeeperInstance=? AND actionId=?").get(instance, actionId);
|
|
157
|
+
}
|
|
158
|
+
listActions(pendingOnly = true) {
|
|
159
|
+
return this.db.prepare(`SELECT * FROM actions${pendingOnly ? " WHERE status='pending'" : ""} ORDER BY id`).all();
|
|
160
|
+
}
|
|
161
|
+
getAction(id) {
|
|
162
|
+
return this.db.prepare("SELECT * FROM actions WHERE id=?").get(id) ?? null;
|
|
163
|
+
}
|
|
164
|
+
decideAction(id, status, operatorId, error) {
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
return Number(this.db.prepare("UPDATE actions SET status=?,decidedBy=?,decidedAt=?,appliedAt=CASE WHEN ?='applied' THEN ? ELSE appliedAt END,error=? WHERE id=?").run(status, operatorId, now, status, now, error ?? null, id).changes) === 1;
|
|
167
|
+
}
|
|
168
|
+
/** Persist the originating capability separately; duplicate submissions cannot rebind it. */
|
|
169
|
+
bindAction(id, handle, agentId, sessionKey) {
|
|
170
|
+
this.db.prepare("INSERT OR IGNORE INTO action_bindings(id,handle,agentId,sessionKey) VALUES(?,?,?,?)").run(id, handle, agentId, sessionKey);
|
|
171
|
+
}
|
|
172
|
+
/** Only an original binding may authorize an external approval effect. */
|
|
173
|
+
actionBinding(id) {
|
|
174
|
+
return this.db.prepare("SELECT handle,agentId,sessionKey FROM action_bindings WHERE id=?").get(id) ?? null;
|
|
175
|
+
}
|
|
176
|
+
/** A crash during an effect leaves a terminal, non-retryable uncertain record. */
|
|
177
|
+
claimAction(id, expected, operator) {
|
|
178
|
+
return Number(this.db.prepare("UPDATE actions SET status='failed',decidedBy=?,decidedAt=?,error='Outcome unconfirmed; reconcile before retry' WHERE id=? AND status=?").run(operator, Date.now(), id, expected).changes) === 1;
|
|
179
|
+
}
|
|
180
|
+
countPending() {
|
|
181
|
+
return this.count("actions");
|
|
182
|
+
}
|
|
183
|
+
setObservers(sessionKey, observers) {
|
|
184
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
185
|
+
try {
|
|
186
|
+
this.db.prepare("DELETE FROM observers WHERE sessionKey=? AND tainted=0").run(sessionKey);
|
|
187
|
+
const q = this.db.prepare("INSERT OR IGNORE INTO observers(sessionKey,observerId,tainted) VALUES(?,?,0)");
|
|
188
|
+
for (const id of observers) q.run(sessionKey, id);
|
|
189
|
+
this.db.exec("COMMIT");
|
|
190
|
+
} catch (error) {
|
|
191
|
+
this.db.exec("ROLLBACK");
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
observers(sessionKey) {
|
|
196
|
+
return this.db.prepare("SELECT observerId FROM observers WHERE sessionKey=? ORDER BY observerId").all(sessionKey).map((r) => r.observerId);
|
|
197
|
+
}
|
|
198
|
+
taintObserver(sessionKey, observerId) {
|
|
199
|
+
this.db.prepare("INSERT INTO observers(sessionKey,observerId,tainted) VALUES(?,?,1) ON CONFLICT(sessionKey,observerId) DO UPDATE SET tainted=1").run(sessionKey, observerId);
|
|
200
|
+
}
|
|
201
|
+
recordToolDecision(toolCallId, tool, params, decision, operatorId) {
|
|
202
|
+
this.db.prepare("INSERT OR REPLACE INTO approval_decisions(toolCallId,tool,paramsJson,decision,operatorId,decidedAt) VALUES(?,?,?,?,?,?)").run(toolCallId, tool, JSON.stringify(params), decision, operatorId ?? null, Date.now());
|
|
203
|
+
}
|
|
204
|
+
consumeToolApproval(toolCallId, tool, params) {
|
|
205
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
206
|
+
try {
|
|
207
|
+
const r = this.db.prepare("SELECT tool,paramsJson,decision FROM approval_decisions WHERE toolCallId=?").get(toolCallId);
|
|
208
|
+
this.db.prepare("DELETE FROM approval_decisions WHERE toolCallId=?").run(toolCallId);
|
|
209
|
+
const ok = !!r && r.tool === tool && r.paramsJson === JSON.stringify(params) && r.decision.startsWith("allow");
|
|
210
|
+
this.db.exec("COMMIT");
|
|
211
|
+
return ok;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.db.exec("ROLLBACK");
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
count(table) {
|
|
218
|
+
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE status='pending'`).get().n);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
function grantRow(row) {
|
|
222
|
+
if (!row) return null;
|
|
223
|
+
const r = row;
|
|
224
|
+
const { expiresAt, title, ...base } = r;
|
|
225
|
+
return { ...base, ...expiresAt === null ? {} : { expiresAt }, ...title === null ? {} : { title } };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/registry.ts
|
|
229
|
+
import { readFileSync as readFileSync2, realpathSync } from "fs";
|
|
230
|
+
import { Value } from "typebox/value";
|
|
231
|
+
import { GatekeeperToolDefSchema, SupportedResourceSchema } from "@clawkeepers/shared";
|
|
232
|
+
import { gatekeeperRuntimeSlot } from "@clawkeepers/gatekeeper-kit";
|
|
233
|
+
var Registry = class {
|
|
234
|
+
constructor(catalogPath, stateDir2) {
|
|
235
|
+
this.catalogPath = catalogPath;
|
|
236
|
+
this.stateDir = stateDir2;
|
|
237
|
+
this.load();
|
|
238
|
+
}
|
|
239
|
+
entries = /* @__PURE__ */ new Map();
|
|
240
|
+
tools = [];
|
|
241
|
+
toolNames() {
|
|
242
|
+
return this.tools.map((t) => t.name);
|
|
243
|
+
}
|
|
244
|
+
resources() {
|
|
245
|
+
return [...this.entries.values()].flatMap((entry) => entry.resources.map((resource) => ({ entry, resource })));
|
|
246
|
+
}
|
|
247
|
+
entryForTool(name) {
|
|
248
|
+
return [...this.entries.values()].find((e) => e.tools.some((t) => t.name === name));
|
|
249
|
+
}
|
|
250
|
+
load() {
|
|
251
|
+
let parsed;
|
|
252
|
+
try {
|
|
253
|
+
parsed = JSON.parse(readFileSync2(this.catalogPath, "utf8"));
|
|
254
|
+
} catch {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.gatekeepers)) throw new Error("Invalid gatekeeper catalog.");
|
|
258
|
+
const names = /* @__PURE__ */ new Set();
|
|
259
|
+
for (const raw of parsed.gatekeepers) {
|
|
260
|
+
if (raw.enabled === false) continue;
|
|
261
|
+
if (!/^gatekeeper-[a-z][a-z0-9_]*$/.test(raw.pluginId) || raw.pluginId !== `gatekeeper-${raw.vendor}` || raw.apiVersion !== 1 || this.entries.has(raw.vendor)) throw new Error("Invalid gatekeeper catalog identity.");
|
|
262
|
+
const root = realpathSync(raw.root);
|
|
263
|
+
if (!Array.isArray(raw.tools) || !Array.isArray(raw.resources)) throw new Error("Invalid gatekeeper catalog metadata.");
|
|
264
|
+
for (const tool of raw.tools) {
|
|
265
|
+
if (!Value.Check(GatekeeperToolDefSchema, tool) || names.has(tool.name) || !tool.name.startsWith(`gk_${raw.vendor}_`)) throw new Error("Invalid gatekeeper catalog tool.");
|
|
266
|
+
names.add(tool.name);
|
|
267
|
+
}
|
|
268
|
+
for (const resource of raw.resources) if (!Value.Check(SupportedResourceSchema, resource) || resource.tools.some((n) => !raw.tools.some((t) => t.name === n && t.resourceType === resource.type))) throw new Error("Invalid gatekeeper catalog resource.");
|
|
269
|
+
const entry = { ...raw, root, tools: structuredClone(raw.tools), resources: structuredClone(raw.resources) };
|
|
270
|
+
this.entries.set(entry.vendor, entry);
|
|
271
|
+
this.tools.push(...entry.tools);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
live(vendor) {
|
|
275
|
+
const entry = this.entries.get(vendor);
|
|
276
|
+
if (!entry) throw new Error("Gatekeeper unavailable.");
|
|
277
|
+
const runtime = gatekeeperRuntimeSlot(entry.pluginId).tryGetRuntime();
|
|
278
|
+
if (!runtime || runtime.pluginId !== entry.pluginId || runtime.vendor !== entry.vendor || runtime.apiVersion !== entry.apiVersion || realpathSync(runtime.root) !== entry.root || realpathSync(runtime.stateDir) !== realpathSync(this.stateDir)) throw new Error("Gatekeeper unavailable.");
|
|
279
|
+
return { entry, vendor: runtime.getVendor() };
|
|
280
|
+
}
|
|
281
|
+
/** Resolve a live vendor for operator-only account setup, never for resource access. */
|
|
282
|
+
connection(vendorName) {
|
|
283
|
+
return this.live(vendorName).vendor;
|
|
284
|
+
}
|
|
285
|
+
/** Resolve account and resource afresh; this is called only by Kernel.resolveGrant. */
|
|
286
|
+
async openSession(grant, queue) {
|
|
287
|
+
const { vendor } = this.live(grant.vendor);
|
|
288
|
+
const account = await vendor.getAccount(grant.operatorId) ?? await vendor.createAccount?.(grant.operatorId);
|
|
289
|
+
if (!account) throw new Error("Gatekeeper unavailable.");
|
|
290
|
+
const resolved = await account.getGatekeeperFor(grant.resourceKey);
|
|
291
|
+
if (resolved.resource.type !== grant.resourceType || resolved.resourceKey !== grant.resourceKey) throw new Error("Gatekeeper unavailable.");
|
|
292
|
+
return { session: await resolved.gatekeeper.startSession(queue), gatekeeper: resolved.gatekeeper, instanceId: instanceId(grant) };
|
|
293
|
+
}
|
|
294
|
+
/** Validate an introduction through the operator's live account. */
|
|
295
|
+
async introduce(vendorName, operatorId, url) {
|
|
296
|
+
const { entry, vendor } = this.live(vendorName);
|
|
297
|
+
let account = await vendor.getAccount(operatorId);
|
|
298
|
+
if (!account && vendor.createAccount) account = await vendor.createAccount(operatorId);
|
|
299
|
+
if (!account) throw new Error("Gatekeeper account unavailable.");
|
|
300
|
+
const resolved = await account.getGatekeeperFor(url);
|
|
301
|
+
if (!entry.resources.some((r) => r.type === resolved.resource.type)) throw new Error("Unsupported resource.");
|
|
302
|
+
return resolved;
|
|
303
|
+
}
|
|
304
|
+
/** Return safe health metadata without exposing resource identities. */
|
|
305
|
+
health() {
|
|
306
|
+
return [...this.entries.values()].map((entry) => {
|
|
307
|
+
try {
|
|
308
|
+
this.live(entry.vendor);
|
|
309
|
+
return { vendor: entry.vendor, healthy: true, accounts: 0 };
|
|
310
|
+
} catch {
|
|
311
|
+
return { vendor: entry.vendor, healthy: false, accounts: 0 };
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
function instanceId(g) {
|
|
317
|
+
return `${g.vendor}\0${g.operatorId}\0${g.resourceKey}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/audit.ts
|
|
321
|
+
import { appendFileSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
322
|
+
import { join as join2 } from "path";
|
|
323
|
+
import { Value as Value2 } from "typebox/value";
|
|
324
|
+
import { AuditRecordSchema } from "@clawkeepers/shared";
|
|
325
|
+
var AuditLog = class {
|
|
326
|
+
constructor(dir) {
|
|
327
|
+
this.dir = dir;
|
|
328
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
329
|
+
}
|
|
330
|
+
write(record) {
|
|
331
|
+
if (!Value2.Check(AuditRecordSchema, record)) throw new Error("Invalid audit record.");
|
|
332
|
+
const day = record.ts.slice(0, 10);
|
|
333
|
+
appendFileSync(join2(this.dir, `${day}.jsonl`), `${JSON.stringify(record)}
|
|
334
|
+
`, { mode: 384 });
|
|
335
|
+
}
|
|
336
|
+
query(limit = 100) {
|
|
337
|
+
const files = readdirSync(this.dir).filter((f) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort().reverse();
|
|
338
|
+
const out = [];
|
|
339
|
+
for (const file of files) {
|
|
340
|
+
for (const line of readFileSync3(join2(this.dir, file), "utf8").trim().split("\n").reverse()) {
|
|
341
|
+
if (!line) continue;
|
|
342
|
+
const value = JSON.parse(line);
|
|
343
|
+
if (Value2.Check(AuditRecordSchema, value)) out.push(value);
|
|
344
|
+
if (out.length >= Math.min(1e3, Math.max(1, limit))) return out;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
async flush() {
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// src/oauth.ts
|
|
354
|
+
import { OAuthNonceMachine } from "@clawkeepers/gatekeeper-kit";
|
|
355
|
+
var namespace = /^[a-z][a-z0-9_]{0,63}$/;
|
|
356
|
+
var noncePattern = /^[A-Za-z0-9_-]{32}$/;
|
|
357
|
+
var denied = () => new Error("Account connection unavailable.");
|
|
358
|
+
var OAuthRouter = class {
|
|
359
|
+
constructor(registry, now = Date.now) {
|
|
360
|
+
this.registry = registry;
|
|
361
|
+
this.now = now;
|
|
362
|
+
}
|
|
363
|
+
flows = /* @__PURE__ */ new Map();
|
|
364
|
+
/** Issue a private local browser URL; callers must supply operator identity from authenticated RPC, not request data. */
|
|
365
|
+
async connect(vendorName, operatorId, resourceTypes) {
|
|
366
|
+
try {
|
|
367
|
+
if (!namespace.test(vendorName) || !operatorId || operatorId.length > 512 || /[\u0000-\u001f\u007f]/u.test(operatorId)) throw denied();
|
|
368
|
+
const entry = this.registry.entries.get(vendorName);
|
|
369
|
+
if (!entry || resourceTypes !== void 0 && (!Array.isArray(resourceTypes) || resourceTypes.length > 32 || new Set(resourceTypes).size !== resourceTypes.length || resourceTypes.some((type) => typeof type !== "string" || !namespace.test(type) || !entry.resources.some((resource) => resource.type === type)))) throw denied();
|
|
370
|
+
const flow = this.flow(vendorName);
|
|
371
|
+
if (!flow.vendor.completeConnection && !await this.staticAccount(flow.vendor)) throw denied();
|
|
372
|
+
const binding = { operatorId, ...resourceTypes ? { resourceTypes: [...resourceTypes] } : {} };
|
|
373
|
+
const state = flow.nonces.issue(JSON.stringify(binding));
|
|
374
|
+
return { url: `/os/gatekeeper/${vendorName}/oauth/start?state=${state}` };
|
|
375
|
+
} catch {
|
|
376
|
+
throw denied();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** Handle GET start/callback only; malformed, replayed, expired and cross-vendor nonces fail without vendor details. */
|
|
380
|
+
async handle(req, res) {
|
|
381
|
+
res.setHeader("Cache-Control", "no-store");
|
|
382
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
383
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'");
|
|
384
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
385
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
386
|
+
const finish = (status, body) => {
|
|
387
|
+
res.statusCode = status;
|
|
388
|
+
res.end(body);
|
|
389
|
+
return true;
|
|
390
|
+
};
|
|
391
|
+
if (req.method !== "GET") {
|
|
392
|
+
res.setHeader("Allow", "GET");
|
|
393
|
+
return finish(405, "Method not allowed.");
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const raw = req.url ?? "";
|
|
397
|
+
if (raw.length > 8192) throw denied();
|
|
398
|
+
const match = /^\/os\/gatekeeper\/([a-z][a-z0-9_]{0,63})\/oauth\/(start|callback)(?:\?([^#]*))?$/.exec(raw);
|
|
399
|
+
if (!match) return finish(404, "Not found.");
|
|
400
|
+
const vendorName = match[1], stage = match[2], query = new URLSearchParams(match[3] ?? "");
|
|
401
|
+
const allowed = stage === "start" ? ["state"] : ["state", "code", "error"];
|
|
402
|
+
for (const key of query.keys()) if (!allowed.includes(key) || query.getAll(key).length !== 1) throw denied();
|
|
403
|
+
const state = query.get("state") ?? "";
|
|
404
|
+
if (!noncePattern.test(state)) throw denied();
|
|
405
|
+
const flow = this.flow(vendorName);
|
|
406
|
+
if (stage === "start") {
|
|
407
|
+
const advanced = flow.nonces.advanceBound(state);
|
|
408
|
+
if (!advanced) throw denied();
|
|
409
|
+
const binding2 = JSON.parse(advanced.operatorId);
|
|
410
|
+
try {
|
|
411
|
+
if (!flow.vendor.completeConnection && await this.staticAccount(flow.vendor)) {
|
|
412
|
+
if (!flow.nonces.consume(advanced.nonce)) throw denied();
|
|
413
|
+
const account = await flow.vendor.getAccount(binding2.operatorId) ?? await flow.vendor.createAccount(binding2.operatorId);
|
|
414
|
+
if (!account) throw denied();
|
|
415
|
+
return finish(200, "Account connected. You may close this window.");
|
|
416
|
+
}
|
|
417
|
+
const result = await flow.vendor.connectAccount(binding2.operatorId, {
|
|
418
|
+
...binding2.resourceTypes ? { resourceTypes: binding2.resourceTypes } : {},
|
|
419
|
+
state: advanced.nonce,
|
|
420
|
+
callbackPath: `/os/gatekeeper/${vendorName}/oauth/callback`
|
|
421
|
+
});
|
|
422
|
+
const location = authorizationUrl(result.url, advanced.nonce);
|
|
423
|
+
res.setHeader("Location", location);
|
|
424
|
+
return finish(303, "Continue account connection in your browser.");
|
|
425
|
+
} catch {
|
|
426
|
+
flow.nonces.consume(advanced.nonce);
|
|
427
|
+
throw denied();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const bound = flow.nonces.consume(state);
|
|
431
|
+
if (!bound || !flow.vendor.completeConnection) throw denied();
|
|
432
|
+
const code = query.get("code");
|
|
433
|
+
if (query.has("error") || !code || code.length > 4096 || /[\u0000-\u0020\u007f]/u.test(code)) throw denied();
|
|
434
|
+
const binding = JSON.parse(bound);
|
|
435
|
+
await flow.vendor.completeConnection(binding.operatorId, { code, state, ...binding.resourceTypes ? { resourceTypes: binding.resourceTypes } : {} });
|
|
436
|
+
if (!await flow.vendor.getAccount(binding.operatorId)) throw denied();
|
|
437
|
+
return finish(200, "Account connected. You may close this window.");
|
|
438
|
+
} catch {
|
|
439
|
+
return finish(400, "Account connection unavailable. Start a new connection from your operator client.");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/** Revoke every outstanding connection on kernel shutdown. */
|
|
443
|
+
clear() {
|
|
444
|
+
this.flows.clear();
|
|
445
|
+
}
|
|
446
|
+
flow(vendorName) {
|
|
447
|
+
const vendor = this.registry.connection(vendorName);
|
|
448
|
+
let flow = this.flows.get(vendorName);
|
|
449
|
+
if (!flow || flow.vendor !== vendor) {
|
|
450
|
+
flow = { vendor, nonces: new OAuthNonceMachine(6e5, this.now) };
|
|
451
|
+
this.flows.set(vendorName, flow);
|
|
452
|
+
}
|
|
453
|
+
return flow;
|
|
454
|
+
}
|
|
455
|
+
async staticAccount(vendor) {
|
|
456
|
+
return !!vendor.createAccount && (await vendor.describe()).autoProvisionsAccount === true;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
function authorizationUrl(value, state) {
|
|
460
|
+
if (typeof value !== "string" || value.length > 8192 || /[\u0000-\u0020\u007f]/u.test(value)) throw denied();
|
|
461
|
+
const url = new URL(value);
|
|
462
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash || url.searchParams.getAll("state").length !== 1 || url.searchParams.get("state") !== state) throw denied();
|
|
463
|
+
for (const key of url.searchParams.keys()) if (/^(?:access_token|refresh_token|id_token|client_secret|password|authorization|code)$/i.test(key)) throw denied();
|
|
464
|
+
return url.href;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/upstream/notify.ts
|
|
468
|
+
import { execFile } from "child_process";
|
|
469
|
+
function sendOperatorDigest(target, actions, requests) {
|
|
470
|
+
if (!target.channel || !target.target || target.channel.length > 64 || target.target.length > 512 || /[\u0000-\u001f\u007f]/u.test(target.channel + target.target) || ![actions, requests].every((n) => Number.isSafeInteger(n) && n >= 0)) return Promise.reject(new Error("Invalid notification configuration."));
|
|
471
|
+
const message = `OpenClaw OS: ${actions} pending action(s), ${requests} access request(s). Use /approvals in your private operator conversation or clawos approvals list.`;
|
|
472
|
+
return new Promise((resolve2, reject) => {
|
|
473
|
+
execFile("openclaw", ["message", "send", "--channel", target.channel, "--target", target.target, "--message", message, "--json"], { env: process.env, timeout: 3e4, maxBuffer: 65536 }, (error) => {
|
|
474
|
+
if (error) reject(new Error("Operator digest delivery unconfirmed."));
|
|
475
|
+
else resolve2();
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/commands.ts
|
|
481
|
+
function isOperatorCommand(body) {
|
|
482
|
+
return /^\/(approvals|reject|grants|grant)(?:\s|$)/u.test(body);
|
|
483
|
+
}
|
|
484
|
+
function operatorCommand(body) {
|
|
485
|
+
if (!isOperatorCommand(body)) return;
|
|
486
|
+
if (body.length > 2048) throw new Error("Command too long.");
|
|
487
|
+
let [surface, verb = "list", argument, ...extra] = body.trim().slice(1).split(/\s+/u);
|
|
488
|
+
if (surface === "reject") {
|
|
489
|
+
argument = verb;
|
|
490
|
+
verb = "reject";
|
|
491
|
+
surface = "approvals";
|
|
492
|
+
if (body.trim().split(/\s+/u).length !== 2) throw new Error("Invalid command.");
|
|
493
|
+
}
|
|
494
|
+
if (surface === "grant") {
|
|
495
|
+
if (body.trim().split(/\s+/u).length !== 2) throw new Error("Invalid command.");
|
|
496
|
+
const url = new URL(verb);
|
|
497
|
+
if (!["https:", "http:", "file:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("Invalid resource URL.");
|
|
498
|
+
return { surface: "grants", verb: "introduce", url: verb };
|
|
499
|
+
}
|
|
500
|
+
if (extra.length) throw new Error("Invalid command.");
|
|
501
|
+
if (verb === "list" && !argument) return { surface, verb };
|
|
502
|
+
if (surface === "grants" && verb === "revoke" && /^grant:[a-z0-9]{8}$/.test(argument ?? "")) return { surface, verb, handle: argument };
|
|
503
|
+
if (surface === "approvals" && ["preview", "apply", "reject", "revert", "grant", "reject-request"].includes(verb) && argument) {
|
|
504
|
+
if (argument !== "all" && !/^[1-9][0-9]*(?:,[1-9][0-9]*)*$/.test(argument)) throw new Error("Invalid action IDs.");
|
|
505
|
+
const ids = argument === "all" ? "all" : argument.split(",").map(Number);
|
|
506
|
+
if (ids !== "all" && (ids.length > 100 || ids.some((id) => !Number.isSafeInteger(id)) || new Set(ids).size !== ids.length)) throw new Error("Invalid action IDs.");
|
|
507
|
+
return { surface, verb, ids };
|
|
508
|
+
}
|
|
509
|
+
throw new Error("Use /approvals list|preview|apply|reject|revert [IDs or all], /reject IDs, /grants, or /grant URL.");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/upstream/lifecycle.ts
|
|
513
|
+
function enqueueRejectionNote(api, binding, id) {
|
|
514
|
+
return api.session.workflow.enqueueNextTurnInjection({ ...binding, text: `Action ${id} was rejected. Discard its simulated effects and re-read the resource before continuing.`, idempotencyKey: `clawos-reject-${id}`, placement: "prepend_context" });
|
|
515
|
+
}
|
|
516
|
+
function finishOperatorCommand(event, ctx, text2) {
|
|
517
|
+
const queuedFinal = text2.length > 0 && event.sendPolicy !== "deny" && !event.suppressUserDelivery ? ctx.dispatcher.sendFinalReply({ text: text2.slice(0, 8192) }) : false;
|
|
518
|
+
ctx.recordProcessed("completed", { reason: "clawos operator command" });
|
|
519
|
+
ctx.markIdle("clawos operator command");
|
|
520
|
+
return { handled: true, queuedFinal, counts: ctx.dispatcher.getQueuedCounts() };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/actions.ts
|
|
524
|
+
import { ActionDescriptionSchema } from "@clawkeepers/shared";
|
|
525
|
+
import { Value as Value3 } from "typebox/value";
|
|
526
|
+
var ActionCoordinator = class {
|
|
527
|
+
constructor(store, audit, cell2, resolve2, restart) {
|
|
528
|
+
this.store = store;
|
|
529
|
+
this.audit = audit;
|
|
530
|
+
this.cell = cell2;
|
|
531
|
+
this.resolve = resolve2;
|
|
532
|
+
this.restart = restart;
|
|
533
|
+
}
|
|
534
|
+
flights = /* @__PURE__ */ new Map();
|
|
535
|
+
stopped = false;
|
|
536
|
+
/** Explicit decisions are ordered and share the drainer's per-instance mutex. */
|
|
537
|
+
async decide(ids, verb, operator) {
|
|
538
|
+
const selected = ids === "all" ? this.store.listActions(false).filter((a) => a.status === (verb === "revert" ? "applied" : "pending")).map((a) => a.id) : [...ids].sort((a, b) => a - b);
|
|
539
|
+
for (const id of selected) {
|
|
540
|
+
const action = this.store.getAction(id);
|
|
541
|
+
if (!action) throw new Error("No such action.");
|
|
542
|
+
await this.serial(action.gatekeeperInstance, () => this.perform(id, verb, operator));
|
|
543
|
+
}
|
|
544
|
+
return { ids: selected };
|
|
545
|
+
}
|
|
546
|
+
/** Drain independent instances; never skip an ineligible or uncertain action. */
|
|
547
|
+
async drain(tags) {
|
|
548
|
+
if (this.stopped) return;
|
|
549
|
+
const instances = new Set(this.store.listActions().map((a) => a.gatekeeperInstance));
|
|
550
|
+
await Promise.allSettled([...instances].map((instance) => this.serial(instance, async () => {
|
|
551
|
+
for (const action of this.store.listActions(false).filter((a) => a.gatekeeperInstance === instance)) {
|
|
552
|
+
if (this.stopped || action.status === "failed") break;
|
|
553
|
+
if (action.status !== "pending") continue;
|
|
554
|
+
const d = description(action);
|
|
555
|
+
if (!d || d.autoApprovable !== true || d.awaitDecision || !d.actionKind || !tags.includes(d.actionKind.tag)) break;
|
|
556
|
+
try {
|
|
557
|
+
await this.perform(action.id, "apply", "auto-approval");
|
|
558
|
+
} catch {
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
})));
|
|
563
|
+
}
|
|
564
|
+
/** Outstanding serialized approval effects, used by the update admission barrier. */
|
|
565
|
+
get activeEffects() {
|
|
566
|
+
return this.flights.size;
|
|
567
|
+
}
|
|
568
|
+
/** Stop new work and wait for existing effects before SQLite is closed. */
|
|
569
|
+
async stop() {
|
|
570
|
+
this.stopped = true;
|
|
571
|
+
await Promise.allSettled([...this.flights.values()]);
|
|
572
|
+
}
|
|
573
|
+
serial(instance, task) {
|
|
574
|
+
if (this.stopped) return Promise.reject(new Error("Approvals unavailable."));
|
|
575
|
+
const result = (this.flights.get(instance) ?? Promise.resolve()).catch(() => {
|
|
576
|
+
}).then(task);
|
|
577
|
+
this.flights.set(instance, result);
|
|
578
|
+
void result.finally(() => {
|
|
579
|
+
if (this.flights.get(instance) === result) this.flights.delete(instance);
|
|
580
|
+
}).catch(() => {
|
|
581
|
+
});
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
async perform(id, verb, operator) {
|
|
585
|
+
const action = this.store.getAction(id), expected = verb === "revert" ? "applied" : "pending";
|
|
586
|
+
if (!action || action.status !== expected) throw new Error("Action is not eligible.");
|
|
587
|
+
if (verb === "apply" && this.store.listActions(false).some((a) => a.gatekeeperInstance === action.gatekeeperInstance && a.id < id && (a.status === "pending" || a.status === "failed"))) throw new Error("Earlier actions require a decision or reconciliation.");
|
|
588
|
+
const d = description(action);
|
|
589
|
+
if (!d || verb === "revert" && !d.implementsRevert) throw new Error("Action is not eligible.");
|
|
590
|
+
const authority = await this.resolve(action);
|
|
591
|
+
try {
|
|
592
|
+
if (verb === "revert" && !authority.gatekeeper.revertAction) throw new Error("Revert unavailable.");
|
|
593
|
+
authority.authorize();
|
|
594
|
+
if (!this.store.claimAction(id, expected, operator)) throw new Error("Action already decided.");
|
|
595
|
+
this.audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: this.cell, kind: "action.decide", actionId: id, title: "Action decision", decision: verb, by: operator, ok: true });
|
|
596
|
+
let restart = false;
|
|
597
|
+
try {
|
|
598
|
+
if (verb === "apply") await authority.gatekeeper.applyAction(action.actionId);
|
|
599
|
+
else if (verb === "reject") restart = (await authority.gatekeeper.rejectAction(action.actionId))?.restart === true;
|
|
600
|
+
else await authority.gatekeeper.revertAction(action.actionId);
|
|
601
|
+
this.store.decideAction(id, verb === "apply" ? "applied" : verb === "reject" ? "rejected" : "reverted", operator);
|
|
602
|
+
this.audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: this.cell, kind: verb === "revert" ? "action.revert" : verb === "apply" ? "action.apply" : "action.decide", actionId: id, title: "Action completed", decision: verb, by: operator, ok: true });
|
|
603
|
+
} catch {
|
|
604
|
+
this.audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: this.cell, kind: "action.decide", actionId: id, title: "Action outcome unconfirmed; automatic retry disabled", decision: "failed", by: operator, ok: false });
|
|
605
|
+
throw new Error("Action outcome unconfirmed; reconciliation required.");
|
|
606
|
+
}
|
|
607
|
+
if (restart) await this.restart(action).catch(() => {
|
|
608
|
+
});
|
|
609
|
+
} finally {
|
|
610
|
+
await authority.close().catch(() => {
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
function description(action) {
|
|
616
|
+
try {
|
|
617
|
+
const d = JSON.parse(action.descriptionJson);
|
|
618
|
+
return Value3.Check(ActionDescriptionSchema, d) ? d : null;
|
|
619
|
+
} catch {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/approvals.ts
|
|
625
|
+
import { ActionDescriptionSchema as ActionDescriptionSchema2 } from "@clawkeepers/shared";
|
|
626
|
+
import { Value as Value4 } from "typebox/value";
|
|
627
|
+
var ApprovalQueueImpl = class {
|
|
628
|
+
constructor(store, audit, cell2) {
|
|
629
|
+
this.store = store;
|
|
630
|
+
this.audit = audit;
|
|
631
|
+
this.cell = cell2;
|
|
632
|
+
}
|
|
633
|
+
forGrant(grant, sessionKey) {
|
|
634
|
+
const instance = instanceId(grant);
|
|
635
|
+
const authorize = () => {
|
|
636
|
+
if (!this.store.authorizeGrant(grant, grant.agentId, sessionKey, this.cell)) throw new Error("Grant is not active.");
|
|
637
|
+
};
|
|
638
|
+
return {
|
|
639
|
+
authorizeObservation: async (d) => {
|
|
640
|
+
authorize();
|
|
641
|
+
const observers = this.store.observers(sessionKey);
|
|
642
|
+
if (this.store.getInstance(instance)?.lockdown) throw new Error("Resource is locked down.");
|
|
643
|
+
if (observers.length && (grant.audience === "owner-only" || d.prohibitAllSharing || d.excludeObservers?.some((id) => observers.includes(id)))) {
|
|
644
|
+
if (d.prohibitAllSharing) {
|
|
645
|
+
this.store.lockdownInstance(instance);
|
|
646
|
+
this.store.setGrantStatus(grant.handle, "lockdown");
|
|
647
|
+
}
|
|
648
|
+
throw new Error("Observation denied for this audience.");
|
|
649
|
+
}
|
|
650
|
+
this.audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: this.cell, agentId: grant.agentId, sessionKey, kind: "observation", vendor: grant.vendor, resourceType: grant.resourceType, handle: grant.handle, title: d.title, ok: true });
|
|
651
|
+
},
|
|
652
|
+
submitAction: async (actionId, d) => {
|
|
653
|
+
authorize();
|
|
654
|
+
if (grant.audience === "owner-only" && this.store.observers(sessionKey).length) throw new Error("Action denied for this audience.");
|
|
655
|
+
if (this.store.getInstance(instance)?.lockdown) throw new Error("Resource is locked down.");
|
|
656
|
+
if (!Number.isSafeInteger(actionId) || actionId < 1 || !Value4.Check(ActionDescriptionSchema2, d)) throw new Error("Invalid action description.");
|
|
657
|
+
const action = this.store.addAction(instance, actionId, d);
|
|
658
|
+
this.store.bindAction(action.id, grant.handle, grant.agentId, sessionKey);
|
|
659
|
+
this.audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: this.cell, agentId: grant.agentId, sessionKey, kind: "action.submit", vendor: grant.vendor, resourceType: grant.resourceType, handle: grant.handle, actionId: action.id, title: d.title, ok: true });
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// src/upstream/channel-authority.ts
|
|
666
|
+
import { resolveCommandAuthorization } from "openclaw/plugin-sdk/command-auth";
|
|
667
|
+
import { resolveSessionAgentIdStrict } from "openclaw/plugin-sdk/agent-scope-runtime";
|
|
668
|
+
import { parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
|
669
|
+
function resolveChannelTurnAuthority(event, context) {
|
|
670
|
+
const inbound = event.ctx;
|
|
671
|
+
const channel = inbound.Provider;
|
|
672
|
+
const sessionKey = event.sessionKey;
|
|
673
|
+
if (context.dispatchKind !== "agent" || event.isTailDispatch || context.abortSignal?.aborted || !channel || inbound.InternalTurnSource || inbound.InputProvenance || typeof inbound.commandText !== "string" || !sessionKey || inbound.SessionKey !== sessionKey) return;
|
|
674
|
+
if (!inbound.AgentId?.trim() && !parseAgentSessionKey(sessionKey)?.agentId) return;
|
|
675
|
+
if (channel !== "webchat" && (inbound.GatewayClientScopes !== void 0 || !inbound.SenderId)) return;
|
|
676
|
+
try {
|
|
677
|
+
if (channel === "webchat") {
|
|
678
|
+
if (inbound.SenderId || !inbound.ApprovalReviewerDeviceId?.trim() || !inbound.GatewayClientScopes?.includes("operator.admin")) return;
|
|
679
|
+
const agentId2 = resolveSessionAgentIdStrict({ sessionKey, config: context.cfg, ...inbound.AgentId ? { agentId: inbound.AgentId } : {} });
|
|
680
|
+
if (inbound.AgentId && agentId2 !== inbound.AgentId) return;
|
|
681
|
+
return {
|
|
682
|
+
channel,
|
|
683
|
+
senderId: `gateway-device:${inbound.ApprovalReviewerDeviceId}`,
|
|
684
|
+
senderIsOwner: true,
|
|
685
|
+
privateAudience: inbound.ChatType !== "group" && inbound.ChatType !== "channel",
|
|
686
|
+
agentId: agentId2,
|
|
687
|
+
sessionKey,
|
|
688
|
+
text: inbound.commandText
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const authorization = resolveCommandAuthorization({
|
|
692
|
+
ctx: inbound,
|
|
693
|
+
cfg: context.cfg,
|
|
694
|
+
commandAuthorized: inbound.CommandAuthorized === true
|
|
695
|
+
});
|
|
696
|
+
const senderId = inbound.SenderId;
|
|
697
|
+
if (!senderId) return;
|
|
698
|
+
if (authorization.providerId !== channel || authorization.senderId !== senderId) return;
|
|
699
|
+
const agentId = resolveSessionAgentIdStrict({ sessionKey, config: context.cfg, ...inbound.AgentId ? { agentId: inbound.AgentId } : {} });
|
|
700
|
+
if (inbound.AgentId && agentId !== inbound.AgentId) return;
|
|
701
|
+
return {
|
|
702
|
+
channel,
|
|
703
|
+
senderId,
|
|
704
|
+
senderIsOwner: authorization.senderIsOwner === true,
|
|
705
|
+
privateAudience: inbound.ChatType === "direct",
|
|
706
|
+
agentId,
|
|
707
|
+
sessionKey,
|
|
708
|
+
text: inbound.commandText
|
|
709
|
+
};
|
|
710
|
+
} catch {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/upstream/runtime-version.ts
|
|
716
|
+
function runtimeVersion(api) {
|
|
717
|
+
return api.runtime?.version ?? "unavailable";
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/upstream/paths.ts
|
|
721
|
+
import { homedir as homedir2 } from "os";
|
|
722
|
+
import { join as join3 } from "path";
|
|
723
|
+
function stateDir() {
|
|
724
|
+
if (process.env.OPENCLAW_STATE_DIR) return process.env.OPENCLAW_STATE_DIR;
|
|
725
|
+
const profile = process.env.OPENCLAW_PROFILE;
|
|
726
|
+
return join3(process.env.OPENCLAW_HOME ?? homedir2(), profile ? `.openclaw-${profile}` : ".openclaw");
|
|
727
|
+
}
|
|
728
|
+
function osPaths() {
|
|
729
|
+
const os = join3(stateDir(), "os");
|
|
730
|
+
return {
|
|
731
|
+
stateDir: stateDir(),
|
|
732
|
+
os,
|
|
733
|
+
sqlite: join3(os, "clawos.sqlite"),
|
|
734
|
+
auditDir: join3(os, "audit"),
|
|
735
|
+
configD: join3(os, "config.d"),
|
|
736
|
+
lock: join3(os, "clawos.lock.json"),
|
|
737
|
+
gatekeepers: join3(os, "gatekeepers"),
|
|
738
|
+
cellKey: join3(os, "cell.key"),
|
|
739
|
+
logs: join3(os, "logs")
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// src/kernel.ts
|
|
744
|
+
var slot = createPluginRuntimeStore({ pluginId: "clawos-kernel", errorMessage: "Kernel unavailable." });
|
|
745
|
+
var text = (value) => ({ content: [{ type: "text", text: value }], details: {} });
|
|
746
|
+
var kernelVersion = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
747
|
+
var EmptyParams = Type.Object({}, { additionalProperties: false });
|
|
748
|
+
var ApprovalListParams = Type.Object({ includeDecided: Type.Optional(Type.Boolean()) }, { additionalProperties: false });
|
|
749
|
+
var AuditQueryParams = Type.Object({ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1e3 })) }, { additionalProperties: false });
|
|
750
|
+
var Kernel = class {
|
|
751
|
+
constructor(api) {
|
|
752
|
+
this.api = api;
|
|
753
|
+
}
|
|
754
|
+
paths = osPaths();
|
|
755
|
+
activeRuns = /* @__PURE__ */ new Set();
|
|
756
|
+
unknownRuns = 0;
|
|
757
|
+
maintenance() {
|
|
758
|
+
return this.config().maintenance === true || existsSync(join4(this.paths.os, "update-maintenance.json"));
|
|
759
|
+
}
|
|
760
|
+
/** Operator-only persistent admission barrier; a crash never silently reopens the cell. */
|
|
761
|
+
setMaintenance(enabled) {
|
|
762
|
+
const path = join4(this.paths.os, "update-maintenance.json");
|
|
763
|
+
if (enabled) {
|
|
764
|
+
writeFileSync(path + ".tmp", JSON.stringify({ maintenance: true }), { mode: 384 });
|
|
765
|
+
renameSync(path + ".tmp", path);
|
|
766
|
+
} else if (existsSync(path)) unlinkSync(path);
|
|
767
|
+
return this.status();
|
|
768
|
+
}
|
|
769
|
+
catalog = new Registry(process.env.CLAWOS_GATEKEEPER_CATALOG ?? join4(this.paths.os, "gatekeepers.json"), this.paths.stateDir);
|
|
770
|
+
async start() {
|
|
771
|
+
if (slot.tryGetRuntime()) throw new Error("Kernel runtime already started.");
|
|
772
|
+
const pin = existsSync(this.paths.lock) ? JSON.parse(readFileSync4(this.paths.lock, "utf8")) : void 0;
|
|
773
|
+
if (pin && pin.kernelSchema !== 1) throw new Error("Kernel schema pin incompatible.");
|
|
774
|
+
const store = new Store(this.paths.sqlite);
|
|
775
|
+
try {
|
|
776
|
+
store.migrate();
|
|
777
|
+
} catch (error) {
|
|
778
|
+
store.close();
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
const audit = new AuditLog(this.paths.auditDir);
|
|
782
|
+
slot.setRuntime({ store, registry: this.catalog, audit, oauth: new OAuthRouter(this.catalog), approvals: new ApprovalQueueImpl(store, audit, cell()), actions: new ActionCoordinator(store, audit, cell(), async (action) => {
|
|
783
|
+
const binding = store.actionBinding(action.id);
|
|
784
|
+
if (!binding) throw new Error("Action has no authority.");
|
|
785
|
+
const resolved = await this.resolveGrant(binding.agentId, binding.sessionKey, binding.handle);
|
|
786
|
+
return { gatekeeper: resolved.gatekeeper, authorize: () => {
|
|
787
|
+
if (this.maintenance() || !store.authorizeGrant(resolved.grant, binding.agentId, binding.sessionKey, cell()) || store.observers(binding.sessionKey).length || store.getInstance(action.gatekeeperInstance)?.lockdown || instanceId(resolved.grant) !== action.gatekeeperInstance) throw new Error("Action authority changed.");
|
|
788
|
+
}, close: async () => {
|
|
789
|
+
this.runtime().sessions.get(binding.sessionKey)?.delete(resolved.session);
|
|
790
|
+
await resolved.session.close();
|
|
791
|
+
} };
|
|
792
|
+
}, async (action) => {
|
|
793
|
+
const b = store.actionBinding(action.id);
|
|
794
|
+
if (b) await enqueueRejectionNote(this.api, b, action.id);
|
|
795
|
+
}), stash: /* @__PURE__ */ new Map(), inflight: /* @__PURE__ */ new Map(), notes: /* @__PURE__ */ new Map(), sessions: /* @__PURE__ */ new Map() });
|
|
796
|
+
this.startDrainer();
|
|
797
|
+
}
|
|
798
|
+
async stop() {
|
|
799
|
+
const r = slot.tryGetRuntime();
|
|
800
|
+
if (!r) return;
|
|
801
|
+
this.stopDrainer();
|
|
802
|
+
r.oauth.clear();
|
|
803
|
+
await r.actions.stop();
|
|
804
|
+
for (const sessions of r.sessions.values()) for (const session of sessions.keys()) await session.close().catch(() => {
|
|
805
|
+
});
|
|
806
|
+
await r.audit.flush();
|
|
807
|
+
r.store.close();
|
|
808
|
+
if (slot.tryGetRuntime() === r) slot.clearRuntime();
|
|
809
|
+
}
|
|
810
|
+
runtime() {
|
|
811
|
+
return slot.getRuntime();
|
|
812
|
+
}
|
|
813
|
+
config() {
|
|
814
|
+
return this.api.pluginConfig ?? {};
|
|
815
|
+
}
|
|
816
|
+
operator(channel, sender) {
|
|
817
|
+
return !!channel && !!sender && this.config().operators?.some((x) => x.channel === channel && x.senderId === sender) === true;
|
|
818
|
+
}
|
|
819
|
+
async resolveGrant(agentId, sessionKey, handle) {
|
|
820
|
+
const r = this.runtime(), candidate = r.store.getGrant(handle), grant = candidate && r.store.authorizeGrant(candidate, agentId, sessionKey, cell());
|
|
821
|
+
if (!grant || r.store.getInstance(instanceId(grant))?.lockdown) throw new Error("No such grant.");
|
|
822
|
+
if (grant.audience === "owner-only" && r.store.observers(sessionKey).length) throw new Error("Grant unavailable for this audience.");
|
|
823
|
+
const queue = r.approvals.forGrant(grant, sessionKey);
|
|
824
|
+
const opened = { ...await r.registry.openSession(grant, queue), queue };
|
|
825
|
+
r.store.upsertInstance({ id: opened.instanceId, vendor: grant.vendor, resourceKey: grant.resourceKey, operatorId: grant.operatorId, observerStrategy: r.registry.entries.get(grant.vendor)?.resources.find((x) => x.type === grant.resourceType)?.observerStrategy ?? "private-only", lockdown: 0 });
|
|
826
|
+
let sessions = r.sessions.get(sessionKey);
|
|
827
|
+
if (!sessions) {
|
|
828
|
+
sessions = /* @__PURE__ */ new Map();
|
|
829
|
+
r.sessions.set(sessionKey, sessions);
|
|
830
|
+
}
|
|
831
|
+
sessions.set(opened.session, grant.handle);
|
|
832
|
+
return { grant, ...opened };
|
|
833
|
+
}
|
|
834
|
+
capabilityPolicy() {
|
|
835
|
+
return { id: "clawos-capability-policy", description: "Deny gatekeeper calls without an active capability.", ...this.catalog.toolNames().length ? { matcher: this.catalog.toolNames() } : {}, evaluate: (event, ctx) => {
|
|
836
|
+
if (!event.toolName.startsWith("gk_")) return;
|
|
837
|
+
const r = slot.tryGetRuntime();
|
|
838
|
+
const grant = typeof event.params.grant === "string" ? r?.store.getGrant(event.params.grant) : null;
|
|
839
|
+
if (r && grant && ctx.agentId && ctx.sessionKey && r.store.authorizeGrant(grant, ctx.agentId, ctx.sessionKey, cell()) && (grant.audience === "shared" || !r.store.observers(ctx.sessionKey).length)) return;
|
|
840
|
+
r?.audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: cell(), ...ctx.agentId ? { agentId: ctx.agentId } : {}, ...ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {}, kind: "tool", title: "Capability policy denied call", decision: "deny", ok: false });
|
|
841
|
+
return { block: true, blockReason: "No such grant." };
|
|
842
|
+
} };
|
|
843
|
+
}
|
|
844
|
+
registerGatekeeperTools(api) {
|
|
845
|
+
for (const d of this.catalog.tools) api.registerTool({ name: d.name, label: d.name, description: d.description, parameters: d.parameters, execute: async (id, p) => toolResult(await this.exec(id, d.name, p)) });
|
|
846
|
+
}
|
|
847
|
+
async exec(id, tool, params) {
|
|
848
|
+
const r = this.runtime(), s = r.stash.get(id);
|
|
849
|
+
r.stash.delete(id);
|
|
850
|
+
if (!s || s.expiresAt < Date.now() || !s.session || !s.queue || s.tool !== tool || s.paramsJson !== JSON.stringify(params) || !s.grant || !r.store.authorizeGrant(s.grant, s.agentId, s.sessionKey, cell()) || s.grant.audience === "owner-only" && r.store.observers(s.sessionKey).length) throw new Error("Operation denied.");
|
|
851
|
+
r.inflight.set(id, s);
|
|
852
|
+
try {
|
|
853
|
+
const approved = r.store.consumeToolApproval(id, tool, params);
|
|
854
|
+
return await s.session.call(tool, params, { agentId: s.agentId, sessionKey: s.sessionKey, ...s.runId ? { runId: s.runId } : {}, toolCallId: id, queue: s.queue, observers: r.store.observers(s.sessionKey), ...approved ? { actionApproval: { toolCallId: id, tool, params: structuredClone(params) } } : {} });
|
|
855
|
+
} catch {
|
|
856
|
+
throw new Error("Operation denied.");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/** Admit operator URLs before prompt construction, without taking over reply dispatch. */
|
|
860
|
+
async onReplyDispatch(e, ctx) {
|
|
861
|
+
if (this.maintenance()) return;
|
|
862
|
+
const turn = resolveChannelTurnAuthority(e, ctx);
|
|
863
|
+
if (!turn) {
|
|
864
|
+
if (ctx.dispatchKind === "agent" && !e.isTailDispatch && !ctx.abortSignal?.aborted && typeof e.ctx?.commandText === "string" && isOperatorCommand(e.ctx.commandText)) return finishOperatorCommand(e, ctx, "");
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const command = isOperatorCommand(turn.text);
|
|
868
|
+
const silence = () => command ? finishOperatorCommand(e, ctx, "") : void 0;
|
|
869
|
+
if (!turn.privateAudience) {
|
|
870
|
+
this.runtime().store.taintObserver(turn.sessionKey, "clawos:shared-audience");
|
|
871
|
+
this.runtime().notes.delete(turn.sessionKey);
|
|
872
|
+
return silence();
|
|
873
|
+
}
|
|
874
|
+
if (!turn.senderIsOwner) {
|
|
875
|
+
const r = this.runtime(), observers = new Set(r.store.observers(turn.sessionKey));
|
|
876
|
+
observers.add(turn.senderId);
|
|
877
|
+
r.store.setObservers(turn.sessionKey, [...observers]);
|
|
878
|
+
return silence();
|
|
879
|
+
}
|
|
880
|
+
if (turn.channel !== "webchat" && !this.operator(turn.channel, turn.senderId)) return silence();
|
|
881
|
+
if (this.runtime().store.observers(turn.sessionKey).length) return silence();
|
|
882
|
+
if (command) {
|
|
883
|
+
let reply = "Operation denied.";
|
|
884
|
+
try {
|
|
885
|
+
const c = operatorCommand(turn.text);
|
|
886
|
+
const r = this.runtime();
|
|
887
|
+
const result = c.surface === "grants" ? c.verb === "list" ? r.store.allGrants(turn.agentId) : c.verb === "introduce" ? await this.introduce(turn.agentId, turn.sessionKey, c.url, turn.senderId, "operator") : await this.revoke(c.handle, turn.senderId) : c.verb === "list" ? this.pending() : c.verb === "preview" ? this.preview(c.ids) : c.verb === "grant" || c.verb === "reject-request" ? await this.decideRequests({ ids: c.ids }, c.verb === "grant", turn.senderId) : await this.decide({ ids: c.ids }, c.verb, turn.senderId);
|
|
888
|
+
reply = JSON.stringify(result);
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
return finishOperatorCommand(e, ctx, reply);
|
|
892
|
+
}
|
|
893
|
+
for (const url of urls(turn.text)) await this.introduce(turn.agentId, turn.sessionKey, url, turn.senderId, "operator").catch(() => {
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
async onBeforeAgentRun(e, ctx) {
|
|
897
|
+
if (this.maintenance()) return { outcome: "block", reason: "maintenance", message: "OpenClaw OS is being maintained." };
|
|
898
|
+
if (ctx.runId) this.activeRuns.add(ctx.runId);
|
|
899
|
+
else this.unknownRuns++;
|
|
900
|
+
if (ctx.sessionKey && e.senderId && !e.senderIsOwner) {
|
|
901
|
+
const r = this.runtime(), observers = new Set(r.store.observers(ctx.sessionKey));
|
|
902
|
+
observers.add(e.senderId);
|
|
903
|
+
r.store.setObservers(ctx.sessionKey, [...observers]);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
/** Keep operator-authorized native tools in the narrowing cap; upstream policy still intersects it. Never pass through group:plugins or ungranted gk tools. */
|
|
907
|
+
async onBeforePromptBuild(_e, ctx) {
|
|
908
|
+
const r = slot.tryGetRuntime();
|
|
909
|
+
if (!r || !ctx.agentId || !ctx.sessionKey) return { toolsAllow: ["os_request_access", "os_list_grants"] };
|
|
910
|
+
const sessionKey = ctx.sessionKey;
|
|
911
|
+
const grants = r.store.listGrants(ctx.agentId).filter((g) => r.store.authorizeGrant(g, ctx.agentId, sessionKey, cell()) && (g.scope === "agent" || g.scope === `session:${sessionKey}`) && (g.audience === "shared" || !r.store.observers(sessionKey).length));
|
|
912
|
+
const allowed = /* @__PURE__ */ new Set(["group:openclaw", "group:fs", "group:runtime", "os_request_access", "os_list_grants"]);
|
|
913
|
+
for (const g of grants) for (const t of r.registry.entries.get(g.vendor)?.tools ?? []) if (t.resourceType === g.resourceType) allowed.add(t.name);
|
|
914
|
+
const rows = grants.slice(0, 100).map((g) => `${g.handle} ${g.vendor}/${g.resourceType}${g.title ? ` ${g.title}` : ""}`);
|
|
915
|
+
const notes = r.store.observers(sessionKey).length ? [] : r.notes.get(sessionKey) ?? [];
|
|
916
|
+
r.notes.delete(sessionKey);
|
|
917
|
+
const context = [...notes, rows.length ? `Available grants:
|
|
918
|
+
${rows.join("\n")}` : ""].filter(Boolean).join("\n").slice(0, 8192);
|
|
919
|
+
return { toolsAllow: [...allowed], ...context ? { appendContext: context } : {} };
|
|
920
|
+
}
|
|
921
|
+
async onBeforeToolCall(e, ctx) {
|
|
922
|
+
if (!e.toolName.startsWith("gk_") && !e.toolName.startsWith("os_")) return;
|
|
923
|
+
const id = e.toolCallId;
|
|
924
|
+
if (!id || !ctx.agentId || !ctx.sessionKey) return { block: true, blockReason: "Missing call identity." };
|
|
925
|
+
const r = this.runtime();
|
|
926
|
+
for (const [key, value] of r.stash) if (value.expiresAt < Date.now()) r.stash.delete(key);
|
|
927
|
+
const base = { agentId: ctx.agentId, sessionKey: ctx.sessionKey, ...ctx.runId ? { runId: ctx.runId } : {}, expiresAt: Date.now() + 18e5, startedAt: Date.now() };
|
|
928
|
+
if (e.toolName.startsWith("os_")) {
|
|
929
|
+
r.stash.set(id, { ...base, tool: e.toolName, paramsJson: JSON.stringify(e.params) });
|
|
930
|
+
return {};
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
const params = e.params;
|
|
934
|
+
if (typeof params.grant !== "string") throw new Error();
|
|
935
|
+
const resolved = await this.resolveGrant(ctx.agentId, ctx.sessionKey, params.grant);
|
|
936
|
+
const dry = await resolved.session.call(e.toolName, params, { agentId: ctx.agentId, sessionKey: ctx.sessionKey, ...ctx.runId ? { runId: ctx.runId } : {}, toolCallId: id, queue: resolved.queue, observers: r.store.observers(ctx.sessionKey), dryRun: true });
|
|
937
|
+
r.stash.set(id, { ...base, session: resolved.session, queue: resolved.queue, grant: resolved.grant, tool: e.toolName, paramsJson: JSON.stringify(params) });
|
|
938
|
+
if (dry.kind === "action" && dry.description.awaitDecision) return { requireApproval: { title: dry.description.title, description: dry.description.description, severity: "critical", allowedDecisions: ["allow-once", "deny"], onResolution: (decision) => r.store.recordToolDecision(id, e.toolName, params, decision) } };
|
|
939
|
+
return {};
|
|
940
|
+
} catch {
|
|
941
|
+
return { block: true, blockReason: "No such grant." };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
async onAfterToolCall(e, _ctx) {
|
|
945
|
+
if (!e.toolCallId) return;
|
|
946
|
+
const r = slot.tryGetRuntime(), s = r?.inflight.get(e.toolCallId) ?? r?.stash.get(e.toolCallId);
|
|
947
|
+
if (!r || !s) return;
|
|
948
|
+
r.stash.delete(e.toolCallId);
|
|
949
|
+
r.inflight.delete(e.toolCallId);
|
|
950
|
+
r.audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: cell(), agentId: s.agentId, sessionKey: s.sessionKey, kind: "tool", ...s.grant ? { vendor: s.grant.vendor, resourceType: s.grant.resourceType, handle: s.grant.handle } : {}, title: s.tool ?? "kernel tool", durationMs: Date.now() - s.startedAt, ok: !e.error });
|
|
951
|
+
}
|
|
952
|
+
async requestAccess(id, p) {
|
|
953
|
+
const r = this.runtime(), s = this.consumeStash(id, "os_request_access", p);
|
|
954
|
+
const url = new URL(p.url);
|
|
955
|
+
if (p.url.length > 4096 || p.reason.length > 4096 || url.username || url.password || url.search || url.hash) throw new Error("Invalid resource request.");
|
|
956
|
+
r.store.addIntroduction({ agentId: s.agentId, sessionKey: s.sessionKey, url: p.url, reason: p.reason, requestedBy: "agent" });
|
|
957
|
+
return text("Access requested; you will be told when it is granted.");
|
|
958
|
+
}
|
|
959
|
+
async listGrantsForCall(id) {
|
|
960
|
+
const r = this.runtime(), s = this.consumeStash(id, "os_list_grants", {});
|
|
961
|
+
return text(JSON.stringify(r.store.listGrants(s.agentId).filter((g) => r.store.authorizeGrant(g, s.agentId, s.sessionKey, cell()) && (g.audience === "shared" || !r.store.observers(s.sessionKey).length)).map((g) => ({ handle: g.handle, vendor: g.vendor, type: g.resourceType, title: g.title }))));
|
|
962
|
+
}
|
|
963
|
+
consumeStash(id, tool, params) {
|
|
964
|
+
const r = this.runtime(), s = r.stash.get(id);
|
|
965
|
+
r.stash.delete(id);
|
|
966
|
+
if (!s || s.tool !== tool || s.expiresAt < Date.now() || s.paramsJson !== JSON.stringify(params)) throw new Error("Operation denied.");
|
|
967
|
+
r.inflight.set(id, s);
|
|
968
|
+
return s;
|
|
969
|
+
}
|
|
970
|
+
async introduce(agentId, sessionKey, url, operatorId, createdBy, title, audience = "owner-only") {
|
|
971
|
+
if (!agentId || !sessionKey || audience !== "owner-only") throw new Error("Shared grants are unavailable in beta.");
|
|
972
|
+
const r = this.runtime();
|
|
973
|
+
for (const { entry, resource } of r.registry.resources()) {
|
|
974
|
+
if (!matches(resource.urlPattern, url)) continue;
|
|
975
|
+
const resolved = await r.registry.introduce(entry.vendor, operatorId, url);
|
|
976
|
+
const grant = r.store.insertGrant({ handle: newHandle(), agentId, cellId: cell(), vendor: entry.vendor, resourceType: resolved.resource.type, resourceKey: resolved.resourceKey, operatorId, scope: "agent", audience, status: "active", createdAt: Date.now(), createdBy, title: title ?? resolved.resource.title });
|
|
977
|
+
r.audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: cell(), agentId, sessionKey, kind: "grant", vendor: grant.vendor, resourceType: grant.resourceType, handle: grant.handle, title: "Grant activated", decision: "active", by: operatorId, ok: true });
|
|
978
|
+
r.notes.set(sessionKey, [...r.notes.get(sessionKey) ?? [], `You now have access to ${grant.vendor} ${grant.resourceType} as ${grant.handle}.`]);
|
|
979
|
+
return grant;
|
|
980
|
+
}
|
|
981
|
+
throw new Error();
|
|
982
|
+
}
|
|
983
|
+
gatewayMethods() {
|
|
984
|
+
const wrap = (schema2, scopes, fn) => async (o) => {
|
|
985
|
+
try {
|
|
986
|
+
const params = checked(schema2, o.params ?? {}), operator = operatorIdentity(o, scopes);
|
|
987
|
+
if (this.maintenance() && (scopes === write || scopes === approvals)) throw new Error();
|
|
988
|
+
o.respond(true, await fn(params, operator));
|
|
989
|
+
} catch {
|
|
990
|
+
o.respond(false, void 0, { code: "UNAUTHORIZED", message: "Operator authorization required." });
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
const read = ["operator.read", "operator.write", "operator.admin"], write = ["operator.write", "operator.admin"], approvals = ["operator.approvals", "operator.admin"];
|
|
994
|
+
return [["os.maintenance.set", wrap(Type.Object({ enabled: Type.Boolean() }, { additionalProperties: false }), ["operator.admin"], (p) => this.setMaintenance(p.enabled))], ["os.status", wrap(EmptyParams, read, () => this.status())], ["os.grants.list", wrap(ListGrantsParams, read, (p) => this.runtime().store.allGrants(p.agentId))], ["os.grants.introduce", wrap(IntroduceParams, write, (p, op) => this.introduce(p.agentId, `rpc:${p.agentId}`, p.url, op, "operator", p.title, p.audience))], ["os.grants.revoke", wrap(RevokeGrantParams, write, (p, op) => this.revoke(p.handle, op))], ["os.approvals.list", wrap(ApprovalListParams, read, (p) => this.pending(p.includeDecided))], ["os.approvals.apply", wrap(ApprovalDecisionParams, approvals, (p, op) => this.decide(p, "apply", op))], ["os.approvals.reject", wrap(ApprovalDecisionParams, approvals, (p, op) => this.decide(p, "reject", op))], ["os.approvals.revert", wrap(ApprovalDecisionParams, approvals, (p, op) => this.decide(p, "revert", op))], ["os.requests.approve", wrap(ApprovalDecisionParams, approvals, (p, op) => this.decideRequests(p, true, op))], ["os.requests.reject", wrap(ApprovalDecisionParams, approvals, (p, op) => this.decideRequests(p, false, op))], ["os.gatekeepers.list", wrap(EmptyParams, read, () => this.runtime().registry.health())], ["os.gatekeepers.connect", wrap(ConnectGatekeeperParams, write, (p, op) => this.runtime().oauth.connect(p.vendor, op, p.resourceTypes))], ["os.audit.query", wrap(AuditQueryParams, read, (p) => this.runtime().audit.query(p.limit))]];
|
|
995
|
+
}
|
|
996
|
+
/** Revoke authority before awaiting cleanup; no retained session may continue using it. */
|
|
997
|
+
async revoke(handle, operator) {
|
|
998
|
+
const r = this.runtime(), grant = r.store.getGrant(handle);
|
|
999
|
+
if (!grant) throw new Error();
|
|
1000
|
+
r.store.setGrantStatus(handle, "revoked");
|
|
1001
|
+
for (const [id, call] of r.stash) if (call.grant?.handle === handle) r.stash.delete(id);
|
|
1002
|
+
for (const sessions of r.sessions.values()) for (const [session, boundHandle] of sessions) if (boundHandle === handle) {
|
|
1003
|
+
sessions.delete(session);
|
|
1004
|
+
await session.close().catch(() => {
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
r.audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: cell(), agentId: grant.agentId, kind: "grant", handle, title: "Grant revoked", decision: "revoked", by: operator, ok: true });
|
|
1008
|
+
return { revoked: true };
|
|
1009
|
+
}
|
|
1010
|
+
pending(includeDecided = false) {
|
|
1011
|
+
const r = this.runtime();
|
|
1012
|
+
const actions = r.store.listActions(!includeDecided);
|
|
1013
|
+
return { actions: actions.slice(0, 100), requests: r.store.listIntroductions(), truncated: actions.length > 100 };
|
|
1014
|
+
}
|
|
1015
|
+
preview(ids) {
|
|
1016
|
+
const rows = this.runtime().store.listActions(false);
|
|
1017
|
+
const selected = ids === "all" ? rows : ids.map((id) => {
|
|
1018
|
+
const row = rows.find((a) => a.id === id);
|
|
1019
|
+
if (!row) throw new Error("No such action.");
|
|
1020
|
+
return row;
|
|
1021
|
+
});
|
|
1022
|
+
return { actions: selected.slice(0, 100), truncated: selected.length > 100 };
|
|
1023
|
+
}
|
|
1024
|
+
async decideRequests(params, allow, operator) {
|
|
1025
|
+
const r = this.runtime(), pending = r.store.listIntroductions(), ids = params.ids === "all" ? pending.map((p) => p.id) : [...params.ids].sort((a, b) => a - b);
|
|
1026
|
+
for (const id of ids) {
|
|
1027
|
+
const request = pending.find((p) => p.id === id);
|
|
1028
|
+
if (!request || !r.store.claimIntroduction(id)) throw new Error("Request unavailable.");
|
|
1029
|
+
try {
|
|
1030
|
+
if (allow) {
|
|
1031
|
+
if (!request.sessionKey || r.store.observers(request.sessionKey).length) throw new Error();
|
|
1032
|
+
await this.introduce(request.agentId, request.sessionKey, request.url, operator, "agent-request");
|
|
1033
|
+
}
|
|
1034
|
+
r.store.finishIntroduction(id, allow ? "granted" : "rejected");
|
|
1035
|
+
} catch {
|
|
1036
|
+
r.store.finishIntroduction(id, "failed");
|
|
1037
|
+
throw new Error("Request could not be resolved.");
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return { ids };
|
|
1041
|
+
}
|
|
1042
|
+
async decide(params, verb, operator) {
|
|
1043
|
+
const r = this.runtime();
|
|
1044
|
+
const result = await r.actions.decide(params.ids, verb, operator);
|
|
1045
|
+
await r.actions.drain(this.maintenance() ? [] : this.config().autoApprove ?? []);
|
|
1046
|
+
return result;
|
|
1047
|
+
}
|
|
1048
|
+
async oauthRouter(req, res) {
|
|
1049
|
+
const r = slot.tryGetRuntime();
|
|
1050
|
+
if (!r) {
|
|
1051
|
+
res.statusCode = 503;
|
|
1052
|
+
res.end("Kernel unavailable.");
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
return r.oauth.handle(req, res);
|
|
1056
|
+
}
|
|
1057
|
+
/** Start one shared-runtime timer; lifecycle order does not grant authority. */
|
|
1058
|
+
startDrainer(_ctx) {
|
|
1059
|
+
const r = slot.tryGetRuntime();
|
|
1060
|
+
if (!r || r.timer) return;
|
|
1061
|
+
r.timer = setInterval(() => {
|
|
1062
|
+
void r.actions.drain(this.maintenance() ? [] : this.config().autoApprove ?? []);
|
|
1063
|
+
}, 3e4);
|
|
1064
|
+
r.timer.unref();
|
|
1065
|
+
}
|
|
1066
|
+
/** Stop timer immediately; stop() subsequently awaits in-flight effects. */
|
|
1067
|
+
stopDrainer() {
|
|
1068
|
+
const r = slot.tryGetRuntime();
|
|
1069
|
+
if (r?.timer) {
|
|
1070
|
+
clearInterval(r.timer);
|
|
1071
|
+
delete r.timer;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
/** This late hook carries no trustworthy owner authority; only deny command fallthrough here. */
|
|
1075
|
+
async onBeforeAgentReply(e, _ctx) {
|
|
1076
|
+
if (isOperatorCommand(e.cleanedBody)) return { handled: true, reply: { text: "" }, reason: "clawos command requires trusted dispatch" };
|
|
1077
|
+
}
|
|
1078
|
+
async onMessageSending(e, ctx) {
|
|
1079
|
+
for (const source of this.config().egress?.denyPatterns ?? []) {
|
|
1080
|
+
let re;
|
|
1081
|
+
try {
|
|
1082
|
+
re = new RegExp(source, "iu");
|
|
1083
|
+
} catch {
|
|
1084
|
+
return { cancel: true, cancelReason: "Invalid egress policy." };
|
|
1085
|
+
}
|
|
1086
|
+
if (re.test(typeof e.content === "string" ? e.content : "")) {
|
|
1087
|
+
this.runtime().audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: cell(), ...ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {}, kind: "egress", title: "Outbound message blocked", decision: "deny", ok: false });
|
|
1088
|
+
return { cancel: true, cancelReason: "Outbound policy denied this message." };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (ctx.sessionKey) for (const id of this.runtime().store.observers(ctx.sessionKey)) this.runtime().store.taintObserver(ctx.sessionKey, id);
|
|
1092
|
+
}
|
|
1093
|
+
async onBeforeInstall(e, _ctx) {
|
|
1094
|
+
const verdict = evaluateInstall(this.config().install, e);
|
|
1095
|
+
return verdict.decision === "allow" ? void 0 : { block: true, blockReason: verdict.reason };
|
|
1096
|
+
}
|
|
1097
|
+
async onAgentEnd(_e, ctx) {
|
|
1098
|
+
if (ctx.runId) this.activeRuns.delete(ctx.runId);
|
|
1099
|
+
const r = slot.tryGetRuntime();
|
|
1100
|
+
if (!r) return;
|
|
1101
|
+
await r.actions.drain(this.maintenance() ? [] : this.config().autoApprove ?? []);
|
|
1102
|
+
const notify = this.config().notify, actions = r.store.countPending(), requests = r.store.countPendingRequests();
|
|
1103
|
+
if (!notify || !ctx.runId || !(actions + requests) || !r.store.claimNotification(ctx.runId)) return;
|
|
1104
|
+
try {
|
|
1105
|
+
await sendOperatorDigest(notify, actions, requests);
|
|
1106
|
+
} catch {
|
|
1107
|
+
r.audit.write({ ts: (/* @__PURE__ */ new Date()).toISOString(), cell: cell(), kind: "tool", title: "Operator digest delivery unconfirmed", ok: false });
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async onSessionEnd(_e, ctx) {
|
|
1111
|
+
if (!ctx.sessionKey) return;
|
|
1112
|
+
const r = slot.tryGetRuntime(), sessions = r?.sessions.get(ctx.sessionKey);
|
|
1113
|
+
if (!sessions) return;
|
|
1114
|
+
for (const session of sessions.keys()) await session.close().catch(() => {
|
|
1115
|
+
});
|
|
1116
|
+
r.sessions.delete(ctx.sessionKey);
|
|
1117
|
+
}
|
|
1118
|
+
async status() {
|
|
1119
|
+
const r = this.runtime();
|
|
1120
|
+
return { cell: cell(), upstreamVersion: runtimeVersion(this.api), kernelVersion, healthy: true, kernelSchema: 1, activeRuns: this.activeRuns.size + this.unknownRuns, activeEffects: r.actions.activeEffects, activeRunTrackingComplete: this.unknownRuns === 0, maintenance: this.maintenance() === true, gatekeepers: r.registry.health(), pendingApprovals: r.store.countPending(), pendingRequests: r.store.countPendingRequests() };
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
function toolResult(result) {
|
|
1124
|
+
if ("content" in result) return { content: result.content, details: result.details ?? {} };
|
|
1125
|
+
return text(JSON.stringify(result));
|
|
1126
|
+
}
|
|
1127
|
+
function checked(schema2, value) {
|
|
1128
|
+
if (!Value5.Check(schema2, value)) throw new Error();
|
|
1129
|
+
return value;
|
|
1130
|
+
}
|
|
1131
|
+
function operatorIdentity(o, allowedScopes) {
|
|
1132
|
+
const c = o.client;
|
|
1133
|
+
if (!c || c.connect?.role !== "operator" || !Array.isArray(c.connect.scopes) || !c.connect.scopes.some((scope) => allowedScopes.includes(scope)) || !c.connect.device?.id || c.isDeviceTokenAuth !== true) throw new Error();
|
|
1134
|
+
return c.connect.device.id;
|
|
1135
|
+
}
|
|
1136
|
+
function cell() {
|
|
1137
|
+
return process.env.CLAWOS_CELL ?? "default";
|
|
1138
|
+
}
|
|
1139
|
+
function urls(v) {
|
|
1140
|
+
return v.match(/(?:https?:\/\/|file:\/\/\/)[^\s<>"']+/gu) ?? [];
|
|
1141
|
+
}
|
|
1142
|
+
function matches(pattern, url) {
|
|
1143
|
+
if (pattern.startsWith("file:///")) return url.startsWith("file:///");
|
|
1144
|
+
try {
|
|
1145
|
+
return new URL(url).protocol === new URL(pattern.replace(/:[a-z][a-z0-9_]*[+]?/gi, "x")).protocol;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function newHandle() {
|
|
1151
|
+
const a = "0123456789abcdefghjkmnpqrstvwxyz", b = randomBytes(8);
|
|
1152
|
+
let out = "grant:";
|
|
1153
|
+
for (const x of b) out += a[x % a.length];
|
|
1154
|
+
return out;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/index.ts
|
|
1158
|
+
var index_default = definePluginEntry({
|
|
1159
|
+
id: "clawos-kernel",
|
|
1160
|
+
name: "OpenClaw OS Kernel",
|
|
1161
|
+
description: "Capability model, gatekeeper registry, approval queue, and audit for OpenClaw OS.",
|
|
1162
|
+
configSchema: buildJsonPluginConfigSchema(schema),
|
|
1163
|
+
register(api) {
|
|
1164
|
+
if (["cli-metadata", "discovery", "full"].includes(api.registrationMode)) {
|
|
1165
|
+
api.registerCli(({ program }) => mountOperatorCli(program), { descriptors: [{ name: "os", description: "OpenClaw OS kernel administration", hasSubcommands: true, machineOutput: ({ argv }) => argv.includes("--json") }] });
|
|
1166
|
+
}
|
|
1167
|
+
if (!["full", "discovery", "tool-discovery"].includes(api.registrationMode)) return;
|
|
1168
|
+
const kernel = new Kernel(api);
|
|
1169
|
+
api.registerTrustedToolPolicy(kernel.capabilityPolicy());
|
|
1170
|
+
api.on("reply_dispatch", (e, ctx) => kernel.onReplyDispatch(e, ctx), { priority: 1e3, eligibleDispatchKinds: ["agent"] });
|
|
1171
|
+
api.on("before_agent_run", (e, ctx) => kernel.onBeforeAgentRun(e, ctx), { priority: 1e3 });
|
|
1172
|
+
api.on("before_prompt_build", (e, ctx) => kernel.onBeforePromptBuild(e, ctx), { priority: 1e3 });
|
|
1173
|
+
api.on("before_tool_call", (e, ctx) => kernel.onBeforeToolCall(e, ctx), { priority: 1e3, timeoutMs: 1e4 });
|
|
1174
|
+
api.on("after_tool_call", (e, ctx) => kernel.onAfterToolCall(e, ctx));
|
|
1175
|
+
api.on("before_agent_reply", (e, ctx) => kernel.onBeforeAgentReply(e, ctx));
|
|
1176
|
+
api.on("message_sending", (e, ctx) => kernel.onMessageSending(e, ctx));
|
|
1177
|
+
api.on("before_install", (e, ctx) => kernel.onBeforeInstall(e, ctx));
|
|
1178
|
+
api.on("agent_end", (e, ctx) => kernel.onAgentEnd(e, ctx));
|
|
1179
|
+
api.on("session_end", (e, ctx) => kernel.onSessionEnd(e, ctx));
|
|
1180
|
+
api.registerTool({
|
|
1181
|
+
name: "os_request_access",
|
|
1182
|
+
label: "Request access",
|
|
1183
|
+
description: "Ask the operator for access to a resource by URL.",
|
|
1184
|
+
parameters: Type2.Object({ url: Type2.String(), reason: Type2.String() }),
|
|
1185
|
+
execute: (toolCallId, params) => kernel.requestAccess(toolCallId, params)
|
|
1186
|
+
});
|
|
1187
|
+
api.registerTool({
|
|
1188
|
+
name: "os_list_grants",
|
|
1189
|
+
label: "List grants",
|
|
1190
|
+
description: "List the resources you currently have access to.",
|
|
1191
|
+
parameters: Type2.Object({}),
|
|
1192
|
+
execute: (toolCallId) => kernel.listGrantsForCall(toolCallId)
|
|
1193
|
+
});
|
|
1194
|
+
kernel.registerGatekeeperTools(api);
|
|
1195
|
+
if (api.registrationMode !== "full") return;
|
|
1196
|
+
api.on("gateway_start", () => kernel.start());
|
|
1197
|
+
api.on("gateway_stop", () => kernel.stop());
|
|
1198
|
+
for (const [name, handler] of kernel.gatewayMethods()) api.registerGatewayMethod(name, handler, { profileAccess: "required" });
|
|
1199
|
+
api.registerHttpRoute({ path: "/os/gatekeeper/", match: "prefix", auth: "plugin", handler: (req, res) => kernel.oauthRouter(req, res) });
|
|
1200
|
+
api.registerService({ id: "clawos-drainer", start: (ctx) => kernel.startDrainer(ctx), stop: () => kernel.stopDrainer() });
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
export {
|
|
1204
|
+
index_default as default
|
|
1205
|
+
};
|