@undefineds.co/linx 0.2.17 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +3 -6
  2. package/dist/generated/version.js +1 -1
  3. package/dist/generated/version.js.map +1 -1
  4. package/dist/index.js +256 -143
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/account-api.js +1 -1
  7. package/dist/lib/account-api.js.map +1 -1
  8. package/dist/lib/account-session.js +1 -1
  9. package/dist/lib/account-session.js.map +1 -1
  10. package/dist/lib/ai-command.js +186 -367
  11. package/dist/lib/ai-command.js.map +1 -1
  12. package/dist/lib/chat-api.js +177 -19
  13. package/dist/lib/chat-api.js.map +1 -1
  14. package/dist/lib/codex-plugin/bridge.js.map +1 -1
  15. package/dist/lib/codex-plugin/codex-native-proxy.js.map +1 -1
  16. package/dist/lib/codex-plugin/index.js.map +1 -1
  17. package/dist/lib/codex-plugin/runner.js.map +1 -1
  18. package/dist/lib/credentials-store.js +7 -1
  19. package/dist/lib/credentials-store.js.map +1 -1
  20. package/dist/lib/default-model.js +1 -0
  21. package/dist/lib/default-model.js.map +1 -1
  22. package/dist/lib/login-command.js +33 -10
  23. package/dist/lib/login-command.js.map +1 -1
  24. package/dist/lib/models.js +2 -27
  25. package/dist/lib/models.js.map +1 -1
  26. package/dist/lib/node-warning-filter.js +34 -0
  27. package/dist/lib/node-warning-filter.js.map +1 -0
  28. package/dist/lib/oidc-auth.js +130 -18
  29. package/dist/lib/oidc-auth.js.map +1 -1
  30. package/dist/lib/oidc-session-storage.js.map +1 -1
  31. package/dist/lib/pi-adapter/auth.js +47 -11
  32. package/dist/lib/pi-adapter/auth.js.map +1 -1
  33. package/dist/lib/pi-adapter/branding.js +764 -76
  34. package/dist/lib/pi-adapter/branding.js.map +1 -1
  35. package/dist/lib/pi-adapter/index.js.map +1 -1
  36. package/dist/lib/pi-adapter/interactive.js +179 -5
  37. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  38. package/dist/lib/pi-adapter/pod-approval.js +8 -0
  39. package/dist/lib/pi-adapter/pod-approval.js.map +1 -0
  40. package/dist/lib/pi-adapter/pod-mirror-mapping.js +189 -0
  41. package/dist/lib/pi-adapter/pod-mirror-mapping.js.map +1 -0
  42. package/dist/lib/pi-adapter/pod-mirror.js +416 -0
  43. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -0
  44. package/dist/lib/pi-adapter/pod-tools.js +104 -0
  45. package/dist/lib/pi-adapter/pod-tools.js.map +1 -0
  46. package/dist/lib/pi-adapter/runtime.js +327 -28
  47. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  48. package/dist/lib/pi-adapter/session.js +608 -0
  49. package/dist/lib/pi-adapter/session.js.map +1 -0
  50. package/dist/lib/pi-adapter/stream.js +154 -4
  51. package/dist/lib/pi-adapter/stream.js.map +1 -1
  52. package/dist/lib/pi-adapter/theme.js.map +1 -1
  53. package/dist/lib/pi-adapter/web-fetch.js +154 -0
  54. package/dist/lib/pi-adapter/web-fetch.js.map +1 -0
  55. package/dist/lib/pod-chat-store.js +40 -30
  56. package/dist/lib/pod-chat-store.js.map +1 -1
  57. package/dist/lib/pod-data-session.js +110 -0
  58. package/dist/lib/pod-data-session.js.map +1 -0
  59. package/dist/lib/profile-identity.js +16 -60
  60. package/dist/lib/profile-identity.js.map +1 -1
  61. package/dist/lib/prompt.js.map +1 -1
  62. package/dist/lib/runtime-target.js +1 -1
  63. package/dist/lib/runtime-target.js.map +1 -1
  64. package/dist/lib/solid-auth.js.map +1 -1
  65. package/dist/lib/thread-utils.js.map +1 -1
  66. package/dist/lib/watch/archive.js +1 -1
  67. package/dist/lib/watch/archive.js.map +1 -1
  68. package/dist/lib/watch/auth.js +1 -1
  69. package/dist/lib/watch/auth.js.map +1 -1
  70. package/dist/lib/watch/codex-composer.js.map +1 -1
  71. package/dist/lib/watch/codex-footer.js.map +1 -1
  72. package/dist/lib/watch/codex-overlay.js.map +1 -1
  73. package/dist/lib/watch/codex-request-form.js +1 -1
  74. package/dist/lib/watch/codex-request-form.js.map +1 -1
  75. package/dist/lib/watch/codex-request-input.js.map +1 -1
  76. package/dist/lib/watch/display.js.map +1 -1
  77. package/dist/lib/watch/format.js.map +1 -1
  78. package/dist/lib/watch/hooks/claude.js +4 -0
  79. package/dist/lib/watch/hooks/claude.js.map +1 -1
  80. package/dist/lib/watch/hooks/codebuddy.js +4 -0
  81. package/dist/lib/watch/hooks/codebuddy.js.map +1 -1
  82. package/dist/lib/watch/hooks/codex.js +4 -0
  83. package/dist/lib/watch/hooks/codex.js.map +1 -1
  84. package/dist/lib/watch/hooks/index.js.map +1 -1
  85. package/dist/lib/watch/hooks/shared.js +0 -1
  86. package/dist/lib/watch/hooks/shared.js.map +1 -1
  87. package/dist/lib/watch/index.js.map +1 -1
  88. package/dist/lib/watch/pod-ai.js +29 -37
  89. package/dist/lib/watch/pod-ai.js.map +1 -1
  90. package/dist/lib/watch/pod-approval.js +822 -220
  91. package/dist/lib/watch/pod-approval.js.map +1 -1
  92. package/dist/lib/watch/pod-persistence.js +214 -106
  93. package/dist/lib/watch/pod-persistence.js.map +1 -1
  94. package/dist/lib/watch/runner.js +243 -38
  95. package/dist/lib/watch/runner.js.map +1 -1
  96. package/dist/lib/watch/secretary.js +238 -0
  97. package/dist/lib/watch/secretary.js.map +1 -0
  98. package/dist/lib/watch/types.js.map +1 -1
  99. package/dist/watch-cli.js +8 -34
  100. package/dist/watch-cli.js.map +1 -1
  101. package/package.json +3 -9
  102. package/vendor/agent-runtime/dist/acp.d.ts +27 -0
  103. package/vendor/agent-runtime/dist/acp.js +86 -0
  104. package/vendor/agent-runtime/dist/companion-model.d.ts +7 -0
  105. package/vendor/agent-runtime/dist/companion-model.js +12 -0
  106. package/vendor/agent-runtime/dist/index.d.ts +3 -0
  107. package/vendor/agent-runtime/dist/index.js +3 -0
  108. package/vendor/agent-runtime/dist/turn-controller.d.ts +69 -0
  109. package/vendor/agent-runtime/dist/turn-controller.js +129 -0
  110. package/vendor/agent-runtime/package.json +11 -0
  111. package/vendor/client/dist/client/index.d.ts +0 -118
  112. package/vendor/client/dist/client/index.js +0 -260
  113. package/vendor/client/dist/index.d.ts +0 -1
  114. package/vendor/client/dist/index.js +0 -1
  115. package/vendor/client/dist/watch/index.d.ts +0 -226
  116. package/vendor/client/dist/watch/index.js +0 -1114
  117. package/vendor/client/package.json +0 -9
@@ -1,17 +1,22 @@
1
1
  import { setTimeout as delay } from 'node:timers/promises';
2
- const WATCH_CHAT_ID = 'linx-watch';
2
+ import { getDefaultPodDataSession } from '../pod-data-session.js';
3
+ import { approvalResource, auditResource, drizzle, grantResource, inboxNotificationTable, initSolidTables, solidSchema, } from '../models.js';
4
+ import { resolveWatchGrantCoverage } from './secretary.js';
5
+ const WATCH_CHAT_ID_PREFIX = 'linx-watch';
3
6
  const WATCH_AGENT_ID = 'linx-watch-assistant';
4
7
  const REMOTE_APPROVAL_POLICY_VERSION = 'linx-watch-remote-approval/v1';
5
8
  const DEFAULT_REMOTE_APPROVAL_POLL_MS = 1000;
9
+ const DEFAULT_WARN_ONLY_TIMEOUT_MS = 5000;
10
+ const MAX_GRANT_POLICY_LENGTH = 1200;
11
+ const MAX_APPROVAL_CONTEXT_LENGTH = 1400;
12
+ const MIN_GRANT_COVERAGE_CONFIDENCE = 0.75;
13
+ const MAX_GRANT_COVERAGE_CANDIDATES = 5;
14
+ const remoteApprovalClientCache = new WeakMap();
6
15
  function createAbortError() {
7
16
  const error = new Error('The operation was aborted.');
8
17
  error.name = 'AbortError';
9
18
  return error;
10
19
  }
11
- async function dynamicImport(specifier) {
12
- const loader = new Function('modulePath', 'return import(modulePath)');
13
- return loader(specifier);
14
- }
15
20
  function normalizeString(value) {
16
21
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
17
22
  }
@@ -37,14 +42,17 @@ function getPodBaseUrl(webIdOrUri) {
37
42
  }
38
43
  return webIdOrUri.replace(/\/$/, '');
39
44
  }
40
- function buildThreadUri(webId, threadId) {
41
- return `${getPodBaseUrl(webId)}/.data/chat/${WATCH_CHAT_ID}/index.ttl#${threadId}`;
45
+ function buildWatchChatId(record) {
46
+ return `${WATCH_CHAT_ID_PREFIX}-${record.backend}`;
47
+ }
48
+ function buildThreadUri(webId, record) {
49
+ return `${getPodBaseUrl(webId)}/.data/chat/${buildWatchChatId(record)}/index.ttl#${record.id}`;
42
50
  }
43
- function buildApprovalUri(webIdOrUri, approvalId) {
44
- return `${getPodBaseUrl(webIdOrUri)}/.data/approvals/${approvalId}.ttl`;
51
+ function isAbsoluteIri(value) {
52
+ return value.startsWith('http://') || value.startsWith('https://');
45
53
  }
46
- function buildGrantUri(webIdOrUri, grantId) {
47
- return `${getPodBaseUrl(webIdOrUri)}/settings/autonomy/grants/${grantId}.ttl`;
54
+ function buildGrantSchemaUri(webIdOrUri) {
55
+ return `${getPodBaseUrl(webIdOrUri)}/settings/autonomy/schema/grant.ttl#GrantWikiPage`;
48
56
  }
49
57
  function buildAgentUri(webId) {
50
58
  return `${getPodBaseUrl(webId)}/.data/agents/${WATCH_AGENT_ID}.ttl`;
@@ -103,16 +111,6 @@ function buildRequestMessage(request) {
103
111
  }
104
112
  return request.message;
105
113
  }
106
- function buildRequestAuditContext(record, request) {
107
- return {
108
- kind: request.kind,
109
- message: buildRequestMessage(request),
110
- ...(request.kind === 'command-approval' && request.command ? { command: request.command } : {}),
111
- ...(request.kind === 'command-approval' && request.cwd ? { cwd: request.cwd } : {}),
112
- backend: record.backend,
113
- sessionId: record.id,
114
- };
115
- }
116
114
  function extractToolCallId(request) {
117
115
  if (!isRecord(request.raw)) {
118
116
  return crypto.randomUUID();
@@ -124,7 +122,7 @@ function extractToolCallId(request) {
124
122
  ?? crypto.randomUUID();
125
123
  }
126
124
  function encodeDecisionReason(decision, note) {
127
- return JSON.stringify({
125
+ return safeJsonStringify({
128
126
  decision,
129
127
  ...(note?.trim() ? { note: note.trim() } : {}),
130
128
  });
@@ -151,35 +149,184 @@ function parseDecisionReason(value) {
151
149
  return null;
152
150
  }
153
151
  }
154
- function parseRequestAuditContext(value) {
152
+ async function warnOnly(runtime, task) {
153
+ try {
154
+ await Promise.race([
155
+ task(),
156
+ runtime.sleep(DEFAULT_WARN_ONLY_TIMEOUT_MS).then(() => {
157
+ throw new Error(`Pod side-effect sync timed out after ${DEFAULT_WARN_ONLY_TIMEOUT_MS}ms`);
158
+ }),
159
+ ]);
160
+ }
161
+ catch (error) {
162
+ if (runtime.onWarning) {
163
+ runtime.onWarning(error);
164
+ return;
165
+ }
166
+ const message = error instanceof Error ? error.message : String(error);
167
+ process.emitWarning(`LinX Pod sync failed: ${message}`);
168
+ }
169
+ }
170
+ function safeJsonStringify(value) {
171
+ try {
172
+ return JSON.stringify(value);
173
+ }
174
+ catch {
175
+ return JSON.stringify({ error: 'unserializable_context' });
176
+ }
177
+ }
178
+ function truncatePodLiteral(value, maxLength) {
179
+ if (value.length <= maxLength) {
180
+ return value;
181
+ }
182
+ return `${value.slice(0, Math.max(0, maxLength - 15))}...[truncated]`;
183
+ }
184
+ function safeCompactJson(value, maxLength) {
185
+ return truncatePodLiteral(safeJsonStringify(value), maxLength);
186
+ }
187
+ function compactApprovalContext(request) {
188
+ return safeCompactJson({
189
+ kind: request.kind,
190
+ message: request.message,
191
+ toolName: request.toolName,
192
+ action: request.action,
193
+ risk: request.risk,
194
+ ...(request.command ? { command: request.command } : {}),
195
+ ...(request.cwd ? { cwd: request.cwd } : {}),
196
+ ...(request.approvalOptions ? { approvalOptions: request.approvalOptions } : {}),
197
+ ...(request.expiresAt ? { expiresAt: normalizeDateLike(request.expiresAt) } : {}),
198
+ ...(request.context ? { sourceContext: truncatePodLiteral(request.context, 500) } : {}),
199
+ }, MAX_APPROVAL_CONTEXT_LENGTH);
200
+ }
201
+ function grantWikiTitleFromApproval(row, explicitTitle) {
202
+ const explicit = normalizeString(explicitTitle);
203
+ if (explicit) {
204
+ return truncatePodLiteral(explicit, 160);
205
+ }
206
+ return truncatePodLiteral(`${row.toolName} grant wiki for ${extractSessionId(row.session)}`, 160);
207
+ }
208
+ function grantWikiSummaryFromApproval(row, explicitSummary) {
209
+ const explicit = normalizeString(explicitSummary);
210
+ if (explicit) {
211
+ return truncatePodLiteral(explicit, 500);
212
+ }
213
+ return truncatePodLiteral(`Authorization wiki page for ${row.toolName}. AI Secretary must read the page body before reusing this grant.`, 500);
214
+ }
215
+ function grantWikiBodyFromApproval(row, explicitBody) {
216
+ const explicit = normalizeString(explicitBody);
217
+ if (explicit) {
218
+ return truncatePodLiteral(explicit, MAX_GRANT_POLICY_LENGTH);
219
+ }
220
+ return truncatePodLiteral([
221
+ '# Grant Semantics',
222
+ '',
223
+ 'This page follows the LLM Wiki pattern: it is the maintained wiki view AI Secretary reads before reusing an authorization.',
224
+ '',
225
+ '## Covers',
226
+ `- Requests semantically inside target ${row.target}.`,
227
+ `- Action family ${row.action}.`,
228
+ `- Risk no higher than ${row.risk}.`,
229
+ '',
230
+ '## Does Not Cover',
231
+ '- Requests that are materially broader than the source approval.',
232
+ '- Requests that change from read-oriented to write/destructive behavior.',
233
+ '- Requests that touch credentials, secrets, package installation, new network side effects, or workspace boundaries unless explicitly documented here.',
234
+ '',
235
+ '## Source Context',
236
+ row.context ?? safeJsonStringify({ toolName: row.toolName, action: row.action, risk: row.risk }),
237
+ ].join('\n'), MAX_GRANT_POLICY_LENGTH);
238
+ }
239
+ function grantIndexTextFromWikiBody(body) {
240
+ return truncatePodLiteral(body, MAX_GRANT_POLICY_LENGTH);
241
+ }
242
+ function grantWikiTagsFromApproval(row, explicitTags) {
243
+ const tags = [
244
+ 'autonomy',
245
+ 'grant',
246
+ row.toolName,
247
+ row.risk,
248
+ ...(explicitTags ?? []),
249
+ ]
250
+ .map((tag) => tag.trim())
251
+ .filter(Boolean);
252
+ return safeJsonStringify([...new Set(tags)]);
253
+ }
254
+ function grantContextFromApproval(row) {
255
+ return safeCompactJson({
256
+ sourceApproval: row.approvalUri ?? row.id,
257
+ session: row.session,
258
+ toolCallId: row.toolCallId,
259
+ toolName: row.toolName,
260
+ target: row.target,
261
+ action: row.action,
262
+ risk: row.risk,
263
+ approvalContext: row.context,
264
+ }, MAX_APPROVAL_CONTEXT_LENGTH);
265
+ }
266
+ function grantSourceHash(row) {
267
+ return `approval:${row.id}:${row.toolCallId}:${row.risk}`;
268
+ }
269
+ function encodeApprovalOptions(options) {
270
+ if (!options || options.length === 0) {
271
+ return undefined;
272
+ }
273
+ return safeJsonStringify(options);
274
+ }
275
+ function parseApprovalOptions(value) {
155
276
  if (typeof value !== 'string' || !value.trim()) {
156
- return null;
277
+ return undefined;
157
278
  }
158
279
  try {
159
280
  const parsed = JSON.parse(value);
160
- if (!isRecord(parsed)) {
161
- return null;
162
- }
163
- const kind = normalizeString(parsed.kind);
164
- const message = normalizeString(parsed.message);
165
- const backend = normalizeString(parsed.backend);
166
- const sessionId = normalizeString(parsed.sessionId);
167
- if (!kind || !message || !backend || !sessionId) {
168
- return null;
281
+ if (!Array.isArray(parsed)) {
282
+ return undefined;
169
283
  }
170
- return {
171
- kind: kind,
172
- message,
173
- backend: backend,
174
- sessionId,
175
- ...(normalizeString(parsed.command) ? { command: normalizeString(parsed.command) } : {}),
176
- ...(normalizeString(parsed.cwd) ? { cwd: normalizeString(parsed.cwd) } : {}),
177
- };
284
+ const options = parsed
285
+ .map((option) => {
286
+ if (!isRecord(option)) {
287
+ return null;
288
+ }
289
+ const optionId = normalizeString(option.optionId);
290
+ const label = normalizeString(option.label);
291
+ if (!optionId || !label) {
292
+ return null;
293
+ }
294
+ const kind = normalizeString(option.kind);
295
+ const description = normalizeString(option.description);
296
+ return {
297
+ optionId,
298
+ label,
299
+ ...(kind ? { kind } : {}),
300
+ ...(description ? { description } : {}),
301
+ };
302
+ })
303
+ .filter((option) => option !== null);
304
+ return options.length > 0 ? options : undefined;
178
305
  }
179
306
  catch {
180
- return null;
307
+ return undefined;
181
308
  }
182
309
  }
310
+ function normalizeDateLike(value) {
311
+ if (value instanceof Date) {
312
+ return Number.isFinite(value.getTime()) ? value.toISOString() : undefined;
313
+ }
314
+ if (typeof value !== 'string' || !value.trim()) {
315
+ return undefined;
316
+ }
317
+ const parsed = new Date(value);
318
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : undefined;
319
+ }
320
+ function resolveApprovalExpiresAt(request, now) {
321
+ const explicit = normalizeDateLike(request.expiresAt);
322
+ if (explicit) {
323
+ return explicit;
324
+ }
325
+ if (typeof request.timeoutMs === 'number' && Number.isFinite(request.timeoutMs) && request.timeoutMs > 0) {
326
+ return new Date(now.getTime() + request.timeoutMs);
327
+ }
328
+ return undefined;
329
+ }
183
330
  function extractSessionId(sessionUri) {
184
331
  if (sessionUri.includes('#')) {
185
332
  return sessionUri.split('#').pop() || sessionUri;
@@ -200,36 +347,42 @@ function decisionFromApprovalRow(row) {
200
347
  }
201
348
  return 'accept';
202
349
  }
203
- function requestAuditForApproval(approvalUri, audits) {
204
- const matches = audits.filter((audit) => audit.approval === approvalUri && audit.action === 'approval_requested');
205
- matches.sort((left, right) => toIsoString(right.createdAt, '').localeCompare(toIsoString(left.createdAt, '')));
206
- return matches[0];
207
- }
208
- function normalizeApprovalSummary(row, audits) {
209
- const approvalUri = buildApprovalUri(row.session, row.id);
210
- const requestAudit = requestAuditForApproval(approvalUri, audits);
211
- const requestContext = parseRequestAuditContext(requestAudit?.context);
350
+ function normalizeApprovalSummary(row) {
212
351
  const createdAt = toIsoString(row.createdAt, new Date(0).toISOString());
213
352
  const sessionUri = row.session;
214
353
  const decision = decisionFromApprovalRow(row);
354
+ const approvalOptions = parseApprovalOptions(row.approvalOptions);
215
355
  return {
216
356
  id: row.id,
357
+ ...(normalizeString(row.approvalUri) ? { approvalUri: normalizeString(row.approvalUri) } : {}),
217
358
  sessionId: extractSessionId(sessionUri),
218
359
  sessionUri,
219
360
  toolCallId: row.toolCallId,
220
361
  toolName: row.toolName,
221
362
  risk: normalizeString(row.risk) ?? 'medium',
222
363
  status: normalizeString(row.status) ?? 'pending',
223
- message: requestContext?.message ?? row.toolName,
224
- ...(requestContext?.command ? { command: requestContext.command } : {}),
225
- ...(requestContext?.cwd ? { cwd: requestContext.cwd } : {}),
364
+ message: formatApprovalMessage(row),
226
365
  ...(normalizeString(row.assignedTo) ? { assignedTo: normalizeString(row.assignedTo) } : {}),
227
366
  ...(normalizeString(row.decisionBy) ? { decisionBy: normalizeString(row.decisionBy) } : {}),
228
367
  ...(decision ? { decision } : {}),
368
+ ...(approvalOptions ? { approvalOptions } : {}),
229
369
  createdAt,
370
+ ...(row.expiresAt ? { expiresAt: toIsoString(row.expiresAt, createdAt) } : {}),
230
371
  ...(row.resolvedAt ? { resolvedAt: toIsoString(row.resolvedAt, createdAt) } : {}),
231
372
  };
232
373
  }
374
+ function formatApprovalMessage(row) {
375
+ if (row.toolName === 'commandExecution') {
376
+ return 'Command execution approval';
377
+ }
378
+ if (row.toolName === 'fileChange') {
379
+ return 'File change approval';
380
+ }
381
+ if (row.toolName === 'permissionRequest') {
382
+ return 'Permission approval';
383
+ }
384
+ return row.toolName;
385
+ }
233
386
  function formatSummaryHeadline(summary) {
234
387
  return `${summary.id} | ${summary.status} | ${summary.risk} | session=${summary.sessionId}`;
235
388
  }
@@ -248,72 +401,11 @@ export function isRemoteApprovalAbortError(error) {
248
401
  function missingRemoteApprovalCredentialsMessage() {
249
402
  return 'LinX remote approval requires `linx login` first.';
250
403
  }
251
- function unsupportedRemoteApprovalAuthMessage() {
252
- return 'LinX remote approval requires client credentials auth in `~/.linx`.';
253
- }
254
404
  async function createDefaultRuntime() {
255
- const [credentialsStore, solidAuth, models] = await Promise.all([
256
- dynamicImport(new URL('../credentials-store.js', import.meta.url).href),
257
- dynamicImport(new URL('../solid-auth.js', import.meta.url).href),
258
- dynamicImport(new URL('...co/models.js', import.meta.url).href),
259
- ]);
260
405
  return {
261
- loadCredentials: credentialsStore.loadCredentials,
262
- getClientCredentials: credentialsStore.getClientCredentials,
263
- authenticate: solidAuth.authenticate,
264
- createStore(session) {
265
- const db = models.drizzle(session, {
266
- logger: false,
267
- disableInteropDiscovery: true,
268
- schema: models.solidSchema,
269
- });
270
- let initialized = false;
271
- async function ensureInitialized() {
272
- if (initialized) {
273
- return;
274
- }
275
- initialized = true;
276
- await db.init([
277
- models.approvalTable,
278
- models.auditTable,
279
- models.grantTable,
280
- models.inboxNotificationTable,
281
- ]).catch(() => undefined);
282
- }
283
- return {
284
- async listApprovals() {
285
- await ensureInitialized();
286
- return await db.select().from(models.approvalTable).execute();
287
- },
288
- async insertApproval(row) {
289
- await ensureInitialized();
290
- await db.insert(models.approvalTable).values(row).execute();
291
- },
292
- async updateApproval(id, patch) {
293
- await ensureInitialized();
294
- await db.update(models.approvalTable).set(patch).where(models.eq(models.approvalTable.id, id)).execute();
295
- },
296
- async listAudits() {
297
- await ensureInitialized();
298
- return await db.select().from(models.auditTable).execute();
299
- },
300
- async insertAudit(row) {
301
- await ensureInitialized();
302
- await db.insert(models.auditTable).values(row).execute();
303
- },
304
- async listGrants() {
305
- await ensureInitialized();
306
- return await db.select().from(models.grantTable).execute();
307
- },
308
- async insertGrant(row) {
309
- await ensureInitialized();
310
- await db.insert(models.grantTable).values(row).execute();
311
- },
312
- async insertInboxNotification(row) {
313
- await ensureInitialized();
314
- await db.insert(models.inboxNotificationTable).values(row).execute();
315
- },
316
- };
406
+ getPodDataSession: getDefaultPodDataSession,
407
+ createStore(session, db) {
408
+ return createNativeRemoteApprovalStore(session.webId, db);
317
409
  },
318
410
  sleep(ms) {
319
411
  return delay(ms);
@@ -321,100 +413,523 @@ async function createDefaultRuntime() {
321
413
  now() {
322
414
  return new Date();
323
415
  },
416
+ resolveGrantCoverage: resolveWatchGrantCoverage,
324
417
  };
325
418
  }
326
419
  async function withRemoteApprovalStore(runtime, fn) {
327
- const stored = runtime.loadCredentials();
328
- if (!stored) {
420
+ const client = await getRemoteApprovalClient(runtime);
421
+ if (!client) {
329
422
  throw new Error(missingRemoteApprovalCredentialsMessage());
330
423
  }
331
- const clientCredentials = runtime.getClientCredentials(stored);
332
- if (!clientCredentials) {
333
- throw new Error(unsupportedRemoteApprovalAuthMessage());
424
+ return await fn({
425
+ store: client.store,
426
+ webId: client.session.webId,
427
+ stored: client.session.credentials,
428
+ });
429
+ }
430
+ async function getRemoteApprovalClient(runtime) {
431
+ let promise = remoteApprovalClientCache.get(runtime);
432
+ if (!promise) {
433
+ promise = createRemoteApprovalClient(runtime)
434
+ .then((client) => {
435
+ if (!client) {
436
+ remoteApprovalClientCache.delete(runtime);
437
+ }
438
+ return client;
439
+ })
440
+ .catch((error) => {
441
+ remoteApprovalClientCache.delete(runtime);
442
+ throw error;
443
+ });
444
+ remoteApprovalClientCache.set(runtime, promise);
334
445
  }
335
- const { session } = await runtime.authenticate(clientCredentials.clientId, clientCredentials.clientSecret, stored.url);
336
- const webId = session.info.webId ?? stored.webId;
337
- if (!webId) {
338
- await session.logout().catch(() => undefined);
339
- throw new Error('Remote approval authentication succeeded without a WebID.');
446
+ return promise;
447
+ }
448
+ async function createRemoteApprovalClient(runtime) {
449
+ const session = await runtime.getPodDataSession();
450
+ if (!session) {
451
+ return null;
340
452
  }
341
- try {
342
- return await fn({
343
- store: runtime.createStore(session),
344
- webId,
345
- stored,
346
- });
453
+ const db = createRemoteApprovalDb(session);
454
+ await initSolidTables(db, [
455
+ approvalResource,
456
+ auditResource,
457
+ grantResource,
458
+ inboxNotificationTable,
459
+ ]);
460
+ return {
461
+ session,
462
+ store: runtime.createStore(session, db),
463
+ };
464
+ }
465
+ function createRemoteApprovalDb(session) {
466
+ return drizzle(session.solidSession, {
467
+ logger: false,
468
+ disableInteropDiscovery: true,
469
+ schema: solidSchema,
470
+ });
471
+ }
472
+ function createNativeRemoteApprovalStore(_webId, db) {
473
+ return {
474
+ listApprovals: () => listApprovalRows(db),
475
+ findApproval: (id, options) => findApprovalRow(db, id, options),
476
+ resolveApprovalReference: (locator) => resolveResourceReference(db, approvalResource, locator),
477
+ insertApproval: (row) => writeApprovalRow(db, row),
478
+ async updateApproval(id, patch) {
479
+ const data = normalizeApprovalUpdate(patch);
480
+ const approvalUri = normalizeString(patch.approvalUri);
481
+ const targetIri = approvalUri ?? (isAbsoluteIri(id) ? id : undefined);
482
+ if (targetIri) {
483
+ const updateByIri = db.updateByIri;
484
+ if (typeof updateByIri !== 'function') {
485
+ throw new Error('Solid database does not support updateByIri');
486
+ }
487
+ const updated = await updateByIri.call(db, approvalResource, targetIri, data);
488
+ if (!updated) {
489
+ throw new Error(`Remote approval not found: ${id}`);
490
+ }
491
+ return;
492
+ }
493
+ const updateByLocator = db.updateByLocator;
494
+ if (typeof updateByLocator !== 'function') {
495
+ throw new Error('Solid database does not support updateByLocator');
496
+ }
497
+ const updated = await updateByLocator.call(db, approvalResource, { id }, data);
498
+ if (!updated) {
499
+ throw new Error(`Remote approval not found: ${id}`);
500
+ }
501
+ },
502
+ listAudits: () => listAuditRows(db),
503
+ insertAudit: (row) => writeAuditRow(db, row),
504
+ listGrants: () => listGrantRows(db),
505
+ resolveGrantReference: (locator) => resolveResourceReference(db, grantResource, locator),
506
+ insertGrant: (row) => writeGrantRow(db, row),
507
+ insertInboxNotification: (row) => writeInboxNotificationRow(db, row),
508
+ };
509
+ }
510
+ async function findApprovalRow(db, id, options = {}) {
511
+ const approvalUri = normalizeString(options.approvalUri) ?? (isAbsoluteIri(id) ? id : undefined);
512
+ if (approvalUri) {
513
+ const findByIri = db.findByIri;
514
+ if (typeof findByIri !== 'function') {
515
+ throw new Error('Solid database does not support findByIri');
516
+ }
517
+ const row = await findByIri.call(db, approvalResource, approvalUri);
518
+ return normalizeApprovalRow(row);
347
519
  }
348
- finally {
349
- await session.logout().catch(() => undefined);
520
+ const findByLocator = db.findByLocator;
521
+ if (typeof findByLocator !== 'function') {
522
+ throw new Error('Solid database does not support findByLocator');
523
+ }
524
+ const row = await findByLocator.call(db, approvalResource, {
525
+ id,
526
+ ...(options.createdAt ? { createdAt: options.createdAt } : {}),
527
+ });
528
+ return normalizeApprovalRow(row);
529
+ }
530
+ function resolveResourceReference(db, resource, locator) {
531
+ if (typeof db.resolveLocatorIri !== 'function' || typeof db.resolveLocatorId !== 'function') {
532
+ throw new Error('Solid database does not support locator reference resolution');
533
+ }
534
+ return {
535
+ id: db.resolveLocatorId(resource, locator),
536
+ iri: db.resolveLocatorIri(resource, locator),
537
+ };
538
+ }
539
+ async function listApprovalRows(db) {
540
+ const rows = await db.select().from(approvalResource).execute();
541
+ return rows.map((row) => normalizeApprovalRow(row)).filter((row) => row !== null);
542
+ }
543
+ async function writeApprovalRow(db, row) {
544
+ await db.insert(approvalResource).values(normalizeApprovalInsert(row)).execute();
545
+ }
546
+ async function listAuditRows(db) {
547
+ const rows = await db.select().from(auditResource).execute();
548
+ return rows.map((row) => normalizeAuditRow(row)).filter((row) => row !== null);
549
+ }
550
+ async function writeAuditRow(db, row) {
551
+ await db.insert(auditResource).values(normalizeAuditInsert(row)).execute();
552
+ }
553
+ async function listGrantRows(db) {
554
+ const rows = await db.select().from(grantResource).execute();
555
+ return rows.map((row) => normalizeGrantRow(row)).filter((row) => row !== null);
556
+ }
557
+ async function writeGrantRow(db, row) {
558
+ const id = normalizeString(row.id) ?? crypto.randomUUID();
559
+ const target = normalizeString(row.target);
560
+ const action = normalizeString(row.action);
561
+ const effect = normalizeString(row.effect);
562
+ const decisionBy = normalizeString(row.decisionBy);
563
+ const decisionRole = normalizeString(row.decisionRole);
564
+ if (!target || !action || !effect || !decisionBy || !decisionRole) {
565
+ throw new Error(`Invalid remote approval grant row: ${id}`);
350
566
  }
567
+ await db.insert(grantResource).values(normalizeGrantInsert({ ...row, id, target, action, effect, decisionBy, decisionRole })).execute();
568
+ }
569
+ async function writeInboxNotificationRow(db, row) {
570
+ await db.insert(inboxNotificationTable).values(normalizeInboxNotificationInsert(row)).execute();
571
+ }
572
+ function normalizeApprovalRow(row) {
573
+ if (!row)
574
+ return null;
575
+ return {
576
+ id: String(row.id),
577
+ approvalUri: normalizeString(row['@id']) ?? normalizeString(row.subject) ?? normalizeString(row.uri),
578
+ session: row.session,
579
+ toolCallId: row.toolCallId,
580
+ toolName: row.toolName,
581
+ target: row.target,
582
+ action: row.action,
583
+ risk: row.risk,
584
+ status: row.status,
585
+ assignedTo: row.assignedTo,
586
+ decisionBy: row.decisionBy,
587
+ decisionRole: row.decisionRole,
588
+ onBehalfOf: row.onBehalfOf,
589
+ reason: row.reason,
590
+ context: row.context,
591
+ approvalOptions: row.approvalOptions,
592
+ policyVersion: row.policyVersion,
593
+ createdAt: row.createdAt,
594
+ expiresAt: row.expiresAt,
595
+ resolvedAt: row.resolvedAt,
596
+ };
597
+ }
598
+ function normalizeAuditRow(row) {
599
+ if (!row)
600
+ return null;
601
+ return {
602
+ id: String(row.id),
603
+ action: row.action,
604
+ actor: row.actor,
605
+ actorRole: row.actorRole,
606
+ onBehalfOf: row.onBehalfOf,
607
+ session: row.session,
608
+ entry: row.entry,
609
+ toolCallId: row.toolCallId,
610
+ toolName: row.toolName,
611
+ approval: row.approval,
612
+ policyVersion: row.policyVersion,
613
+ createdAt: row.createdAt,
614
+ };
615
+ }
616
+ function normalizeGrantRow(row) {
617
+ if (!row)
618
+ return null;
619
+ return {
620
+ id: String(row.id),
621
+ target: row.target,
622
+ action: row.action,
623
+ title: row.title,
624
+ summary: row.summary,
625
+ body: row.body,
626
+ schema: row.schema,
627
+ pageKind: row.pageKind,
628
+ wikiStatus: row.wikiStatus,
629
+ tags: row.tags,
630
+ source: row.source,
631
+ sourceHash: row.sourceHash,
632
+ compiledAt: row.compiledAt,
633
+ compiledFrom: row.compiledFrom,
634
+ related: row.related,
635
+ effect: row.effect,
636
+ riskCeiling: row.riskCeiling,
637
+ policy: row.policy,
638
+ context: row.context,
639
+ decisionBy: row.decisionBy,
640
+ decisionRole: row.decisionRole,
641
+ onBehalfOf: row.onBehalfOf,
642
+ createdAt: row.createdAt,
643
+ revokedAt: row.revokedAt,
644
+ };
645
+ }
646
+ function normalizeApprovalInsert(row) {
647
+ return {
648
+ id: row.id,
649
+ session: row.session,
650
+ toolCallId: row.toolCallId,
651
+ toolName: row.toolName,
652
+ target: row.target,
653
+ action: row.action,
654
+ risk: row.risk,
655
+ status: row.status,
656
+ ...(row.assignedTo ? { assignedTo: row.assignedTo } : {}),
657
+ ...(row.decisionBy ? { decisionBy: row.decisionBy } : {}),
658
+ ...(row.decisionRole ? { decisionRole: row.decisionRole } : {}),
659
+ ...(row.onBehalfOf ? { onBehalfOf: row.onBehalfOf } : {}),
660
+ ...(row.reason ? { reason: row.reason } : {}),
661
+ ...(row.context ? { context: row.context } : {}),
662
+ ...(row.approvalOptions ? { approvalOptions: row.approvalOptions } : {}),
663
+ ...(row.policyVersion ? { policyVersion: row.policyVersion } : {}),
664
+ createdAt: new Date(toIsoString(row.createdAt, new Date().toISOString())),
665
+ ...(row.expiresAt ? { expiresAt: new Date(toIsoString(row.expiresAt, new Date().toISOString())) } : {}),
666
+ ...(row.resolvedAt ? { resolvedAt: new Date(toIsoString(row.resolvedAt, new Date().toISOString())) } : {}),
667
+ };
668
+ }
669
+ function normalizeApprovalUpdate(patch) {
670
+ const next = {};
671
+ for (const key of [
672
+ 'session',
673
+ 'toolCallId',
674
+ 'toolName',
675
+ 'target',
676
+ 'action',
677
+ 'risk',
678
+ 'status',
679
+ 'assignedTo',
680
+ 'decisionBy',
681
+ 'decisionRole',
682
+ 'onBehalfOf',
683
+ 'reason',
684
+ 'context',
685
+ 'approvalOptions',
686
+ 'policyVersion',
687
+ ]) {
688
+ if (patch[key] !== undefined) {
689
+ ;
690
+ next[key] = patch[key];
691
+ }
692
+ }
693
+ if (patch.createdAt !== undefined)
694
+ next.createdAt = new Date(toIsoString(patch.createdAt, new Date().toISOString()));
695
+ if (patch.expiresAt !== undefined)
696
+ next.expiresAt = new Date(toIsoString(patch.expiresAt, new Date().toISOString()));
697
+ if (patch.resolvedAt !== undefined)
698
+ next.resolvedAt = new Date(toIsoString(patch.resolvedAt, new Date().toISOString()));
699
+ return next;
700
+ }
701
+ function normalizeAuditInsert(row) {
702
+ return {
703
+ id: row.id,
704
+ action: row.action,
705
+ actor: row.actor,
706
+ actorRole: row.actorRole,
707
+ ...(row.onBehalfOf ? { onBehalfOf: row.onBehalfOf } : {}),
708
+ ...(row.session ? { session: row.session } : {}),
709
+ ...(row.entry ? { entry: row.entry } : {}),
710
+ ...(row.toolCallId ? { toolCallId: row.toolCallId } : {}),
711
+ ...(row.toolName ? { toolName: row.toolName } : {}),
712
+ ...(row.approval ? { approval: row.approval } : {}),
713
+ ...(row.policyVersion ? { policyVersion: row.policyVersion } : {}),
714
+ createdAt: new Date(toIsoString(row.createdAt, new Date().toISOString())),
715
+ };
716
+ }
717
+ function normalizeGrantInsert(row) {
718
+ return {
719
+ id: row.id,
720
+ target: row.target,
721
+ action: row.action,
722
+ ...(row.title ? { title: truncatePodLiteral(row.title, 160) } : {}),
723
+ ...(row.summary ? { summary: truncatePodLiteral(row.summary, 500) } : {}),
724
+ ...(row.body ? { body: truncatePodLiteral(row.body, MAX_GRANT_POLICY_LENGTH) } : {}),
725
+ ...(row.schema ? { schema: row.schema } : {}),
726
+ ...(row.pageKind ? { pageKind: row.pageKind } : {}),
727
+ ...(row.wikiStatus ? { wikiStatus: row.wikiStatus } : {}),
728
+ ...(row.tags ? { tags: truncatePodLiteral(row.tags, 500) } : {}),
729
+ ...(row.source ? { source: row.source } : {}),
730
+ ...(row.sourceHash ? { sourceHash: row.sourceHash } : {}),
731
+ ...(row.compiledAt ? { compiledAt: new Date(toIsoString(row.compiledAt, new Date().toISOString())) } : {}),
732
+ ...(row.compiledFrom ? { compiledFrom: row.compiledFrom } : {}),
733
+ ...(row.related ? { related: row.related } : {}),
734
+ effect: row.effect,
735
+ ...(row.riskCeiling ? { riskCeiling: row.riskCeiling } : {}),
736
+ ...(row.policy ? { policy: truncatePodLiteral(row.policy, MAX_GRANT_POLICY_LENGTH) } : {}),
737
+ ...(row.context ? { context: truncatePodLiteral(row.context, MAX_APPROVAL_CONTEXT_LENGTH) } : {}),
738
+ decisionBy: row.decisionBy,
739
+ decisionRole: row.decisionRole,
740
+ ...(row.onBehalfOf ? { onBehalfOf: row.onBehalfOf } : {}),
741
+ createdAt: new Date(toIsoString(row.createdAt, new Date().toISOString())),
742
+ ...(row.revokedAt ? { revokedAt: new Date(toIsoString(row.revokedAt, new Date().toISOString())) } : {}),
743
+ };
744
+ }
745
+ function normalizeInboxNotificationInsert(row) {
746
+ return {
747
+ id: row.id,
748
+ ...(row.actor ? { actor: row.actor } : {}),
749
+ object: row.object,
750
+ createdAt: new Date(toIsoString(row.createdAt, new Date().toISOString())),
751
+ };
752
+ }
753
+ function isActiveAllowGrant(grant) {
754
+ return grant.effect === 'allow' && !grant.revokedAt && !!(normalizeString(grant.body) || normalizeString(grant.policy));
755
+ }
756
+ function isGrantRiskCandidate(grant, requestRisk) {
757
+ const ceiling = riskScore(typeof grant.riskCeiling === 'string' ? grant.riskCeiling : undefined);
758
+ return ceiling === 0 || ceiling >= riskScore(requestRisk);
759
+ }
760
+ function rankGrantCandidate(grant, requestContext) {
761
+ let score = 0;
762
+ if (grant.target === requestContext.target) {
763
+ score += 4;
764
+ }
765
+ if (grant.action === requestContext.action) {
766
+ score += 3;
767
+ }
768
+ if (grant.schema) {
769
+ score += 2;
770
+ }
771
+ if (grant.pageKind === 'autonomy-grant') {
772
+ score += 1;
773
+ }
774
+ return score;
775
+ }
776
+ function selectSemanticGrantCandidates(grants, requestContext) {
777
+ const risk = normalizeString(requestContext.risk) ?? 'medium';
778
+ return grants
779
+ .filter((grant) => isActiveAllowGrant(grant) && isGrantRiskCandidate(grant, risk))
780
+ .sort((left, right) => rankGrantCandidate(right, requestContext) - rankGrantCandidate(left, requestContext))
781
+ .slice(0, MAX_GRANT_COVERAGE_CANDIDATES);
782
+ }
783
+ function acceptsGrantCoverage(decision) {
784
+ return decision?.covers === true
785
+ && typeof decision.confidence === 'number'
786
+ && decision.confidence >= MIN_GRANT_COVERAGE_CONFIDENCE;
787
+ }
788
+ async function resolveSemanticGrantDecision(options) {
789
+ const candidates = selectSemanticGrantCandidates(options.grants, options.requestContext);
790
+ if (candidates.length === 0) {
791
+ return null;
792
+ }
793
+ const resolver = options.runtime.resolveGrantCoverage ?? resolveWatchGrantCoverage;
794
+ for (const grant of candidates) {
795
+ const coverage = await resolver({
796
+ record: options.record,
797
+ request: options.request,
798
+ requestContext: options.requestContext,
799
+ grant,
800
+ }).catch(() => null);
801
+ if (acceptsGrantCoverage(coverage)) {
802
+ return 'accept_for_session';
803
+ }
804
+ }
805
+ return null;
806
+ }
807
+ function buildWatchGrantRequestContext(input) {
808
+ return {
809
+ session: buildThreadUri(input.webId, input.record),
810
+ target: buildThreadUri(input.webId, input.record),
811
+ action: buildActionUri(input.request),
812
+ risk: buildRisk(input.request),
813
+ toolName: buildToolName(input.request),
814
+ cwd: input.record.cwd,
815
+ backend: input.record.backend,
816
+ mode: input.record.mode,
817
+ };
818
+ }
819
+ function buildGenericGrantRequestContext(input) {
820
+ return {
821
+ session: input.subject.sessionUri,
822
+ target: input.subject.targetUri ?? input.subject.sessionUri,
823
+ action: input.request.action,
824
+ risk: input.request.risk,
825
+ toolName: input.request.toolName,
826
+ cwd: input.request.cwd,
827
+ kind: input.request.kind,
828
+ };
351
829
  }
352
830
  export async function createRemoteWatchApproval(options) {
353
831
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
354
- return withRemoteApprovalStore(activeRuntime, async ({ store, webId }) => {
355
- const approvalId = crypto.randomUUID();
832
+ return createRemoteApproval({
833
+ subject: ({ webId }) => ({
834
+ sessionUri: buildThreadUri(webId, options.record),
835
+ actorUri: buildAgentUri(webId),
836
+ policyVersion: REMOTE_APPROVAL_POLICY_VERSION,
837
+ }),
838
+ request: ({ sessionUri }) => ({
839
+ kind: options.request.kind,
840
+ message: buildRequestMessage(options.request),
841
+ toolCallId: extractToolCallId(options.request),
842
+ toolName: buildToolName(options.request),
843
+ action: buildActionUri(options.request),
844
+ risk: buildRisk(options.request),
845
+ ...(options.request.kind === 'command-approval' && options.request.command ? { command: options.request.command } : {}),
846
+ ...(options.request.kind === 'command-approval' && options.request.cwd ? { cwd: options.request.cwd } : {}),
847
+ ...(options.request.approvalOptions ? { approvalOptions: options.request.approvalOptions } : {}),
848
+ ...(options.request.timeoutMs ? { timeoutMs: options.request.timeoutMs } : {}),
849
+ ...(options.request.expiresAt ? { expiresAt: options.request.expiresAt } : {}),
850
+ entry: sessionUri,
851
+ }),
852
+ runtime: activeRuntime,
853
+ });
854
+ }
855
+ export async function createRemoteApproval(options) {
856
+ const activeRuntime = options.runtime ?? await createDefaultRuntime();
857
+ return withRemoteApprovalStore(activeRuntime, async ({ store, webId, stored }) => {
858
+ const subject = typeof options.subject === 'function'
859
+ ? options.subject({ webId, stored })
860
+ : options.subject;
861
+ const request = typeof options.request === 'function'
862
+ ? options.request({ webId, stored, sessionUri: subject.sessionUri })
863
+ : options.request;
864
+ const approvalLocalId = crypto.randomUUID();
356
865
  const now = activeRuntime.now();
357
- const sessionUri = buildThreadUri(webId, options.record.id);
358
- const approvalUri = buildApprovalUri(webId, approvalId);
359
- const toolCallId = extractToolCallId(options.request);
360
- const requestContext = buildRequestAuditContext(options.record, options.request);
866
+ const approvalReference = store.resolveApprovalReference({ id: approvalLocalId, createdAt: now });
867
+ const approvalId = approvalReference.id;
868
+ const sessionUri = subject.sessionUri;
869
+ const approvalUri = approvalReference.iri;
870
+ const targetUri = subject.targetUri ?? sessionUri;
871
+ const assignedTo = subject.assignedTo ?? webId;
872
+ const onBehalfOf = subject.onBehalfOf ?? webId;
873
+ const policyVersion = subject.policyVersion ?? REMOTE_APPROVAL_POLICY_VERSION;
874
+ const requestEntry = request.entry ?? approvalUri;
875
+ const expiresAt = resolveApprovalExpiresAt(request, now);
876
+ const approvalOptions = encodeApprovalOptions(request.approvalOptions);
877
+ const context = compactApprovalContext(request);
361
878
  await store.insertApproval({
362
879
  id: approvalId,
880
+ approvalUri,
363
881
  session: sessionUri,
364
- toolCallId,
365
- toolName: buildToolName(options.request),
366
- target: sessionUri,
367
- action: buildActionUri(options.request),
368
- risk: buildRisk(options.request),
882
+ toolCallId: request.toolCallId,
883
+ toolName: request.toolName,
884
+ target: targetUri,
885
+ action: request.action,
886
+ risk: request.risk,
369
887
  status: 'pending',
370
- assignedTo: webId,
371
- policyVersion: REMOTE_APPROVAL_POLICY_VERSION,
888
+ assignedTo,
889
+ context,
890
+ ...(approvalOptions ? { approvalOptions } : {}),
891
+ policyVersion,
372
892
  createdAt: now,
893
+ ...(expiresAt ? { expiresAt } : {}),
373
894
  });
374
- await store.insertAudit({
895
+ const requestAudit = {
375
896
  id: crypto.randomUUID(),
376
897
  action: 'approval_requested',
377
- actor: buildAgentUri(webId),
898
+ actor: subject.actorUri,
378
899
  actorRole: 'secretary',
379
- onBehalfOf: webId,
900
+ onBehalfOf,
380
901
  session: sessionUri,
381
- toolCallId,
902
+ entry: requestEntry,
903
+ toolCallId: request.toolCallId,
904
+ toolName: request.toolName,
382
905
  approval: approvalUri,
383
- context: JSON.stringify(requestContext),
384
- policyVersion: REMOTE_APPROVAL_POLICY_VERSION,
906
+ policyVersion,
385
907
  createdAt: now,
386
- });
387
- await store.insertInboxNotification({
908
+ };
909
+ await warnOnly(activeRuntime, () => store.insertAudit(requestAudit));
910
+ await warnOnly(activeRuntime, () => store.insertInboxNotification({
388
911
  id: crypto.randomUUID(),
389
- actor: buildAgentUri(webId),
912
+ actor: subject.actorUri,
390
913
  object: approvalUri,
391
914
  createdAt: now,
392
- }).catch(() => undefined);
915
+ }));
393
916
  return normalizeApprovalSummary({
394
917
  id: approvalId,
918
+ approvalUri,
395
919
  session: sessionUri,
396
- toolCallId,
397
- toolName: buildToolName(options.request),
398
- target: sessionUri,
399
- action: buildActionUri(options.request),
400
- risk: buildRisk(options.request),
920
+ toolCallId: request.toolCallId,
921
+ toolName: request.toolName,
922
+ target: targetUri,
923
+ action: request.action,
924
+ risk: request.risk,
401
925
  status: 'pending',
402
- assignedTo: webId,
403
- policyVersion: REMOTE_APPROVAL_POLICY_VERSION,
926
+ assignedTo,
927
+ context,
928
+ ...(approvalOptions ? { approvalOptions } : {}),
929
+ policyVersion,
404
930
  createdAt: now,
405
- }, [{
406
- id: crypto.randomUUID(),
407
- action: 'approval_requested',
408
- actor: buildAgentUri(webId),
409
- actorRole: 'secretary',
410
- onBehalfOf: webId,
411
- session: sessionUri,
412
- toolCallId,
413
- approval: approvalUri,
414
- context: JSON.stringify(requestContext),
415
- policyVersion: REMOTE_APPROVAL_POLICY_VERSION,
416
- createdAt: now,
417
- }]);
931
+ ...(expiresAt ? { expiresAt } : {}),
932
+ });
418
933
  });
419
934
  }
420
935
  export async function waitForRemoteWatchApproval(options) {
@@ -424,10 +939,13 @@ export async function waitForRemoteWatchApproval(options) {
424
939
  if (options.signal?.aborted) {
425
940
  throw createAbortError();
426
941
  }
427
- const approvals = await store.listApprovals();
428
- const row = approvals.find((entry) => entry.id === options.approvalId);
942
+ const row = await readRemoteApprovalRow(store, {
943
+ approvalId: options.approvalId,
944
+ approvalUri: options.approvalUri,
945
+ });
429
946
  if (!row) {
430
- throw new Error(`Remote approval disappeared before resolution: ${options.approvalId}`);
947
+ await activeRuntime.sleep(options.pollMs ?? DEFAULT_REMOTE_APPROVAL_POLL_MS);
948
+ continue;
431
949
  }
432
950
  const decision = decisionFromApprovalRow(row);
433
951
  if (decision) {
@@ -441,17 +959,20 @@ export async function requestRemoteWatchApproval(options) {
441
959
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
442
960
  const delegated = await withRemoteApprovalStore(activeRuntime, async ({ store, webId }) => {
443
961
  const grants = await store.listGrants();
444
- const requestAction = buildActionUri(options.request);
445
- const requestTarget = buildThreadUri(webId, options.record.id);
446
- const requestRisk = buildRisk(options.request);
447
- return grants.some((grant) => (grant.effect === 'allow'
448
- && grant.action === requestAction
449
- && grant.target === requestTarget
450
- && riskScore(typeof grant.riskCeiling === 'string' ? grant.riskCeiling : undefined) >= riskScore(requestRisk)
451
- && !grant.revokedAt));
962
+ return resolveSemanticGrantDecision({
963
+ runtime: activeRuntime,
964
+ grants,
965
+ record: options.record,
966
+ request: options.request,
967
+ requestContext: buildWatchGrantRequestContext({
968
+ webId,
969
+ record: options.record,
970
+ request: options.request,
971
+ }),
972
+ });
452
973
  });
453
974
  if (delegated) {
454
- return 'accept_for_session';
975
+ return delegated;
455
976
  }
456
977
  const summary = await createRemoteWatchApproval({
457
978
  record: options.record,
@@ -460,6 +981,62 @@ export async function requestRemoteWatchApproval(options) {
460
981
  });
461
982
  return waitForRemoteWatchApproval({
462
983
  approvalId: summary.id,
984
+ approvalUri: summary.approvalUri,
985
+ pollMs: options.pollMs,
986
+ signal: options.signal,
987
+ runtime: activeRuntime,
988
+ });
989
+ }
990
+ export async function resolveExistingRemoteWatchGrant(options) {
991
+ const activeRuntime = options.runtime ?? await createDefaultRuntime();
992
+ return withRemoteApprovalStore(activeRuntime, async ({ store, webId }) => {
993
+ const grants = await store.listGrants();
994
+ return resolveSemanticGrantDecision({
995
+ runtime: activeRuntime,
996
+ grants,
997
+ record: options.record,
998
+ request: options.request,
999
+ requestContext: buildWatchGrantRequestContext({
1000
+ webId,
1001
+ record: options.record,
1002
+ request: options.request,
1003
+ }),
1004
+ });
1005
+ });
1006
+ }
1007
+ export async function requestRemoteApproval(options) {
1008
+ const activeRuntime = options.runtime ?? await createDefaultRuntime();
1009
+ const delegated = await withRemoteApprovalStore(activeRuntime, async ({ store, webId, stored }) => {
1010
+ const subject = typeof options.subject === 'function'
1011
+ ? options.subject({ webId, stored })
1012
+ : options.subject;
1013
+ const request = typeof options.request === 'function'
1014
+ ? options.request({ webId, stored, sessionUri: subject.sessionUri })
1015
+ : options.request;
1016
+ const grants = await store.listGrants();
1017
+ const requestContext = buildGenericGrantRequestContext({ subject, request });
1018
+ return resolveSemanticGrantDecision({
1019
+ runtime: activeRuntime,
1020
+ grants,
1021
+ request: {
1022
+ ...request,
1023
+ session: subject.sessionUri,
1024
+ target: requestContext.target,
1025
+ },
1026
+ requestContext,
1027
+ });
1028
+ });
1029
+ if (delegated) {
1030
+ return delegated;
1031
+ }
1032
+ const summary = await createRemoteApproval({
1033
+ subject: options.subject,
1034
+ request: options.request,
1035
+ runtime: activeRuntime,
1036
+ });
1037
+ return waitForRemoteWatchApproval({
1038
+ approvalId: summary.id,
1039
+ approvalUri: summary.approvalUri,
463
1040
  pollMs: options.pollMs,
464
1041
  signal: options.signal,
465
1042
  runtime: activeRuntime,
@@ -469,12 +1046,9 @@ export async function listRemoteWatchApprovals(options = {}) {
469
1046
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
470
1047
  const requestedStatus = options.status ?? 'pending';
471
1048
  return withRemoteApprovalStore(activeRuntime, async ({ store, webId }) => {
472
- const [approvals, audits] = await Promise.all([
473
- store.listApprovals(),
474
- store.listAudits(),
475
- ]);
1049
+ const approvals = await store.listApprovals();
476
1050
  return approvals
477
- .map((row) => normalizeApprovalSummary(row, audits))
1051
+ .map((row) => normalizeApprovalSummary(row))
478
1052
  .filter((summary) => !summary.assignedTo || summary.assignedTo === webId)
479
1053
  .filter((summary) => requestedStatus === 'all' || summary.status === requestedStatus)
480
1054
  .sort((left, right) => right.createdAt.localeCompare(left.createdAt));
@@ -483,96 +1057,124 @@ export async function listRemoteWatchApprovals(options = {}) {
483
1057
  export async function resolveRemoteWatchApproval(options) {
484
1058
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
485
1059
  return withRemoteApprovalStore(activeRuntime, async ({ store, webId }) => {
486
- const approvals = await store.listApprovals();
487
- const row = approvals.find((entry) => entry.id === options.approvalId);
1060
+ const row = await readRemoteApprovalRow(store, {
1061
+ approvalId: options.approvalId,
1062
+ approvalUri: options.approvalUri,
1063
+ });
488
1064
  if (!row) {
489
1065
  throw new Error(`Remote approval not found: ${options.approvalId}`);
490
1066
  }
491
1067
  if (row.status !== 'pending') {
492
- const audits = await store.listAudits();
493
- return normalizeApprovalSummary(row, audits);
1068
+ return normalizeApprovalSummary(row);
494
1069
  }
495
1070
  const now = activeRuntime.now();
496
- const approvalUri = buildApprovalUri(row.session, row.id);
1071
+ const approvalUri = normalizeString(row.approvalUri)
1072
+ ?? store.resolveApprovalReference({ id: row.id, createdAt: row.createdAt }).iri;
497
1073
  const nextStatus = options.decision === 'accept' || options.decision === 'accept_for_session'
498
1074
  ? 'approved'
499
1075
  : 'rejected';
1076
+ const decisionRole = options.decisionRole ?? 'human';
500
1077
  await store.updateApproval(row.id, {
1078
+ approvalUri,
501
1079
  status: nextStatus,
502
1080
  decisionBy: webId,
503
- decisionRole: 'human',
1081
+ decisionRole,
504
1082
  onBehalfOf: webId,
505
1083
  reason: encodeDecisionReason(options.decision, options.note),
506
1084
  resolvedAt: now,
507
1085
  });
508
- await store.insertAudit({
1086
+ await warnOnly(activeRuntime, () => store.insertAudit({
509
1087
  id: crypto.randomUUID(),
510
1088
  action: nextStatus === 'approved' ? 'approval_approved' : 'approval_rejected',
511
1089
  actor: webId,
512
- actorRole: 'human',
1090
+ actorRole: decisionRole,
513
1091
  onBehalfOf: webId,
514
1092
  session: row.session,
1093
+ entry: approvalUri,
515
1094
  toolCallId: row.toolCallId,
1095
+ toolName: row.toolName,
516
1096
  approval: approvalUri,
517
- context: JSON.stringify({
518
- decision: options.decision,
519
- ...(options.note?.trim() ? { note: options.note.trim() } : {}),
520
- }),
521
1097
  policyVersion: REMOTE_APPROVAL_POLICY_VERSION,
522
1098
  createdAt: now,
523
- });
1099
+ }));
524
1100
  if (options.decision === 'accept_for_session') {
525
1101
  const grantId = crypto.randomUUID();
1102
+ const body = grantWikiBodyFromApproval(row, options.grantWikiBody);
526
1103
  await store.insertGrant({
527
1104
  id: grantId,
528
1105
  target: row.target,
529
1106
  action: row.action,
1107
+ title: grantWikiTitleFromApproval(row, options.grantWikiTitle),
1108
+ summary: grantWikiSummaryFromApproval(row, options.grantWikiSummary),
1109
+ body,
1110
+ schema: buildGrantSchemaUri(webId),
1111
+ pageKind: 'autonomy-grant',
1112
+ wikiStatus: 'active',
1113
+ tags: grantWikiTagsFromApproval(row, options.grantWikiTags),
1114
+ source: 'approval',
1115
+ sourceHash: grantSourceHash(row),
1116
+ compiledAt: now,
1117
+ compiledFrom: [approvalUri],
1118
+ related: [row.session],
530
1119
  effect: 'allow',
531
1120
  riskCeiling: row.risk,
1121
+ policy: grantIndexTextFromWikiBody(body),
1122
+ context: grantContextFromApproval(row),
532
1123
  decisionBy: webId,
533
- decisionRole: 'human',
1124
+ decisionRole,
534
1125
  onBehalfOf: webId,
535
1126
  createdAt: now,
536
1127
  });
537
- await store.insertInboxNotification({
1128
+ await warnOnly(activeRuntime, () => store.insertInboxNotification({
538
1129
  id: crypto.randomUUID(),
539
1130
  actor: webId,
540
- object: buildGrantUri(row.session, grantId),
1131
+ object: store.resolveGrantReference({ id: grantId }).iri,
541
1132
  createdAt: now,
542
- }).catch(() => undefined);
1133
+ }));
543
1134
  }
544
- await store.insertInboxNotification({
1135
+ await warnOnly(activeRuntime, () => store.insertInboxNotification({
545
1136
  id: crypto.randomUUID(),
546
1137
  actor: webId,
547
1138
  object: approvalUri,
548
1139
  createdAt: now,
549
- }).catch(() => undefined);
1140
+ }));
550
1141
  const nextRow = {
551
1142
  ...row,
1143
+ approvalUri,
552
1144
  status: nextStatus,
553
1145
  decisionBy: webId,
554
- decisionRole: 'human',
1146
+ decisionRole,
555
1147
  onBehalfOf: webId,
556
1148
  reason: encodeDecisionReason(options.decision, options.note),
557
1149
  resolvedAt: now,
558
1150
  };
559
- const audits = await store.listAudits();
560
- return normalizeApprovalSummary(nextRow, audits);
1151
+ return normalizeApprovalSummary(nextRow);
561
1152
  });
562
1153
  }
1154
+ async function readRemoteApprovalRow(store, options) {
1155
+ if (store.findApproval) {
1156
+ const row = await store.findApproval(options.approvalId, {
1157
+ approvalUri: options.approvalUri,
1158
+ });
1159
+ return row;
1160
+ }
1161
+ const approvals = await store.listApprovals();
1162
+ return approvals.find((entry) => entry.id === options.approvalId) ?? null;
1163
+ }
563
1164
  export const __podApprovalInternal = {
564
1165
  createAbortError,
1166
+ createDefaultRuntime,
565
1167
  buildActionUri,
566
- buildRequestAuditContext,
567
1168
  buildRisk,
568
1169
  buildToolName,
1170
+ createNativeRemoteApprovalStore,
569
1171
  extractToolCallId,
570
1172
  decisionFromApprovalRow,
571
1173
  encodeDecisionReason,
572
1174
  formatSummaryHeadline,
1175
+ readRemoteApprovalRow,
573
1176
  isRemoteApprovalAbortError,
574
1177
  normalizeApprovalSummary,
575
1178
  parseDecisionReason,
576
- parseRequestAuditContext,
577
1179
  };
578
1180
  //# sourceMappingURL=pod-approval.js.map