@undefineds.co/linx 0.2.14 → 0.2.16

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 (43) hide show
  1. package/README.md +3 -6
  2. package/dist/index.js +50 -104
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/ai-command.js +38 -60
  5. package/dist/lib/ai-command.js.map +1 -1
  6. package/dist/lib/pi-adapter/branding.js +2 -2
  7. package/dist/lib/pi-adapter/branding.js.map +1 -1
  8. package/dist/lib/pi-adapter/pod-approval.js +1 -114
  9. package/dist/lib/pi-adapter/pod-approval.js.map +1 -1
  10. package/dist/lib/pi-adapter/pod-native.js +3 -2
  11. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  12. package/dist/lib/pi-adapter/runtime.js +2 -2
  13. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  14. package/dist/lib/pod-data-session.js +43 -3
  15. package/dist/lib/pod-data-session.js.map +1 -1
  16. package/dist/lib/watch/hooks/claude.js +4 -0
  17. package/dist/lib/watch/hooks/claude.js.map +1 -1
  18. package/dist/lib/watch/hooks/codebuddy.js +4 -0
  19. package/dist/lib/watch/hooks/codebuddy.js.map +1 -1
  20. package/dist/lib/watch/hooks/codex.js +4 -0
  21. package/dist/lib/watch/hooks/codex.js.map +1 -1
  22. package/dist/lib/watch/pod-ai.js +22 -14
  23. package/dist/lib/watch/pod-ai.js.map +1 -1
  24. package/dist/lib/watch/pod-approval.js +444 -40
  25. package/dist/lib/watch/pod-approval.js.map +1 -1
  26. package/dist/lib/watch/pod-persistence.js +162 -43
  27. package/dist/lib/watch/pod-persistence.js.map +1 -1
  28. package/dist/lib/watch/runner.js +243 -38
  29. package/dist/lib/watch/runner.js.map +1 -1
  30. package/dist/lib/watch/secretary.js +238 -0
  31. package/dist/lib/watch/secretary.js.map +1 -0
  32. package/dist/watch-cli.js +8 -34
  33. package/dist/watch-cli.js.map +1 -1
  34. package/package.json +3 -2
  35. package/vendor/agent-runtime/dist/acp.d.ts +27 -0
  36. package/vendor/agent-runtime/dist/acp.js +86 -0
  37. package/vendor/agent-runtime/dist/companion-model.d.ts +7 -0
  38. package/vendor/agent-runtime/dist/companion-model.js +12 -0
  39. package/vendor/agent-runtime/dist/index.d.ts +3 -0
  40. package/vendor/agent-runtime/dist/index.js +3 -0
  41. package/vendor/agent-runtime/dist/turn-controller.d.ts +69 -0
  42. package/vendor/agent-runtime/dist/turn-controller.js +129 -0
  43. package/vendor/agent-runtime/package.json +11 -0
@@ -2,11 +2,18 @@ import { setTimeout as delay } from 'node:timers/promises';
2
2
  import { getDefaultPodDataSession } from '../pod-data-session.js';
3
3
  import { AS, ODRL, UDFS } from '@undefineds.co/models/namespaces';
4
4
  import { ApprovalVocab, AuditVocab, GrantVocab, InboxNotificationVocab } from '@undefineds.co/models/vocab/sidecar';
5
+ import { resolveWatchGrantCoverage } from './secretary.js';
5
6
  import { buildApprovalDocumentUrl, RDF_TYPE, buildApprovalResourceUrl, buildAuditDocumentUrl, buildAuditResourceUrl, buildGrantResourceUrl, buildInboxResourceUrl, firstIri, firstLiteral, iri, listTurtleResources, listTurtleResourcesRecursive, literal, parseManagedTurtleBlocks, readTurtleResource, subjectIdFromResourceUrl, upsertManagedTurtleBlock, } from '../pi-adapter/pod-native.js';
6
- const WATCH_CHAT_ID = 'linx-watch';
7
+ const WATCH_CHAT_ID_PREFIX = 'linx-watch';
7
8
  const WATCH_AGENT_ID = 'linx-watch-assistant';
8
9
  const REMOTE_APPROVAL_POLICY_VERSION = 'linx-watch-remote-approval/v1';
9
10
  const DEFAULT_REMOTE_APPROVAL_POLL_MS = 1000;
11
+ const DEFAULT_WARN_ONLY_TIMEOUT_MS = 5000;
12
+ const DEFAULT_APPROVAL_LIST_DAYS = 7;
13
+ const MAX_GRANT_POLICY_LENGTH = 1200;
14
+ const MAX_APPROVAL_CONTEXT_LENGTH = 1400;
15
+ const MIN_GRANT_COVERAGE_CONFIDENCE = 0.75;
16
+ const MAX_GRANT_COVERAGE_CANDIDATES = 5;
10
17
  const remoteApprovalClientCache = new WeakMap();
11
18
  function createAbortError() {
12
19
  const error = new Error('The operation was aborted.');
@@ -38,8 +45,11 @@ function getPodBaseUrl(webIdOrUri) {
38
45
  }
39
46
  return webIdOrUri.replace(/\/$/, '');
40
47
  }
41
- function buildThreadUri(webId, threadId) {
42
- return `${getPodBaseUrl(webId)}/.data/chat/${WATCH_CHAT_ID}/index.ttl#${threadId}`;
48
+ function buildWatchChatId(record) {
49
+ return `${WATCH_CHAT_ID_PREFIX}-${record.backend}`;
50
+ }
51
+ function buildThreadUri(webId, record) {
52
+ return `${getPodBaseUrl(webId)}/.data/chat/${buildWatchChatId(record)}/index.ttl#${record.id}`;
43
53
  }
44
54
  function buildApprovalUri(webIdOrUri, approvalId) {
45
55
  return buildApprovalResourceUrl(webIdOrUri, approvalId);
@@ -47,11 +57,14 @@ function buildApprovalUri(webIdOrUri, approvalId) {
47
57
  function buildApprovalUriForDate(webIdOrUri, approvalId, createdAt) {
48
58
  return buildApprovalResourceUrl(webIdOrUri, approvalId, createdAt);
49
59
  }
60
+ function documentUrlFromResourceUri(resourceUri) {
61
+ return resourceUri.split('#', 1)[0] ?? resourceUri;
62
+ }
50
63
  function buildGrantUri(webIdOrUri, grantId) {
51
64
  return buildGrantResourceUrl(webIdOrUri, grantId);
52
65
  }
53
- function buildGrantDocumentUrl(webIdOrUri) {
54
- return `${getPodBaseUrl(webIdOrUri)}/settings/autonomy/grants.ttl`;
66
+ function buildGrantSchemaUri(webIdOrUri) {
67
+ return `${getPodBaseUrl(webIdOrUri)}/settings/autonomy/schema/grant.ttl#GrantWikiPage`;
55
68
  }
56
69
  function buildAgentUri(webId) {
57
70
  return `${getPodBaseUrl(webId)}/.data/agents/${WATCH_AGENT_ID}.ttl`;
@@ -150,7 +163,12 @@ function parseDecisionReason(value) {
150
163
  }
151
164
  async function warnOnly(runtime, task) {
152
165
  try {
153
- await task();
166
+ await Promise.race([
167
+ task(),
168
+ runtime.sleep(DEFAULT_WARN_ONLY_TIMEOUT_MS).then(() => {
169
+ throw new Error(`Pod side-effect sync timed out after ${DEFAULT_WARN_ONLY_TIMEOUT_MS}ms`);
170
+ }),
171
+ ]);
154
172
  }
155
173
  catch (error) {
156
174
  if (runtime.onWarning) {
@@ -169,6 +187,168 @@ function safeJsonStringify(value) {
169
187
  return JSON.stringify({ error: 'unserializable_context' });
170
188
  }
171
189
  }
190
+ function truncatePodLiteral(value, maxLength) {
191
+ if (value.length <= maxLength) {
192
+ return value;
193
+ }
194
+ return `${value.slice(0, Math.max(0, maxLength - 15))}...[truncated]`;
195
+ }
196
+ function safeCompactJson(value, maxLength) {
197
+ return truncatePodLiteral(safeJsonStringify(value), maxLength);
198
+ }
199
+ function compactApprovalContext(request) {
200
+ return safeCompactJson({
201
+ kind: request.kind,
202
+ message: request.message,
203
+ toolName: request.toolName,
204
+ action: request.action,
205
+ risk: request.risk,
206
+ ...(request.command ? { command: request.command } : {}),
207
+ ...(request.cwd ? { cwd: request.cwd } : {}),
208
+ ...(request.approvalOptions ? { approvalOptions: request.approvalOptions } : {}),
209
+ ...(request.expiresAt ? { expiresAt: normalizeDateLike(request.expiresAt) } : {}),
210
+ ...(request.context ? { sourceContext: truncatePodLiteral(request.context, 500) } : {}),
211
+ }, MAX_APPROVAL_CONTEXT_LENGTH);
212
+ }
213
+ function grantWikiTitleFromApproval(row, explicitTitle) {
214
+ const explicit = normalizeString(explicitTitle);
215
+ if (explicit) {
216
+ return truncatePodLiteral(explicit, 160);
217
+ }
218
+ return truncatePodLiteral(`${row.toolName} grant wiki for ${extractSessionId(row.session)}`, 160);
219
+ }
220
+ function grantWikiSummaryFromApproval(row, explicitSummary) {
221
+ const explicit = normalizeString(explicitSummary);
222
+ if (explicit) {
223
+ return truncatePodLiteral(explicit, 500);
224
+ }
225
+ return truncatePodLiteral(`Authorization wiki page for ${row.toolName}. AI Secretary must read the page body before reusing this grant.`, 500);
226
+ }
227
+ function grantWikiBodyFromApproval(row, explicitBody) {
228
+ const explicit = normalizeString(explicitBody);
229
+ if (explicit) {
230
+ return truncatePodLiteral(explicit, MAX_GRANT_POLICY_LENGTH);
231
+ }
232
+ return truncatePodLiteral([
233
+ '# Grant Semantics',
234
+ '',
235
+ 'This page follows the LLM Wiki pattern: it is the maintained wiki view AI Secretary reads before reusing an authorization.',
236
+ '',
237
+ '## Covers',
238
+ `- Requests semantically inside target ${row.target}.`,
239
+ `- Action family ${row.action}.`,
240
+ `- Risk no higher than ${row.risk}.`,
241
+ '',
242
+ '## Does Not Cover',
243
+ '- Requests that are materially broader than the source approval.',
244
+ '- Requests that change from read-oriented to write/destructive behavior.',
245
+ '- Requests that touch credentials, secrets, package installation, new network side effects, or workspace boundaries unless explicitly documented here.',
246
+ '',
247
+ '## Source Context',
248
+ row.context ?? safeJsonStringify({ toolName: row.toolName, action: row.action, risk: row.risk }),
249
+ ].join('\n'), MAX_GRANT_POLICY_LENGTH);
250
+ }
251
+ function grantIndexTextFromWikiBody(body) {
252
+ return truncatePodLiteral(body, MAX_GRANT_POLICY_LENGTH);
253
+ }
254
+ function grantWikiTagsFromApproval(row, explicitTags) {
255
+ const tags = [
256
+ 'autonomy',
257
+ 'grant',
258
+ row.toolName,
259
+ row.risk,
260
+ ...(explicitTags ?? []),
261
+ ]
262
+ .map((tag) => tag.trim())
263
+ .filter(Boolean);
264
+ return safeJsonStringify([...new Set(tags)]);
265
+ }
266
+ function grantContextFromApproval(row) {
267
+ return safeCompactJson({
268
+ sourceApproval: buildApprovalUriForDate(row.session, row.id, new Date(toIsoString(row.createdAt, new Date().toISOString()))),
269
+ session: row.session,
270
+ toolCallId: row.toolCallId,
271
+ toolName: row.toolName,
272
+ target: row.target,
273
+ action: row.action,
274
+ risk: row.risk,
275
+ approvalContext: row.context,
276
+ }, MAX_APPROVAL_CONTEXT_LENGTH);
277
+ }
278
+ function literalValues(predicates, predicate) {
279
+ return (predicates.get(predicate) ?? [])
280
+ .map((object) => isRecord(object) && object.type === 'literal' && typeof object.value === 'string' ? object.value : '')
281
+ .filter(Boolean);
282
+ }
283
+ function iriValues(predicates, predicate) {
284
+ return (predicates.get(predicate) ?? [])
285
+ .map((object) => isRecord(object) && object.type === 'iri' && typeof object.value === 'string' ? object.value : '')
286
+ .filter(Boolean);
287
+ }
288
+ function grantSourceHash(row) {
289
+ return `approval:${row.id}:${row.toolCallId}:${row.risk}`;
290
+ }
291
+ function encodeApprovalOptions(options) {
292
+ if (!options || options.length === 0) {
293
+ return undefined;
294
+ }
295
+ return safeJsonStringify(options);
296
+ }
297
+ function parseApprovalOptions(value) {
298
+ if (typeof value !== 'string' || !value.trim()) {
299
+ return undefined;
300
+ }
301
+ try {
302
+ const parsed = JSON.parse(value);
303
+ if (!Array.isArray(parsed)) {
304
+ return undefined;
305
+ }
306
+ const options = parsed
307
+ .map((option) => {
308
+ if (!isRecord(option)) {
309
+ return null;
310
+ }
311
+ const optionId = normalizeString(option.optionId);
312
+ const label = normalizeString(option.label);
313
+ if (!optionId || !label) {
314
+ return null;
315
+ }
316
+ const kind = normalizeString(option.kind);
317
+ const description = normalizeString(option.description);
318
+ return {
319
+ optionId,
320
+ label,
321
+ ...(kind ? { kind } : {}),
322
+ ...(description ? { description } : {}),
323
+ };
324
+ })
325
+ .filter((option) => option !== null);
326
+ return options.length > 0 ? options : undefined;
327
+ }
328
+ catch {
329
+ return undefined;
330
+ }
331
+ }
332
+ function normalizeDateLike(value) {
333
+ if (value instanceof Date) {
334
+ return Number.isFinite(value.getTime()) ? value.toISOString() : undefined;
335
+ }
336
+ if (typeof value !== 'string' || !value.trim()) {
337
+ return undefined;
338
+ }
339
+ const parsed = new Date(value);
340
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : undefined;
341
+ }
342
+ function resolveApprovalExpiresAt(request, now) {
343
+ const explicit = normalizeDateLike(request.expiresAt);
344
+ if (explicit) {
345
+ return explicit;
346
+ }
347
+ if (typeof request.timeoutMs === 'number' && Number.isFinite(request.timeoutMs) && request.timeoutMs > 0) {
348
+ return new Date(now.getTime() + request.timeoutMs);
349
+ }
350
+ return undefined;
351
+ }
172
352
  function extractSessionId(sessionUri) {
173
353
  if (sessionUri.includes('#')) {
174
354
  return sessionUri.split('#').pop() || sessionUri;
@@ -193,8 +373,10 @@ function normalizeApprovalSummary(row) {
193
373
  const createdAt = toIsoString(row.createdAt, new Date(0).toISOString());
194
374
  const sessionUri = row.session;
195
375
  const decision = decisionFromApprovalRow(row);
376
+ const approvalOptions = parseApprovalOptions(row.approvalOptions);
196
377
  return {
197
378
  id: row.id,
379
+ ...(normalizeString(row.approvalUri) ? { approvalUri: normalizeString(row.approvalUri) } : {}),
198
380
  sessionId: extractSessionId(sessionUri),
199
381
  sessionUri,
200
382
  toolCallId: row.toolCallId,
@@ -205,7 +387,9 @@ function normalizeApprovalSummary(row) {
205
387
  ...(normalizeString(row.assignedTo) ? { assignedTo: normalizeString(row.assignedTo) } : {}),
206
388
  ...(normalizeString(row.decisionBy) ? { decisionBy: normalizeString(row.decisionBy) } : {}),
207
389
  ...(decision ? { decision } : {}),
390
+ ...(approvalOptions ? { approvalOptions } : {}),
208
391
  createdAt,
392
+ ...(row.expiresAt ? { expiresAt: toIsoString(row.expiresAt, createdAt) } : {}),
209
393
  ...(row.resolvedAt ? { resolvedAt: toIsoString(row.resolvedAt, createdAt) } : {}),
210
394
  };
211
395
  }
@@ -251,6 +435,7 @@ async function createDefaultRuntime() {
251
435
  now() {
252
436
  return new Date();
253
437
  },
438
+ resolveGrantCoverage: resolveWatchGrantCoverage,
254
439
  };
255
440
  }
256
441
  async function withRemoteApprovalStore(runtime, fn) {
@@ -295,6 +480,7 @@ async function createRemoteApprovalClient(runtime) {
295
480
  function createNativeRemoteApprovalStore(webId, fetcher) {
296
481
  return {
297
482
  listApprovals: () => listApprovalRows(webId, fetcher),
483
+ findApproval: (id, options) => findApprovalRow(webId, fetcher, id, options),
298
484
  insertApproval: (row) => writeApprovalRow(webId, fetcher, row),
299
485
  async updateApproval(id, patch) {
300
486
  const existing = (await listApprovalRows(webId, fetcher)).find((row) => row.id === id);
@@ -310,14 +496,39 @@ function createNativeRemoteApprovalStore(webId, fetcher) {
310
496
  insertInboxNotification: (row) => writeInboxNotificationRow(webId, fetcher, row),
311
497
  };
312
498
  }
499
+ async function findApprovalRow(webId, fetcher, id, options = {}) {
500
+ if (options.resourceUri) {
501
+ return readApprovalRowFromResource(fetcher, options.resourceUri);
502
+ }
503
+ if (options.createdAt) {
504
+ const createdAt = new Date(toIsoString(options.createdAt, new Date().toISOString()));
505
+ return readApprovalRowFromResource(fetcher, buildApprovalResourceUrl(webId, id, createdAt));
506
+ }
507
+ return (await listApprovalRows(webId, fetcher)).find((row) => row.id === id) ?? null;
508
+ }
509
+ async function readApprovalRowFromResource(fetcher, resourceUri) {
510
+ const turtle = await readTurtleResource(fetcher, documentUrlFromResourceUri(resourceUri));
511
+ if (!turtle) {
512
+ return null;
513
+ }
514
+ for (const [subject, predicates] of parseManagedTurtleBlocks(turtle, documentUrlFromResourceUri(resourceUri))) {
515
+ if (subject !== resourceUri) {
516
+ continue;
517
+ }
518
+ const row = approvalRowFromPredicates(subject, predicates);
519
+ if (row) {
520
+ return row;
521
+ }
522
+ }
523
+ return null;
524
+ }
313
525
  async function listApprovalRows(webId, fetcher) {
314
- const [currentUrls, legacyUrls] = await Promise.all([
315
- listTurtleResourcesRecursive(fetcher, `${getPodBaseUrl(webId)}/.data/approvals/`).catch(() => []),
316
- listTurtleResources(fetcher, `${getPodBaseUrl(webId)}/.data/approvals/`).catch(() => []),
317
- ]);
318
- const urls = [...new Set([...currentUrls, ...legacyUrls])];
526
+ const urls = [
527
+ ...recentApprovalDocumentUrls(webId),
528
+ ...await listTurtleResources(fetcher, `${getPodBaseUrl(webId)}/.data/approvals/`).catch(() => []),
529
+ ];
319
530
  const rows = [];
320
- for (const url of urls.filter((entry) => entry.endsWith('.ttl'))) {
531
+ for (const url of [...new Set(urls)].filter((entry) => entry.endsWith('.ttl'))) {
321
532
  const turtle = await readTurtleResource(fetcher, url).catch(() => null);
322
533
  if (!turtle)
323
534
  continue;
@@ -329,6 +540,15 @@ async function listApprovalRows(webId, fetcher) {
329
540
  }
330
541
  return rows;
331
542
  }
543
+ function recentApprovalDocumentUrls(webId, days = DEFAULT_APPROVAL_LIST_DAYS) {
544
+ const urls = [];
545
+ const base = Date.now();
546
+ for (let offset = 0; offset < days; offset += 1) {
547
+ const date = new Date(base - offset * 24 * 60 * 60 * 1000);
548
+ urls.push(buildApprovalDocumentUrl(webId, date));
549
+ }
550
+ return urls;
551
+ }
332
552
  async function writeApprovalRow(webId, fetcher, row) {
333
553
  const createdAt = new Date(toIsoString(row.createdAt, new Date().toISOString()));
334
554
  const documentUrl = buildApprovalDocumentUrl(webId, createdAt);
@@ -349,8 +569,11 @@ async function writeApprovalRow(webId, fetcher, row) {
349
569
  ...(row.decisionRole ? [{ predicate: ApprovalVocab.decisionRole, object: literal(row.decisionRole) }] : []),
350
570
  ...(row.onBehalfOf ? [{ predicate: ApprovalVocab.onBehalfOf, object: iri(row.onBehalfOf) }] : []),
351
571
  ...(row.reason ? [{ predicate: ApprovalVocab.reason, object: literal(row.reason) }] : []),
572
+ ...(row.context ? [{ predicate: ApprovalVocab.context, object: literal(row.context) }] : []),
573
+ ...(row.approvalOptions ? [{ predicate: ApprovalVocab.approvalOptions, object: literal(row.approvalOptions) }] : []),
352
574
  ...(row.policyVersion ? [{ predicate: ApprovalVocab.policyVersion, object: literal(row.policyVersion) }] : []),
353
575
  { predicate: ApprovalVocab.createdAt, object: literal(toIsoString(row.createdAt, new Date().toISOString())) },
576
+ ...(row.expiresAt ? [{ predicate: ApprovalVocab.expiresAt, object: literal(toIsoString(row.expiresAt, new Date().toISOString())) }] : []),
354
577
  ...(row.resolvedAt ? [{ predicate: ApprovalVocab.resolvedAt, object: literal(toIsoString(row.resolvedAt, new Date().toISOString())) }] : []),
355
578
  ],
356
579
  });
@@ -394,7 +617,6 @@ async function writeAuditRow(webId, fetcher, row) {
394
617
  }
395
618
  async function listGrantRows(webId, fetcher) {
396
619
  const urls = [
397
- `${getPodBaseUrl(webId)}/settings/autonomy/grants.ttl`,
398
620
  ...await listTurtleResources(fetcher, `${getPodBaseUrl(webId)}/settings/autonomy/grants/`).catch(() => []),
399
621
  ];
400
622
  const rows = [];
@@ -412,8 +634,8 @@ async function listGrantRows(webId, fetcher) {
412
634
  }
413
635
  async function writeGrantRow(webId, fetcher, row) {
414
636
  const id = normalizeString(row.id) ?? crypto.randomUUID();
415
- const documentUrl = buildGrantDocumentUrl(webId);
416
637
  const subjectUrl = buildGrantResourceUrl(webId, id);
638
+ const documentUrl = subjectUrl;
417
639
  const target = normalizeString(row.target);
418
640
  const action = normalizeString(row.action);
419
641
  const effect = normalizeString(row.effect);
@@ -429,8 +651,22 @@ async function writeGrantRow(webId, fetcher, row) {
429
651
  { predicate: RDF_TYPE, object: iri(UDFS.AutonomyGrant) },
430
652
  { predicate: GrantVocab.target, object: iri(target) },
431
653
  { predicate: GrantVocab.action, object: iri(action) },
654
+ ...(normalizeString(row.title) ? [{ predicate: GrantVocab.title, object: literal(truncatePodLiteral(normalizeString(row.title), 160)) }] : []),
655
+ ...(normalizeString(row.summary) ? [{ predicate: GrantVocab.summary, object: literal(truncatePodLiteral(normalizeString(row.summary), 500)) }] : []),
656
+ ...(normalizeString(row.body) ? [{ predicate: GrantVocab.body, object: literal(truncatePodLiteral(normalizeString(row.body), MAX_GRANT_POLICY_LENGTH)) }] : []),
657
+ ...(normalizeString(row.schema) ? [{ predicate: GrantVocab.schema, object: iri(normalizeString(row.schema)) }] : []),
658
+ ...(normalizeString(row.pageKind) ? [{ predicate: GrantVocab.pageKind, object: literal(normalizeString(row.pageKind)) }] : []),
659
+ ...(normalizeString(row.wikiStatus) ? [{ predicate: GrantVocab.wikiStatus, object: literal(normalizeString(row.wikiStatus)) }] : []),
660
+ ...(normalizeString(row.tags) ? [{ predicate: GrantVocab.tags, object: literal(truncatePodLiteral(normalizeString(row.tags), 500)) }] : []),
661
+ ...(normalizeString(row.source) ? [{ predicate: GrantVocab.source, object: literal(normalizeString(row.source)) }] : []),
662
+ ...(normalizeString(row.sourceHash) ? [{ predicate: GrantVocab.sourceHash, object: literal(normalizeString(row.sourceHash)) }] : []),
663
+ ...(row.compiledAt ? [{ predicate: GrantVocab.compiledAt, object: literal(toIsoString(row.compiledAt, new Date().toISOString())) }] : []),
664
+ ...(row.compiledFrom ?? []).map((value) => ({ predicate: GrantVocab.compiledFrom, object: iri(value) })),
665
+ ...(row.related ?? []).map((value) => ({ predicate: GrantVocab.related, object: iri(value) })),
432
666
  { predicate: GrantVocab.effect, object: literal(effect) },
433
667
  ...(normalizeString(row.riskCeiling) ? [{ predicate: GrantVocab.riskCeiling, object: literal(normalizeString(row.riskCeiling)) }] : []),
668
+ ...(normalizeString(row.policy) ? [{ predicate: GrantVocab.policy, object: literal(truncatePodLiteral(normalizeString(row.policy), MAX_GRANT_POLICY_LENGTH)) }] : []),
669
+ ...(normalizeString(row.context) ? [{ predicate: GrantVocab.context, object: literal(truncatePodLiteral(normalizeString(row.context), MAX_APPROVAL_CONTEXT_LENGTH)) }] : []),
434
670
  { predicate: GrantVocab.decisionBy, object: iri(decisionBy) },
435
671
  { predicate: GrantVocab.decisionRole, object: literal(decisionRole) },
436
672
  ...(normalizeString(row.onBehalfOf) ? [{ predicate: GrantVocab.onBehalfOf, object: iri(normalizeString(row.onBehalfOf)) }] : []),
@@ -477,8 +713,11 @@ function approvalRowFromPredicates(url, predicates) {
477
713
  decisionRole: firstLiteral(predicates, ApprovalVocab.decisionRole),
478
714
  onBehalfOf: firstIri(predicates, ApprovalVocab.onBehalfOf),
479
715
  reason: firstLiteral(predicates, ApprovalVocab.reason),
716
+ context: firstLiteral(predicates, ApprovalVocab.context),
717
+ approvalOptions: firstLiteral(predicates, ApprovalVocab.approvalOptions),
480
718
  policyVersion: firstLiteral(predicates, ApprovalVocab.policyVersion),
481
719
  createdAt,
720
+ expiresAt: firstLiteral(predicates, ApprovalVocab.expiresAt),
482
721
  resolvedAt: firstLiteral(predicates, ApprovalVocab.resolvedAt),
483
722
  };
484
723
  }
@@ -519,8 +758,22 @@ function grantRowFromPredicates(url, predicates) {
519
758
  id: subjectIdFromResourceUrl(url),
520
759
  target,
521
760
  action,
761
+ title: firstLiteral(predicates, GrantVocab.title),
762
+ summary: firstLiteral(predicates, GrantVocab.summary),
763
+ body: firstLiteral(predicates, GrantVocab.body),
764
+ schema: firstIri(predicates, GrantVocab.schema),
765
+ pageKind: firstLiteral(predicates, GrantVocab.pageKind),
766
+ wikiStatus: firstLiteral(predicates, GrantVocab.wikiStatus),
767
+ tags: firstLiteral(predicates, GrantVocab.tags),
768
+ source: firstLiteral(predicates, GrantVocab.source),
769
+ sourceHash: firstLiteral(predicates, GrantVocab.sourceHash),
770
+ compiledAt: firstLiteral(predicates, GrantVocab.compiledAt),
771
+ compiledFrom: iriValues(predicates, GrantVocab.compiledFrom),
772
+ related: iriValues(predicates, GrantVocab.related),
522
773
  effect,
523
774
  riskCeiling: firstLiteral(predicates, GrantVocab.riskCeiling),
775
+ policy: firstLiteral(predicates, GrantVocab.policy),
776
+ context: firstLiteral(predicates, GrantVocab.context),
524
777
  decisionBy,
525
778
  decisionRole,
526
779
  onBehalfOf: firstIri(predicates, GrantVocab.onBehalfOf),
@@ -528,11 +781,88 @@ function grantRowFromPredicates(url, predicates) {
528
781
  revokedAt: firstLiteral(predicates, GrantVocab.revokedAt),
529
782
  };
530
783
  }
784
+ function isActiveAllowGrant(grant) {
785
+ return grant.effect === 'allow' && !grant.revokedAt && !!(normalizeString(grant.body) || normalizeString(grant.policy));
786
+ }
787
+ function isGrantRiskCandidate(grant, requestRisk) {
788
+ const ceiling = riskScore(typeof grant.riskCeiling === 'string' ? grant.riskCeiling : undefined);
789
+ return ceiling === 0 || ceiling >= riskScore(requestRisk);
790
+ }
791
+ function rankGrantCandidate(grant, requestContext) {
792
+ let score = 0;
793
+ if (grant.target === requestContext.target) {
794
+ score += 4;
795
+ }
796
+ if (grant.action === requestContext.action) {
797
+ score += 3;
798
+ }
799
+ if (grant.schema) {
800
+ score += 2;
801
+ }
802
+ if (grant.pageKind === 'autonomy-grant') {
803
+ score += 1;
804
+ }
805
+ return score;
806
+ }
807
+ function selectSemanticGrantCandidates(grants, requestContext) {
808
+ const risk = normalizeString(requestContext.risk) ?? 'medium';
809
+ return grants
810
+ .filter((grant) => isActiveAllowGrant(grant) && isGrantRiskCandidate(grant, risk))
811
+ .sort((left, right) => rankGrantCandidate(right, requestContext) - rankGrantCandidate(left, requestContext))
812
+ .slice(0, MAX_GRANT_COVERAGE_CANDIDATES);
813
+ }
814
+ function acceptsGrantCoverage(decision) {
815
+ return decision?.covers === true
816
+ && typeof decision.confidence === 'number'
817
+ && decision.confidence >= MIN_GRANT_COVERAGE_CONFIDENCE;
818
+ }
819
+ async function resolveSemanticGrantDecision(options) {
820
+ const candidates = selectSemanticGrantCandidates(options.grants, options.requestContext);
821
+ if (candidates.length === 0) {
822
+ return null;
823
+ }
824
+ const resolver = options.runtime.resolveGrantCoverage ?? resolveWatchGrantCoverage;
825
+ for (const grant of candidates) {
826
+ const coverage = await resolver({
827
+ record: options.record,
828
+ request: options.request,
829
+ requestContext: options.requestContext,
830
+ grant,
831
+ }).catch(() => null);
832
+ if (acceptsGrantCoverage(coverage)) {
833
+ return 'accept_for_session';
834
+ }
835
+ }
836
+ return null;
837
+ }
838
+ function buildWatchGrantRequestContext(input) {
839
+ return {
840
+ session: buildThreadUri(input.webId, input.record),
841
+ target: buildThreadUri(input.webId, input.record),
842
+ action: buildActionUri(input.request),
843
+ risk: buildRisk(input.request),
844
+ toolName: buildToolName(input.request),
845
+ cwd: input.record.cwd,
846
+ backend: input.record.backend,
847
+ mode: input.record.mode,
848
+ };
849
+ }
850
+ function buildGenericGrantRequestContext(input) {
851
+ return {
852
+ session: input.subject.sessionUri,
853
+ target: input.subject.targetUri ?? input.subject.sessionUri,
854
+ action: input.request.action,
855
+ risk: input.request.risk,
856
+ toolName: input.request.toolName,
857
+ cwd: input.request.cwd,
858
+ kind: input.request.kind,
859
+ };
860
+ }
531
861
  export async function createRemoteWatchApproval(options) {
532
862
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
533
863
  return createRemoteApproval({
534
864
  subject: ({ webId }) => ({
535
- sessionUri: buildThreadUri(webId, options.record.id),
865
+ sessionUri: buildThreadUri(webId, options.record),
536
866
  actorUri: buildAgentUri(webId),
537
867
  policyVersion: REMOTE_APPROVAL_POLICY_VERSION,
538
868
  }),
@@ -545,6 +875,9 @@ export async function createRemoteWatchApproval(options) {
545
875
  risk: buildRisk(options.request),
546
876
  ...(options.request.kind === 'command-approval' && options.request.command ? { command: options.request.command } : {}),
547
877
  ...(options.request.kind === 'command-approval' && options.request.cwd ? { cwd: options.request.cwd } : {}),
878
+ ...(options.request.approvalOptions ? { approvalOptions: options.request.approvalOptions } : {}),
879
+ ...(options.request.timeoutMs ? { timeoutMs: options.request.timeoutMs } : {}),
880
+ ...(options.request.expiresAt ? { expiresAt: options.request.expiresAt } : {}),
548
881
  entry: sessionUri,
549
882
  }),
550
883
  runtime: activeRuntime,
@@ -568,6 +901,9 @@ export async function createRemoteApproval(options) {
568
901
  const onBehalfOf = subject.onBehalfOf ?? webId;
569
902
  const policyVersion = subject.policyVersion ?? REMOTE_APPROVAL_POLICY_VERSION;
570
903
  const requestEntry = request.entry ?? approvalUri;
904
+ const expiresAt = resolveApprovalExpiresAt(request, now);
905
+ const approvalOptions = encodeApprovalOptions(request.approvalOptions);
906
+ const context = compactApprovalContext(request);
571
907
  await store.insertApproval({
572
908
  id: approvalId,
573
909
  session: sessionUri,
@@ -578,8 +914,11 @@ export async function createRemoteApproval(options) {
578
914
  risk: request.risk,
579
915
  status: 'pending',
580
916
  assignedTo,
917
+ context,
918
+ ...(approvalOptions ? { approvalOptions } : {}),
581
919
  policyVersion,
582
920
  createdAt: now,
921
+ ...(expiresAt ? { expiresAt } : {}),
583
922
  });
584
923
  const requestAudit = {
585
924
  id: crypto.randomUUID(),
@@ -604,6 +943,7 @@ export async function createRemoteApproval(options) {
604
943
  }));
605
944
  return normalizeApprovalSummary({
606
945
  id: approvalId,
946
+ approvalUri,
607
947
  session: sessionUri,
608
948
  toolCallId: request.toolCallId,
609
949
  toolName: request.toolName,
@@ -612,8 +952,11 @@ export async function createRemoteApproval(options) {
612
952
  risk: request.risk,
613
953
  status: 'pending',
614
954
  assignedTo,
955
+ context,
956
+ ...(approvalOptions ? { approvalOptions } : {}),
615
957
  policyVersion,
616
958
  createdAt: now,
959
+ ...(expiresAt ? { expiresAt } : {}),
617
960
  });
618
961
  });
619
962
  }
@@ -624,10 +967,13 @@ export async function waitForRemoteWatchApproval(options) {
624
967
  if (options.signal?.aborted) {
625
968
  throw createAbortError();
626
969
  }
627
- const approvals = await store.listApprovals();
628
- const row = approvals.find((entry) => entry.id === options.approvalId);
970
+ const row = await readRemoteApprovalRow(store, {
971
+ approvalId: options.approvalId,
972
+ approvalUri: options.approvalUri,
973
+ });
629
974
  if (!row) {
630
- throw new Error(`Remote approval disappeared before resolution: ${options.approvalId}`);
975
+ await activeRuntime.sleep(options.pollMs ?? DEFAULT_REMOTE_APPROVAL_POLL_MS);
976
+ continue;
631
977
  }
632
978
  const decision = decisionFromApprovalRow(row);
633
979
  if (decision) {
@@ -641,17 +987,20 @@ export async function requestRemoteWatchApproval(options) {
641
987
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
642
988
  const delegated = await withRemoteApprovalStore(activeRuntime, async ({ store, webId }) => {
643
989
  const grants = await store.listGrants();
644
- const requestAction = buildActionUri(options.request);
645
- const requestTarget = buildThreadUri(webId, options.record.id);
646
- const requestRisk = buildRisk(options.request);
647
- return grants.some((grant) => (grant.effect === 'allow'
648
- && grant.action === requestAction
649
- && grant.target === requestTarget
650
- && riskScore(typeof grant.riskCeiling === 'string' ? grant.riskCeiling : undefined) >= riskScore(requestRisk)
651
- && !grant.revokedAt));
990
+ return resolveSemanticGrantDecision({
991
+ runtime: activeRuntime,
992
+ grants,
993
+ record: options.record,
994
+ request: options.request,
995
+ requestContext: buildWatchGrantRequestContext({
996
+ webId,
997
+ record: options.record,
998
+ request: options.request,
999
+ }),
1000
+ });
652
1001
  });
653
1002
  if (delegated) {
654
- return 'accept_for_session';
1003
+ return delegated;
655
1004
  }
656
1005
  const summary = await createRemoteWatchApproval({
657
1006
  record: options.record,
@@ -660,11 +1009,29 @@ export async function requestRemoteWatchApproval(options) {
660
1009
  });
661
1010
  return waitForRemoteWatchApproval({
662
1011
  approvalId: summary.id,
1012
+ approvalUri: summary.approvalUri,
663
1013
  pollMs: options.pollMs,
664
1014
  signal: options.signal,
665
1015
  runtime: activeRuntime,
666
1016
  });
667
1017
  }
1018
+ export async function resolveExistingRemoteWatchGrant(options) {
1019
+ const activeRuntime = options.runtime ?? await createDefaultRuntime();
1020
+ return withRemoteApprovalStore(activeRuntime, async ({ store, webId }) => {
1021
+ const grants = await store.listGrants();
1022
+ return resolveSemanticGrantDecision({
1023
+ runtime: activeRuntime,
1024
+ grants,
1025
+ record: options.record,
1026
+ request: options.request,
1027
+ requestContext: buildWatchGrantRequestContext({
1028
+ webId,
1029
+ record: options.record,
1030
+ request: options.request,
1031
+ }),
1032
+ });
1033
+ });
1034
+ }
668
1035
  export async function requestRemoteApproval(options) {
669
1036
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
670
1037
  const delegated = await withRemoteApprovalStore(activeRuntime, async ({ store, webId, stored }) => {
@@ -675,15 +1042,20 @@ export async function requestRemoteApproval(options) {
675
1042
  ? options.request({ webId, stored, sessionUri: subject.sessionUri })
676
1043
  : options.request;
677
1044
  const grants = await store.listGrants();
678
- const requestTarget = subject.targetUri ?? subject.sessionUri;
679
- return grants.some((grant) => (grant.effect === 'allow'
680
- && grant.action === request.action
681
- && grant.target === requestTarget
682
- && riskScore(typeof grant.riskCeiling === 'string' ? grant.riskCeiling : undefined) >= riskScore(request.risk)
683
- && !grant.revokedAt));
1045
+ const requestContext = buildGenericGrantRequestContext({ subject, request });
1046
+ return resolveSemanticGrantDecision({
1047
+ runtime: activeRuntime,
1048
+ grants,
1049
+ request: {
1050
+ ...request,
1051
+ session: subject.sessionUri,
1052
+ target: requestContext.target,
1053
+ },
1054
+ requestContext,
1055
+ });
684
1056
  });
685
1057
  if (delegated) {
686
- return 'accept_for_session';
1058
+ return delegated;
687
1059
  }
688
1060
  const summary = await createRemoteApproval({
689
1061
  subject: options.subject,
@@ -692,6 +1064,7 @@ export async function requestRemoteApproval(options) {
692
1064
  });
693
1065
  return waitForRemoteWatchApproval({
694
1066
  approvalId: summary.id,
1067
+ approvalUri: summary.approvalUri,
695
1068
  pollMs: options.pollMs,
696
1069
  signal: options.signal,
697
1070
  runtime: activeRuntime,
@@ -712,8 +1085,10 @@ export async function listRemoteWatchApprovals(options = {}) {
712
1085
  export async function resolveRemoteWatchApproval(options) {
713
1086
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
714
1087
  return withRemoteApprovalStore(activeRuntime, async ({ store, webId }) => {
715
- const approvals = await store.listApprovals();
716
- const row = approvals.find((entry) => entry.id === options.approvalId);
1088
+ const row = await readRemoteApprovalRow(store, {
1089
+ approvalId: options.approvalId,
1090
+ approvalUri: options.approvalUri,
1091
+ });
717
1092
  if (!row) {
718
1093
  throw new Error(`Remote approval not found: ${options.approvalId}`);
719
1094
  }
@@ -726,10 +1101,11 @@ export async function resolveRemoteWatchApproval(options) {
726
1101
  const nextStatus = options.decision === 'accept' || options.decision === 'accept_for_session'
727
1102
  ? 'approved'
728
1103
  : 'rejected';
1104
+ const decisionRole = options.decisionRole ?? 'human';
729
1105
  await store.updateApproval(row.id, {
730
1106
  status: nextStatus,
731
1107
  decisionBy: webId,
732
- decisionRole: 'human',
1108
+ decisionRole,
733
1109
  onBehalfOf: webId,
734
1110
  reason: encodeDecisionReason(options.decision, options.note),
735
1111
  resolvedAt: now,
@@ -738,7 +1114,7 @@ export async function resolveRemoteWatchApproval(options) {
738
1114
  id: crypto.randomUUID(),
739
1115
  action: nextStatus === 'approved' ? 'approval_approved' : 'approval_rejected',
740
1116
  actor: webId,
741
- actorRole: 'human',
1117
+ actorRole: decisionRole,
742
1118
  onBehalfOf: webId,
743
1119
  session: row.session,
744
1120
  entry: approvalUri,
@@ -750,14 +1126,29 @@ export async function resolveRemoteWatchApproval(options) {
750
1126
  }));
751
1127
  if (options.decision === 'accept_for_session') {
752
1128
  const grantId = crypto.randomUUID();
1129
+ const body = grantWikiBodyFromApproval(row, options.grantWikiBody);
753
1130
  await store.insertGrant({
754
1131
  id: grantId,
755
1132
  target: row.target,
756
1133
  action: row.action,
1134
+ title: grantWikiTitleFromApproval(row, options.grantWikiTitle),
1135
+ summary: grantWikiSummaryFromApproval(row, options.grantWikiSummary),
1136
+ body,
1137
+ schema: buildGrantSchemaUri(webId),
1138
+ pageKind: 'autonomy-grant',
1139
+ wikiStatus: 'active',
1140
+ tags: grantWikiTagsFromApproval(row, options.grantWikiTags),
1141
+ source: 'approval',
1142
+ sourceHash: grantSourceHash(row),
1143
+ compiledAt: now,
1144
+ compiledFrom: [approvalUri],
1145
+ related: [row.session],
757
1146
  effect: 'allow',
758
1147
  riskCeiling: row.risk,
1148
+ policy: grantIndexTextFromWikiBody(body),
1149
+ context: grantContextFromApproval(row),
759
1150
  decisionBy: webId,
760
- decisionRole: 'human',
1151
+ decisionRole,
761
1152
  onBehalfOf: webId,
762
1153
  createdAt: now,
763
1154
  });
@@ -778,7 +1169,7 @@ export async function resolveRemoteWatchApproval(options) {
778
1169
  ...row,
779
1170
  status: nextStatus,
780
1171
  decisionBy: webId,
781
- decisionRole: 'human',
1172
+ decisionRole,
782
1173
  onBehalfOf: webId,
783
1174
  reason: encodeDecisionReason(options.decision, options.note),
784
1175
  resolvedAt: now,
@@ -786,6 +1177,18 @@ export async function resolveRemoteWatchApproval(options) {
786
1177
  return normalizeApprovalSummary(nextRow);
787
1178
  });
788
1179
  }
1180
+ async function readRemoteApprovalRow(store, options) {
1181
+ if (store.findApproval) {
1182
+ const row = await store.findApproval(options.approvalId, {
1183
+ resourceUri: options.approvalUri,
1184
+ });
1185
+ if (row || options.approvalUri) {
1186
+ return row;
1187
+ }
1188
+ }
1189
+ const approvals = await store.listApprovals();
1190
+ return approvals.find((entry) => entry.id === options.approvalId) ?? null;
1191
+ }
789
1192
  export const __podApprovalInternal = {
790
1193
  createAbortError,
791
1194
  createDefaultRuntime,
@@ -797,6 +1200,7 @@ export const __podApprovalInternal = {
797
1200
  decisionFromApprovalRow,
798
1201
  encodeDecisionReason,
799
1202
  formatSummaryHeadline,
1203
+ readRemoteApprovalRow,
800
1204
  isRemoteApprovalAbortError,
801
1205
  normalizeApprovalSummary,
802
1206
  parseDecisionReason,