@undefineds.co/linx 0.2.15 → 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 (41) 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-native.js +3 -2
  9. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  10. package/dist/lib/pi-adapter/runtime.js +2 -2
  11. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  12. package/dist/lib/pod-data-session.js +43 -3
  13. package/dist/lib/pod-data-session.js.map +1 -1
  14. package/dist/lib/watch/hooks/claude.js +4 -0
  15. package/dist/lib/watch/hooks/claude.js.map +1 -1
  16. package/dist/lib/watch/hooks/codebuddy.js +4 -0
  17. package/dist/lib/watch/hooks/codebuddy.js.map +1 -1
  18. package/dist/lib/watch/hooks/codex.js +4 -0
  19. package/dist/lib/watch/hooks/codex.js.map +1 -1
  20. package/dist/lib/watch/pod-ai.js +22 -14
  21. package/dist/lib/watch/pod-ai.js.map +1 -1
  22. package/dist/lib/watch/pod-approval.js +387 -35
  23. package/dist/lib/watch/pod-approval.js.map +1 -1
  24. package/dist/lib/watch/pod-persistence.js +162 -43
  25. package/dist/lib/watch/pod-persistence.js.map +1 -1
  26. package/dist/lib/watch/runner.js +240 -38
  27. package/dist/lib/watch/runner.js.map +1 -1
  28. package/dist/lib/watch/secretary.js +238 -0
  29. package/dist/lib/watch/secretary.js.map +1 -0
  30. package/dist/watch-cli.js +8 -34
  31. package/dist/watch-cli.js.map +1 -1
  32. package/package.json +3 -2
  33. package/vendor/agent-runtime/dist/acp.d.ts +27 -0
  34. package/vendor/agent-runtime/dist/acp.js +86 -0
  35. package/vendor/agent-runtime/dist/companion-model.d.ts +7 -0
  36. package/vendor/agent-runtime/dist/companion-model.js +12 -0
  37. package/vendor/agent-runtime/dist/index.d.ts +3 -0
  38. package/vendor/agent-runtime/dist/index.js +3 -0
  39. package/vendor/agent-runtime/dist/turn-controller.d.ts +69 -0
  40. package/vendor/agent-runtime/dist/turn-controller.js +129 -0
  41. 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);
@@ -53,8 +63,8 @@ function documentUrlFromResourceUri(resourceUri) {
53
63
  function buildGrantUri(webIdOrUri, grantId) {
54
64
  return buildGrantResourceUrl(webIdOrUri, grantId);
55
65
  }
56
- function buildGrantDocumentUrl(webIdOrUri) {
57
- return `${getPodBaseUrl(webIdOrUri)}/settings/autonomy/grants.ttl`;
66
+ function buildGrantSchemaUri(webIdOrUri) {
67
+ return `${getPodBaseUrl(webIdOrUri)}/settings/autonomy/schema/grant.ttl#GrantWikiPage`;
58
68
  }
59
69
  function buildAgentUri(webId) {
60
70
  return `${getPodBaseUrl(webId)}/.data/agents/${WATCH_AGENT_ID}.ttl`;
@@ -153,7 +163,12 @@ function parseDecisionReason(value) {
153
163
  }
154
164
  async function warnOnly(runtime, task) {
155
165
  try {
156
- 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
+ ]);
157
172
  }
158
173
  catch (error) {
159
174
  if (runtime.onWarning) {
@@ -172,6 +187,168 @@ function safeJsonStringify(value) {
172
187
  return JSON.stringify({ error: 'unserializable_context' });
173
188
  }
174
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
+ }
175
352
  function extractSessionId(sessionUri) {
176
353
  if (sessionUri.includes('#')) {
177
354
  return sessionUri.split('#').pop() || sessionUri;
@@ -196,6 +373,7 @@ function normalizeApprovalSummary(row) {
196
373
  const createdAt = toIsoString(row.createdAt, new Date(0).toISOString());
197
374
  const sessionUri = row.session;
198
375
  const decision = decisionFromApprovalRow(row);
376
+ const approvalOptions = parseApprovalOptions(row.approvalOptions);
199
377
  return {
200
378
  id: row.id,
201
379
  ...(normalizeString(row.approvalUri) ? { approvalUri: normalizeString(row.approvalUri) } : {}),
@@ -209,7 +387,9 @@ function normalizeApprovalSummary(row) {
209
387
  ...(normalizeString(row.assignedTo) ? { assignedTo: normalizeString(row.assignedTo) } : {}),
210
388
  ...(normalizeString(row.decisionBy) ? { decisionBy: normalizeString(row.decisionBy) } : {}),
211
389
  ...(decision ? { decision } : {}),
390
+ ...(approvalOptions ? { approvalOptions } : {}),
212
391
  createdAt,
392
+ ...(row.expiresAt ? { expiresAt: toIsoString(row.expiresAt, createdAt) } : {}),
213
393
  ...(row.resolvedAt ? { resolvedAt: toIsoString(row.resolvedAt, createdAt) } : {}),
214
394
  };
215
395
  }
@@ -255,6 +435,7 @@ async function createDefaultRuntime() {
255
435
  now() {
256
436
  return new Date();
257
437
  },
438
+ resolveGrantCoverage: resolveWatchGrantCoverage,
258
439
  };
259
440
  }
260
441
  async function withRemoteApprovalStore(runtime, fn) {
@@ -342,13 +523,12 @@ async function readApprovalRowFromResource(fetcher, resourceUri) {
342
523
  return null;
343
524
  }
344
525
  async function listApprovalRows(webId, fetcher) {
345
- const [currentUrls, legacyUrls] = await Promise.all([
346
- listTurtleResourcesRecursive(fetcher, `${getPodBaseUrl(webId)}/.data/approvals/`).catch(() => []),
347
- listTurtleResources(fetcher, `${getPodBaseUrl(webId)}/.data/approvals/`).catch(() => []),
348
- ]);
349
- const urls = [...new Set([...currentUrls, ...legacyUrls])];
526
+ const urls = [
527
+ ...recentApprovalDocumentUrls(webId),
528
+ ...await listTurtleResources(fetcher, `${getPodBaseUrl(webId)}/.data/approvals/`).catch(() => []),
529
+ ];
350
530
  const rows = [];
351
- for (const url of urls.filter((entry) => entry.endsWith('.ttl'))) {
531
+ for (const url of [...new Set(urls)].filter((entry) => entry.endsWith('.ttl'))) {
352
532
  const turtle = await readTurtleResource(fetcher, url).catch(() => null);
353
533
  if (!turtle)
354
534
  continue;
@@ -360,6 +540,15 @@ async function listApprovalRows(webId, fetcher) {
360
540
  }
361
541
  return rows;
362
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
+ }
363
552
  async function writeApprovalRow(webId, fetcher, row) {
364
553
  const createdAt = new Date(toIsoString(row.createdAt, new Date().toISOString()));
365
554
  const documentUrl = buildApprovalDocumentUrl(webId, createdAt);
@@ -380,8 +569,11 @@ async function writeApprovalRow(webId, fetcher, row) {
380
569
  ...(row.decisionRole ? [{ predicate: ApprovalVocab.decisionRole, object: literal(row.decisionRole) }] : []),
381
570
  ...(row.onBehalfOf ? [{ predicate: ApprovalVocab.onBehalfOf, object: iri(row.onBehalfOf) }] : []),
382
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) }] : []),
383
574
  ...(row.policyVersion ? [{ predicate: ApprovalVocab.policyVersion, object: literal(row.policyVersion) }] : []),
384
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())) }] : []),
385
577
  ...(row.resolvedAt ? [{ predicate: ApprovalVocab.resolvedAt, object: literal(toIsoString(row.resolvedAt, new Date().toISOString())) }] : []),
386
578
  ],
387
579
  });
@@ -425,7 +617,6 @@ async function writeAuditRow(webId, fetcher, row) {
425
617
  }
426
618
  async function listGrantRows(webId, fetcher) {
427
619
  const urls = [
428
- `${getPodBaseUrl(webId)}/settings/autonomy/grants.ttl`,
429
620
  ...await listTurtleResources(fetcher, `${getPodBaseUrl(webId)}/settings/autonomy/grants/`).catch(() => []),
430
621
  ];
431
622
  const rows = [];
@@ -443,8 +634,8 @@ async function listGrantRows(webId, fetcher) {
443
634
  }
444
635
  async function writeGrantRow(webId, fetcher, row) {
445
636
  const id = normalizeString(row.id) ?? crypto.randomUUID();
446
- const documentUrl = buildGrantDocumentUrl(webId);
447
637
  const subjectUrl = buildGrantResourceUrl(webId, id);
638
+ const documentUrl = subjectUrl;
448
639
  const target = normalizeString(row.target);
449
640
  const action = normalizeString(row.action);
450
641
  const effect = normalizeString(row.effect);
@@ -460,8 +651,22 @@ async function writeGrantRow(webId, fetcher, row) {
460
651
  { predicate: RDF_TYPE, object: iri(UDFS.AutonomyGrant) },
461
652
  { predicate: GrantVocab.target, object: iri(target) },
462
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) })),
463
666
  { predicate: GrantVocab.effect, object: literal(effect) },
464
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)) }] : []),
465
670
  { predicate: GrantVocab.decisionBy, object: iri(decisionBy) },
466
671
  { predicate: GrantVocab.decisionRole, object: literal(decisionRole) },
467
672
  ...(normalizeString(row.onBehalfOf) ? [{ predicate: GrantVocab.onBehalfOf, object: iri(normalizeString(row.onBehalfOf)) }] : []),
@@ -508,8 +713,11 @@ function approvalRowFromPredicates(url, predicates) {
508
713
  decisionRole: firstLiteral(predicates, ApprovalVocab.decisionRole),
509
714
  onBehalfOf: firstIri(predicates, ApprovalVocab.onBehalfOf),
510
715
  reason: firstLiteral(predicates, ApprovalVocab.reason),
716
+ context: firstLiteral(predicates, ApprovalVocab.context),
717
+ approvalOptions: firstLiteral(predicates, ApprovalVocab.approvalOptions),
511
718
  policyVersion: firstLiteral(predicates, ApprovalVocab.policyVersion),
512
719
  createdAt,
720
+ expiresAt: firstLiteral(predicates, ApprovalVocab.expiresAt),
513
721
  resolvedAt: firstLiteral(predicates, ApprovalVocab.resolvedAt),
514
722
  };
515
723
  }
@@ -550,8 +758,22 @@ function grantRowFromPredicates(url, predicates) {
550
758
  id: subjectIdFromResourceUrl(url),
551
759
  target,
552
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),
553
773
  effect,
554
774
  riskCeiling: firstLiteral(predicates, GrantVocab.riskCeiling),
775
+ policy: firstLiteral(predicates, GrantVocab.policy),
776
+ context: firstLiteral(predicates, GrantVocab.context),
555
777
  decisionBy,
556
778
  decisionRole,
557
779
  onBehalfOf: firstIri(predicates, GrantVocab.onBehalfOf),
@@ -559,11 +781,88 @@ function grantRowFromPredicates(url, predicates) {
559
781
  revokedAt: firstLiteral(predicates, GrantVocab.revokedAt),
560
782
  };
561
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
+ }
562
861
  export async function createRemoteWatchApproval(options) {
563
862
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
564
863
  return createRemoteApproval({
565
864
  subject: ({ webId }) => ({
566
- sessionUri: buildThreadUri(webId, options.record.id),
865
+ sessionUri: buildThreadUri(webId, options.record),
567
866
  actorUri: buildAgentUri(webId),
568
867
  policyVersion: REMOTE_APPROVAL_POLICY_VERSION,
569
868
  }),
@@ -576,6 +875,9 @@ export async function createRemoteWatchApproval(options) {
576
875
  risk: buildRisk(options.request),
577
876
  ...(options.request.kind === 'command-approval' && options.request.command ? { command: options.request.command } : {}),
578
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 } : {}),
579
881
  entry: sessionUri,
580
882
  }),
581
883
  runtime: activeRuntime,
@@ -599,6 +901,9 @@ export async function createRemoteApproval(options) {
599
901
  const onBehalfOf = subject.onBehalfOf ?? webId;
600
902
  const policyVersion = subject.policyVersion ?? REMOTE_APPROVAL_POLICY_VERSION;
601
903
  const requestEntry = request.entry ?? approvalUri;
904
+ const expiresAt = resolveApprovalExpiresAt(request, now);
905
+ const approvalOptions = encodeApprovalOptions(request.approvalOptions);
906
+ const context = compactApprovalContext(request);
602
907
  await store.insertApproval({
603
908
  id: approvalId,
604
909
  session: sessionUri,
@@ -609,8 +914,11 @@ export async function createRemoteApproval(options) {
609
914
  risk: request.risk,
610
915
  status: 'pending',
611
916
  assignedTo,
917
+ context,
918
+ ...(approvalOptions ? { approvalOptions } : {}),
612
919
  policyVersion,
613
920
  createdAt: now,
921
+ ...(expiresAt ? { expiresAt } : {}),
614
922
  });
615
923
  const requestAudit = {
616
924
  id: crypto.randomUUID(),
@@ -644,8 +952,11 @@ export async function createRemoteApproval(options) {
644
952
  risk: request.risk,
645
953
  status: 'pending',
646
954
  assignedTo,
955
+ context,
956
+ ...(approvalOptions ? { approvalOptions } : {}),
647
957
  policyVersion,
648
958
  createdAt: now,
959
+ ...(expiresAt ? { expiresAt } : {}),
649
960
  });
650
961
  });
651
962
  }
@@ -676,17 +987,20 @@ export async function requestRemoteWatchApproval(options) {
676
987
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
677
988
  const delegated = await withRemoteApprovalStore(activeRuntime, async ({ store, webId }) => {
678
989
  const grants = await store.listGrants();
679
- const requestAction = buildActionUri(options.request);
680
- const requestTarget = buildThreadUri(webId, options.record.id);
681
- const requestRisk = buildRisk(options.request);
682
- return grants.some((grant) => (grant.effect === 'allow'
683
- && grant.action === requestAction
684
- && grant.target === requestTarget
685
- && riskScore(typeof grant.riskCeiling === 'string' ? grant.riskCeiling : undefined) >= riskScore(requestRisk)
686
- && !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
+ });
687
1001
  });
688
1002
  if (delegated) {
689
- return 'accept_for_session';
1003
+ return delegated;
690
1004
  }
691
1005
  const summary = await createRemoteWatchApproval({
692
1006
  record: options.record,
@@ -701,6 +1015,23 @@ export async function requestRemoteWatchApproval(options) {
701
1015
  runtime: activeRuntime,
702
1016
  });
703
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
+ }
704
1035
  export async function requestRemoteApproval(options) {
705
1036
  const activeRuntime = options.runtime ?? await createDefaultRuntime();
706
1037
  const delegated = await withRemoteApprovalStore(activeRuntime, async ({ store, webId, stored }) => {
@@ -711,15 +1042,20 @@ export async function requestRemoteApproval(options) {
711
1042
  ? options.request({ webId, stored, sessionUri: subject.sessionUri })
712
1043
  : options.request;
713
1044
  const grants = await store.listGrants();
714
- const requestTarget = subject.targetUri ?? subject.sessionUri;
715
- return grants.some((grant) => (grant.effect === 'allow'
716
- && grant.action === request.action
717
- && grant.target === requestTarget
718
- && riskScore(typeof grant.riskCeiling === 'string' ? grant.riskCeiling : undefined) >= riskScore(request.risk)
719
- && !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
+ });
720
1056
  });
721
1057
  if (delegated) {
722
- return 'accept_for_session';
1058
+ return delegated;
723
1059
  }
724
1060
  const summary = await createRemoteApproval({
725
1061
  subject: options.subject,
@@ -765,10 +1101,11 @@ export async function resolveRemoteWatchApproval(options) {
765
1101
  const nextStatus = options.decision === 'accept' || options.decision === 'accept_for_session'
766
1102
  ? 'approved'
767
1103
  : 'rejected';
1104
+ const decisionRole = options.decisionRole ?? 'human';
768
1105
  await store.updateApproval(row.id, {
769
1106
  status: nextStatus,
770
1107
  decisionBy: webId,
771
- decisionRole: 'human',
1108
+ decisionRole,
772
1109
  onBehalfOf: webId,
773
1110
  reason: encodeDecisionReason(options.decision, options.note),
774
1111
  resolvedAt: now,
@@ -777,7 +1114,7 @@ export async function resolveRemoteWatchApproval(options) {
777
1114
  id: crypto.randomUUID(),
778
1115
  action: nextStatus === 'approved' ? 'approval_approved' : 'approval_rejected',
779
1116
  actor: webId,
780
- actorRole: 'human',
1117
+ actorRole: decisionRole,
781
1118
  onBehalfOf: webId,
782
1119
  session: row.session,
783
1120
  entry: approvalUri,
@@ -789,14 +1126,29 @@ export async function resolveRemoteWatchApproval(options) {
789
1126
  }));
790
1127
  if (options.decision === 'accept_for_session') {
791
1128
  const grantId = crypto.randomUUID();
1129
+ const body = grantWikiBodyFromApproval(row, options.grantWikiBody);
792
1130
  await store.insertGrant({
793
1131
  id: grantId,
794
1132
  target: row.target,
795
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],
796
1146
  effect: 'allow',
797
1147
  riskCeiling: row.risk,
1148
+ policy: grantIndexTextFromWikiBody(body),
1149
+ context: grantContextFromApproval(row),
798
1150
  decisionBy: webId,
799
- decisionRole: 'human',
1151
+ decisionRole,
800
1152
  onBehalfOf: webId,
801
1153
  createdAt: now,
802
1154
  });
@@ -817,7 +1169,7 @@ export async function resolveRemoteWatchApproval(options) {
817
1169
  ...row,
818
1170
  status: nextStatus,
819
1171
  decisionBy: webId,
820
- decisionRole: 'human',
1172
+ decisionRole,
821
1173
  onBehalfOf: webId,
822
1174
  reason: encodeDecisionReason(options.decision, options.note),
823
1175
  resolvedAt: now,