clawclamp 0.1.13 → 0.1.14

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/assets/app.js CHANGED
@@ -14,6 +14,10 @@ const grantNoteInput = qs("grant-note");
14
14
  const grantHint = qs("grant-hint");
15
15
  const grantsEl = qs("grants");
16
16
  const auditEl = qs("audit");
17
+ const auditPageSizeEl = qs("audit-page-size");
18
+ const auditPrevEl = qs("audit-prev");
19
+ const auditNextEl = qs("audit-next");
20
+ const auditPageInfoEl = qs("audit-page-info");
17
21
  const policyListEl = qs("policy-list");
18
22
  const policyIdInput = qs("policy-id");
19
23
  const policyContentInput = qs("policy-content");
@@ -23,7 +27,9 @@ const policyDeleteBtn = qs("policy-delete");
23
27
  const policyStatusEl = qs("policy-status");
24
28
  let policyReadOnly = false;
25
29
  let policies = [];
26
- let deniedTools = new Set();
30
+ let auditPage = 1;
31
+ let auditPageSize = Number(auditPageSizeEl?.value || 50);
32
+ let auditTotal = 0;
27
33
 
28
34
  async function fetchJson(path, opts = {}) {
29
35
  const res = await fetch(path, {
@@ -163,6 +169,15 @@ function renderAudit(entries) {
163
169
  });
164
170
  }
165
171
 
172
+ function renderAuditPager(total, page, pageSize) {
173
+ auditTotal = total || 0;
174
+ auditPage = page || 1;
175
+ const totalPages = Math.max(1, Math.ceil(auditTotal / pageSize));
176
+ auditPageInfoEl.textContent = `第 ${auditPage} / ${totalPages} 页 · 共 ${auditTotal} 条`;
177
+ auditPrevEl.disabled = auditPage <= 1;
178
+ auditNextEl.disabled = auditPage >= totalPages;
179
+ }
180
+
166
181
  function renderPolicyList(list) {
167
182
  policies = list;
168
183
  if (!list.length) {
@@ -200,8 +215,9 @@ async function refreshAll() {
200
215
  renderMode(state);
201
216
  const grants = await fetchJson(`${API_BASE}/grants`);
202
217
  renderGrants(grants.grants || []);
203
- const logs = await fetchJson(`${API_BASE}/logs`);
218
+ const logs = await fetchJson(`${API_BASE}/logs?page=${auditPage}&pageSize=${auditPageSize}`);
204
219
  renderAudit(logs.entries || []);
220
+ renderAuditPager(logs.total || 0, logs.page || auditPage, logs.pageSize || auditPageSize);
205
221
  await refreshPolicies();
206
222
  }
207
223
 
@@ -240,6 +256,25 @@ grantForm.addEventListener("submit", async (event) => {
240
256
  await refreshAll();
241
257
  });
242
258
 
259
+ auditPageSizeEl.addEventListener("change", async () => {
260
+ auditPageSize = Number(auditPageSizeEl.value || 50);
261
+ auditPage = 1;
262
+ await refreshAll();
263
+ });
264
+
265
+ auditPrevEl.addEventListener("click", async () => {
266
+ if (auditPage <= 1) return;
267
+ auditPage -= 1;
268
+ await refreshAll();
269
+ });
270
+
271
+ auditNextEl.addEventListener("click", async () => {
272
+ const totalPages = Math.max(1, Math.ceil(auditTotal / auditPageSize));
273
+ if (auditPage >= totalPages) return;
274
+ auditPage += 1;
275
+ await refreshAll();
276
+ });
277
+
243
278
  refreshAll();
244
279
  setInterval(refreshAll, 10_000);
245
280
 
package/assets/index.html CHANGED
@@ -66,6 +66,22 @@
66
66
  <section class="card">
67
67
  <h2>审计日志</h2>
68
68
  <div class="note">最近的工具调用记录(允许、拒绝、灰度放行)。</div>
69
+ <div class="toolbar">
70
+ <label class="toolbar-item">
71
+ 每页
72
+ <select id="audit-page-size">
73
+ <option value="20">20</option>
74
+ <option value="50" selected>50</option>
75
+ <option value="100">100</option>
76
+ <option value="200">200</option>
77
+ </select>
78
+ </label>
79
+ <div class="toolbar-item pagination">
80
+ <button id="audit-prev" class="btn">上一页</button>
81
+ <span id="audit-page-info" class="note">第 1 页</span>
82
+ <button id="audit-next" class="btn">下一页</button>
83
+ </div>
84
+ </div>
69
85
  <div class="table" id="audit"></div>
70
86
  </section>
71
87
 
package/assets/styles.css CHANGED
@@ -119,6 +119,31 @@ h2 {
119
119
  margin-top: 6px;
120
120
  }
121
121
 
122
+ .toolbar {
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: space-between;
126
+ gap: 10px;
127
+ margin-top: 10px;
128
+ flex-wrap: wrap;
129
+ }
130
+
131
+ .toolbar-item {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 8px;
135
+ font-size: 0.82rem;
136
+ color: var(--muted);
137
+ }
138
+
139
+ .toolbar-item select {
140
+ min-width: 76px;
141
+ }
142
+
143
+ .pagination {
144
+ margin-left: auto;
145
+ }
146
+
122
147
  .btn {
123
148
  padding: 8px 12px;
124
149
  border-radius: 8px;
@@ -171,7 +196,8 @@ h2 {
171
196
  color: var(--muted);
172
197
  }
173
198
 
174
- input {
199
+ input,
200
+ select {
175
201
  border: 1px solid var(--border);
176
202
  border-radius: 8px;
177
203
  padding: 8px 10px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawclamp",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "OpenClaw Cedar authorization guard with audit UI",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/audit.ts CHANGED
@@ -5,11 +5,17 @@ import type { AuditEntry } from "./types.js";
5
5
  import { withStateFileLock } from "./storage.js";
6
6
 
7
7
  const AUDIT_FILE = "audit.jsonl";
8
+ const AUDIT_ROTATE_MAX_LINES = 5000;
9
+ const AUDIT_ROTATE_KEEP_LINES = 2500;
8
10
 
9
11
  function resolveAuditPath(stateDir: string): string {
10
12
  return path.join(stateDir, "clawclamp", AUDIT_FILE);
11
13
  }
12
14
 
15
+ function resolveAuditArchivePath(stateDir: string, timestamp: string): string {
16
+ return path.join(stateDir, "clawclamp", `audit-${timestamp}.jsonl`);
17
+ }
18
+
13
19
  export function createAuditEntryId(): string {
14
20
  return randomUUID();
15
21
  }
@@ -19,10 +25,38 @@ export async function appendAuditEntry(stateDir: string, entry: AuditEntry): Pro
19
25
  const filePath = resolveAuditPath(stateDir);
20
26
  await fs.mkdir(path.dirname(filePath), { recursive: true });
21
27
  await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf8");
28
+ await rotateAuditLogIfNeeded(stateDir, filePath);
22
29
  });
23
30
  }
24
31
 
25
- export async function readAuditEntries(stateDir: string, limit: number): Promise<AuditEntry[]> {
32
+ async function rotateAuditLogIfNeeded(stateDir: string, filePath: string): Promise<void> {
33
+ let raw: string;
34
+ try {
35
+ raw = await fs.readFile(filePath, "utf8");
36
+ } catch (error) {
37
+ const code = (error as { code?: string }).code;
38
+ if (code === "ENOENT") {
39
+ return;
40
+ }
41
+ throw error;
42
+ }
43
+ const lines = raw.split("\n").filter(Boolean);
44
+ if (lines.length <= AUDIT_ROTATE_MAX_LINES) {
45
+ return;
46
+ }
47
+ const cutoff = Math.max(0, lines.length - AUDIT_ROTATE_KEEP_LINES);
48
+ const archived = lines.slice(0, cutoff);
49
+ const current = lines.slice(cutoff);
50
+ const archivePath = resolveAuditArchivePath(stateDir, new Date().toISOString().replace(/[:.]/g, "-"));
51
+ await fs.writeFile(archivePath, `${archived.join("\n")}\n`, "utf8");
52
+ await fs.writeFile(filePath, current.length ? `${current.join("\n")}\n` : "", "utf8");
53
+ }
54
+
55
+ export async function readAuditEntries(
56
+ stateDir: string,
57
+ page: number,
58
+ pageSize: number,
59
+ ): Promise<{ entries: AuditEntry[]; total: number; page: number }> {
26
60
  return withStateFileLock(stateDir, "audit", async () => {
27
61
  const filePath = resolveAuditPath(stateDir);
28
62
  let raw: string;
@@ -31,14 +65,19 @@ export async function readAuditEntries(stateDir: string, limit: number): Promise
31
65
  } catch (error) {
32
66
  const code = (error as { code?: string }).code;
33
67
  if (code === "ENOENT") {
34
- return [];
68
+ return { entries: [], total: 0, page: 1 };
35
69
  }
36
70
  throw error;
37
71
  }
38
72
 
39
73
  const lines = raw.trim().split("\n").filter(Boolean);
40
- const start = Math.max(0, lines.length - limit);
41
- const recent = lines.slice(start);
74
+ const total = lines.length;
75
+ const safePageSize = Math.max(1, pageSize);
76
+ const totalPages = Math.max(1, Math.ceil(total / safePageSize));
77
+ const safePage = Math.min(Math.max(1, page), totalPages);
78
+ const start = Math.max(0, total - safePage * safePageSize);
79
+ const end = total - (safePage - 1) * safePageSize;
80
+ const recent = lines.slice(start, end);
42
81
  const entries: AuditEntry[] = [];
43
82
  for (const line of recent) {
44
83
  try {
@@ -50,6 +89,6 @@ export async function readAuditEntries(stateDir: string, limit: number): Promise
50
89
  // Ignore malformed lines.
51
90
  }
52
91
  }
53
- return entries;
92
+ return { entries, total, page: safePage };
54
93
  });
55
94
  }
package/src/cedarling.ts CHANGED
@@ -24,6 +24,10 @@ export async function getCedarling(config: CedarlingConfig): Promise<CedarlingIn
24
24
  return cedarlingPromise;
25
25
  }
26
26
 
27
+ export function resetCedarlingInstance(): void {
28
+ cedarlingPromise = null;
29
+ }
30
+
27
31
  async function ensureWasmInitialized(): Promise<void> {
28
32
  if (!wasmInitPromise) {
29
33
  wasmInitPromise = (async () => {
package/src/grants.ts CHANGED
@@ -4,48 +4,134 @@ import { randomUUID } from "node:crypto";
4
4
  import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk";
5
5
  import type { ClawClampConfig, GrantRecord } from "./types.js";
6
6
  import { withStateFileLock } from "./storage.js";
7
+ import { buildDefaultPolicyStore } from "./policy.js";
7
8
 
8
- const GRANTS_FILE = "grants.json";
9
+ const POLICY_FILE = "policy-store.json";
10
+ const POLICY_STORE_ID = "clawclamp";
11
+ const GRANT_POLICY_PREFIX = "grant:";
9
12
 
10
- type GrantState = {
11
- grants: GrantRecord[];
13
+ type PolicyRecord = {
14
+ cedar_version?: string;
15
+ name?: string;
16
+ description?: string;
17
+ policy_content: string;
12
18
  };
13
19
 
14
- const EMPTY_STATE: GrantState = { grants: [] };
20
+ type PolicyStoreBody = {
21
+ name?: string;
22
+ description?: string;
23
+ schema?: unknown;
24
+ trusted_issuers?: Record<string, unknown>;
25
+ policies?: Record<string, PolicyRecord>;
26
+ };
27
+
28
+ type PolicyStoreSnapshot = {
29
+ cedar_version: string;
30
+ policy_stores: Record<string, PolicyStoreBody>;
31
+ };
15
32
 
16
- function resolveGrantPath(stateDir: string): string {
17
- return path.join(stateDir, "clawclamp", GRANTS_FILE);
33
+ function resolvePolicyPath(stateDir: string): string {
34
+ return path.join(stateDir, "clawclamp", POLICY_FILE);
18
35
  }
19
36
 
20
- async function readGrantState(stateDir: string): Promise<GrantState> {
21
- const filePath = resolveGrantPath(stateDir);
22
- const { value } = await readJsonFileWithFallback(filePath, EMPTY_STATE);
23
- if (!value || typeof value !== "object" || Array.isArray(value)) {
24
- return { grants: [] };
25
- }
26
- const grants = Array.isArray((value as GrantState).grants) ? (value as GrantState).grants : [];
27
- return { grants };
37
+ function encodeBase64(value: string): string {
38
+ return Buffer.from(value, "utf8").toString("base64");
39
+ }
40
+
41
+ function decodeBase64(value: string): string {
42
+ return Buffer.from(value, "base64").toString("utf8");
43
+ }
44
+
45
+ async function readPolicyStore(stateDir: string): Promise<PolicyStoreSnapshot> {
46
+ const filePath = resolvePolicyPath(stateDir);
47
+ const { value } = await readJsonFileWithFallback<PolicyStoreSnapshot>(
48
+ filePath,
49
+ buildDefaultPolicyStore() as PolicyStoreSnapshot,
50
+ );
51
+ return value;
28
52
  }
29
53
 
30
- async function writeGrantState(stateDir: string, state: GrantState): Promise<void> {
31
- const filePath = resolveGrantPath(stateDir);
54
+ async function writePolicyStore(stateDir: string, store: PolicyStoreSnapshot): Promise<void> {
55
+ const filePath = resolvePolicyPath(stateDir);
32
56
  await fs.mkdir(path.dirname(filePath), { recursive: true });
33
- await writeJsonFileAtomically(filePath, state);
57
+ await writeJsonFileAtomically(filePath, store);
58
+ }
59
+
60
+ function getWritableStore(store: PolicyStoreSnapshot): PolicyStoreBody {
61
+ const current = store.policy_stores?.[POLICY_STORE_ID];
62
+ if (!current) {
63
+ throw new Error("clawclamp policy store not found");
64
+ }
65
+ current.policies ??= {};
66
+ return current;
67
+ }
68
+
69
+ function grantPolicyId(createdAtMs: number): string {
70
+ return `${GRANT_POLICY_PREFIX}${createdAtMs}:${randomUUID()}`;
71
+ }
72
+
73
+ function buildGrantPolicy(toolName: string, expiresAtMs: number): string {
74
+ const toolClause =
75
+ toolName === "*" ? "true" : `resource.name == ${JSON.stringify(toolName)}`;
76
+ return `permit(principal, action, resource)
77
+ when {
78
+ action == Action::"Invoke" &&
79
+ context.now < ${expiresAtMs} &&
80
+ ${toolClause}
81
+ };`;
34
82
  }
35
83
 
36
- function pruneExpired(grants: GrantRecord[], now: Date): GrantRecord[] {
37
- return grants.filter((grant) => Date.parse(grant.expiresAt) > now.getTime());
84
+ function parseGrantPolicy(id: string, record: PolicyRecord): GrantRecord | null {
85
+ if (!id.startsWith(GRANT_POLICY_PREFIX)) {
86
+ return null;
87
+ }
88
+ const content = decodeBase64(record.policy_content ?? "");
89
+ const createdAtMatch = /^grant:(\d+):/.exec(id);
90
+ const expiresAtMatch = /context\.now\s*<\s*(\d+)/.exec(content);
91
+ const toolMatch = /resource\.name\s*==\s*"([^"]+)"/.exec(content);
92
+ const createdAtMs = createdAtMatch ? Number(createdAtMatch[1]) : Date.now();
93
+ const expiresAtMs = expiresAtMatch ? Number(expiresAtMatch[1]) : 0;
94
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= 0) {
95
+ return null;
96
+ }
97
+ return {
98
+ id,
99
+ toolName: toolMatch?.[1] ?? "*",
100
+ createdAt: new Date(createdAtMs).toISOString(),
101
+ expiresAt: new Date(expiresAtMs).toISOString(),
102
+ note: record.description?.trim() || undefined,
103
+ };
104
+ }
105
+
106
+ function sortNewestFirst(left: GrantRecord, right: GrantRecord): number {
107
+ return Date.parse(right.createdAt) - Date.parse(left.createdAt);
108
+ }
109
+
110
+ function pruneExpiredPolicies(policies: Record<string, PolicyRecord>, nowMs: number): boolean {
111
+ let changed = false;
112
+ for (const [id, record] of Object.entries(policies)) {
113
+ const grant = parseGrantPolicy(id, record);
114
+ if (grant && Date.parse(grant.expiresAt) <= nowMs) {
115
+ delete policies[id];
116
+ changed = true;
117
+ }
118
+ }
119
+ return changed;
38
120
  }
39
121
 
40
122
  export async function listGrants(stateDir: string): Promise<GrantRecord[]> {
41
- return withStateFileLock(stateDir, "grants", async () => {
42
- const state = await readGrantState(stateDir);
43
- const now = new Date();
44
- const pruned = pruneExpired(state.grants, now);
45
- if (pruned.length !== state.grants.length) {
46
- await writeGrantState(stateDir, { grants: pruned });
123
+ return withStateFileLock(stateDir, "policy-store", async () => {
124
+ const store = await readPolicyStore(stateDir);
125
+ const policyStore = getWritableStore(store);
126
+ const nowMs = Date.now();
127
+ const changed = pruneExpiredPolicies(policyStore.policies ?? {}, nowMs);
128
+ if (changed) {
129
+ await writePolicyStore(stateDir, store);
47
130
  }
48
- return pruned;
131
+ return Object.entries(policyStore.policies ?? {})
132
+ .map(([id, record]) => parseGrantPolicy(id, record))
133
+ .filter((grant): grant is GrantRecord => Boolean(grant))
134
+ .sort(sortNewestFirst);
49
135
  });
50
136
  }
51
137
 
@@ -56,47 +142,42 @@ export async function createGrant(params: {
56
142
  ttlSeconds?: number;
57
143
  note?: string;
58
144
  }): Promise<GrantRecord> {
59
- return withStateFileLock(params.stateDir, "grants", async () => {
60
- const state = await readGrantState(params.stateDir);
61
- const now = new Date();
145
+ return withStateFileLock(params.stateDir, "policy-store", async () => {
146
+ const store = await readPolicyStore(params.stateDir);
147
+ const policyStore = getWritableStore(store);
148
+ const nowMs = Date.now();
62
149
  const ttlSeconds = Math.min(
63
150
  Math.max(60, params.ttlSeconds ?? params.config.grants.defaultTtlSeconds),
64
151
  params.config.grants.maxTtlSeconds,
65
152
  );
66
- const expiresAt = new Date(now.getTime() + ttlSeconds * 1000);
67
- const grant: GrantRecord = {
68
- id: randomUUID(),
153
+ const expiresAtMs = nowMs + ttlSeconds * 1000;
154
+ const id = grantPolicyId(nowMs);
155
+ policyStore.policies![id] = {
156
+ cedar_version: store.cedar_version,
157
+ name: `Grant ${params.toolName}`,
158
+ description: params.note?.trim() || undefined,
159
+ policy_content: encodeBase64(buildGrantPolicy(params.toolName, expiresAtMs)),
160
+ };
161
+ await writePolicyStore(params.stateDir, store);
162
+ return {
163
+ id,
69
164
  toolName: params.toolName,
70
- createdAt: now.toISOString(),
71
- expiresAt: expiresAt.toISOString(),
72
- note: params.note?.trim() ? params.note.trim() : undefined,
165
+ createdAt: new Date(nowMs).toISOString(),
166
+ expiresAt: new Date(expiresAtMs).toISOString(),
167
+ note: params.note?.trim() || undefined,
73
168
  };
74
- const pruned = pruneExpired(state.grants, now);
75
- pruned.push(grant);
76
- await writeGrantState(params.stateDir, { grants: pruned });
77
- return grant;
78
169
  });
79
170
  }
80
171
 
81
172
  export async function revokeGrant(stateDir: string, grantId: string): Promise<boolean> {
82
- return withStateFileLock(stateDir, "grants", async () => {
83
- const state = await readGrantState(stateDir);
84
- const next = state.grants.filter((grant) => grant.id !== grantId);
85
- const changed = next.length !== state.grants.length;
86
- if (changed) {
87
- await writeGrantState(stateDir, { grants: next });
88
- }
89
- return changed;
90
- });
91
- }
92
-
93
- export function findActiveGrant(grants: GrantRecord[], toolName: string): GrantRecord | undefined {
94
- const now = Date.now();
95
- return grants.find((grant) => {
96
- if (grant.toolName !== "*" && grant.toolName !== toolName) {
173
+ return withStateFileLock(stateDir, "policy-store", async () => {
174
+ const store = await readPolicyStore(stateDir);
175
+ const policyStore = getWritableStore(store);
176
+ if (!policyStore.policies?.[grantId]) {
97
177
  return false;
98
178
  }
99
- const expiresAt = Date.parse(grant.expiresAt);
100
- return Number.isFinite(expiresAt) && expiresAt > now;
179
+ delete policyStore.policies[grantId];
180
+ await writePolicyStore(stateDir, store);
181
+ return true;
101
182
  });
102
183
  }
package/src/guard.ts CHANGED
@@ -5,9 +5,8 @@ import type {
5
5
  PluginHookToolContext,
6
6
  } from "openclaw/plugins/types";
7
7
  import type { CedarlingConfig, CedarlingInstance } from "./cedarling.js";
8
- import { getCedarling } from "./cedarling.js";
8
+ import { getCedarling, resetCedarlingInstance } from "./cedarling.js";
9
9
  import { appendAuditEntry, createAuditEntryId } from "./audit.js";
10
- import { listGrants, findActiveGrant } from "./grants.js";
11
10
  import { getModeOverride } from "./mode.js";
12
11
  import { buildDefaultPolicyStore } from "./policy.js";
13
12
  import { ensurePolicyStore } from "./policy-store.js";
@@ -111,8 +110,6 @@ async function evaluateCedar(params: {
111
110
  config: ClawClampConfig;
112
111
  toolName: string;
113
112
  risk: RiskLevel;
114
- grantActive: boolean;
115
- grantExpiresAt?: string;
116
113
  }): Promise<CedarEvaluation> {
117
114
  const now = Date.now();
118
115
  const request = {
@@ -134,8 +131,6 @@ async function evaluateCedar(params: {
134
131
  now,
135
132
  tool: params.toolName,
136
133
  risk: params.risk,
137
- grant_active: params.grantActive,
138
- grant_expires_at: params.grantExpiresAt ? Date.parse(params.grantExpiresAt) : 0,
139
134
  },
140
135
  };
141
136
 
@@ -154,6 +149,7 @@ export class ClawClampService {
154
149
  private readonly stateDir: string;
155
150
  private readonly api: OpenClawPluginApi;
156
151
  private cedarlingPromise: Promise<CedarlingInstance> | null = null;
152
+ private cedarlingPolicyStoreJson: string | null = null;
157
153
 
158
154
  constructor(params: { api: OpenClawPluginApi; config: ClawClampConfig; stateDir: string }) {
159
155
  this.api = params.api;
@@ -170,7 +166,6 @@ export class ClawClampService {
170
166
  const paramsSummary = summarizeParams(event.params, this.config);
171
167
  const auditId = createAuditEntryId();
172
168
  const mode = await this.getEffectiveMode();
173
- const grant = await this.resolveGrant(toolName);
174
169
 
175
170
  let cedarDecision: CedarEvaluation;
176
171
  if (this.config.enabled) {
@@ -181,8 +176,6 @@ export class ClawClampService {
181
176
  config: this.config,
182
177
  toolName,
183
178
  risk,
184
- grantActive: Boolean(grant),
185
- grantExpiresAt: grant?.expiresAt,
186
179
  });
187
180
  } catch (error) {
188
181
  const reason = error instanceof Error ? error.message : String(error);
@@ -229,8 +222,6 @@ export class ClawClampService {
229
222
  decision: finalDecision,
230
223
  reason,
231
224
  params: paramsSummary,
232
- grantId: grant?.id,
233
- grantExpiresAt: grant?.expiresAt,
234
225
  grayMode: mode === "gray",
235
226
  };
236
227
 
@@ -270,19 +261,19 @@ export class ClawClampService {
270
261
  return override ?? this.config.mode;
271
262
  }
272
263
 
273
- async resolveGrant(toolName: string) {
274
- const grants = await listGrants(this.stateDir);
275
- return findActiveGrant(grants, toolName);
276
- }
277
-
278
264
  private async getCedarlingInstance(): Promise<CedarlingInstance> {
279
- if (!this.cedarlingPromise) {
280
- const policyStore = await ensurePolicyStore({ stateDir: this.stateDir, config: this.config });
265
+ const policyStore = await ensurePolicyStore({ stateDir: this.stateDir, config: this.config });
266
+ if (
267
+ !this.cedarlingPromise ||
268
+ (!policyStore.readOnly && policyStore.json && policyStore.json !== this.cedarlingPolicyStoreJson)
269
+ ) {
281
270
  const cedarConfig = buildCedarlingConfig({
282
271
  config: this.config,
283
272
  policyStoreLocal: policyStore.readOnly ? undefined : policyStore.json,
284
273
  });
274
+ resetCedarlingInstance();
285
275
  this.cedarlingPromise = getCedarling(cedarConfig);
276
+ this.cedarlingPolicyStoreJson = policyStore.readOnly ? null : (policyStore.json ?? null);
286
277
  }
287
278
  try {
288
279
  return await this.cedarlingPromise;
@@ -294,6 +285,8 @@ export class ClawClampService {
294
285
 
295
286
  async resetCedarling(): Promise<void> {
296
287
  this.cedarlingPromise = null;
288
+ this.cedarlingPolicyStoreJson = null;
289
+ resetCedarlingInstance();
297
290
  }
298
291
  }
299
292
 
package/src/http.ts CHANGED
@@ -279,13 +279,20 @@ export function createClawClampHttpHandler(params: {
279
279
  }
280
280
 
281
281
  if (apiPath === "logs" && req.method === "GET") {
282
- const limitParam = parsed.searchParams.get("limit");
283
- const limit = Math.min(
284
- Math.max(1, Number(limitParam) || params.config.audit.maxEntries),
282
+ const pageParam = parsed.searchParams.get("page");
283
+ const pageSizeParam = parsed.searchParams.get("pageSize");
284
+ const page = Math.max(1, Number(pageParam) || 1);
285
+ const pageSize = Math.min(
286
+ Math.max(1, Number(pageSizeParam) || 50),
285
287
  params.config.audit.maxEntries,
286
288
  );
287
- const entries = await readAuditEntries(params.stateDir, limit);
288
- sendJson(res, 200, { entries: entries.reverse() });
289
+ const result = await readAuditEntries(params.stateDir, page, pageSize);
290
+ sendJson(res, 200, {
291
+ entries: result.entries.reverse(),
292
+ total: result.total,
293
+ page: result.page,
294
+ pageSize,
295
+ });
289
296
  return true;
290
297
  }
291
298
 
package/src/policy.ts CHANGED
@@ -33,18 +33,11 @@ action "Invoke" appliesTo {
33
33
  now: Long,
34
34
  tool: String,
35
35
  risk: String,
36
- grant_active: Bool,
37
- grant_expires_at: Long,
38
36
  }
39
37
  };
40
38
  `;
41
39
 
42
- const DEFAULT_POLICIES: string[] = [
43
- `permit(principal, action, resource)
44
- when {
45
- action == Action::"Invoke" && context.grant_active == true
46
- };`,
47
- ];
40
+ const DEFAULT_POLICIES: string[] = [];
48
41
 
49
42
  const POLICY_STORE_ID = "clawclamp";
50
43