@safebrowse/daemon 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface SafeBrowseDaemonOptions {
8
8
  policyPack?: PolicyPack;
9
9
  knowledgeBase?: KnowledgeBaseContext;
10
10
  verifiedRegistry?: VerifiedRegistryBundle;
11
+ parserAllowlistedEgress?: string[];
11
12
  }
12
13
  export declare function createSafeBrowseServer(options?: SafeBrowseDaemonOptions): Promise<Server>;
13
14
  export declare function startSafeBrowseDaemon(options?: SafeBrowseDaemonOptions): Promise<Server>;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAIjG,OAAO,EAcL,KAAK,oBAAoB,EAEzB,KAAK,UAAU,EAMhB,MAAM,kBAAkB,CAAC;AAQ1B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE/D,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;CAC3C;AAgGD,wBAAsB,sBAAsB,CAC1C,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,MAAM,CAAC,CAuHjB;AAED,wBAAsB,qBAAqB,CACzC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,MAAM,CAAC,CAWjB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAIjG,OAAO,EA4BL,KAAK,oBAAoB,EAKzB,KAAK,UAAU,EAShB,MAAM,kBAAkB,CAAC;AAS1B,OAAO,KAAK,EAAE,sBAAsB,EAAyB,MAAM,kBAAkB,CAAC;AAEtF,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IAC1C,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;CACpC;AAgYD,wBAAsB,sBAAsB,CAC1C,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,MAAM,CAAC,CA+fjB;AAED,wBAAsB,qBAAqB,CACzC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,MAAM,CAAC,CAWjB"}
package/dist/server.js CHANGED
@@ -1,10 +1,14 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { stat } from "node:fs/promises";
3
3
  import { createServer } from "node:http";
4
4
  import { resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { brokerArtifact, brokerArtifactV2, buildReplayBundle, compilePolicy, evaluateAction, evaluateMemoryWrite, evaluateToolRequest, prepareToolOnboarding, sanitizeObservation, verifyToolCallback } from "@safebrowse/core";
6
+ import { applyV4FailClosedMediation, attachCapabilitiesToPlannerInput, brokerArtifact, brokerArtifactV2, buildReplayBundle, compilePolicy, createApprovalGrantHash, evaluateAction, evaluateCapabilityUse, evaluateMemoryWrite, evaluateMemoryWriteV4, evaluateToolRequest, mintCapabilitiesForObservation, prepareToolOnboarding, prepareToolOnboardingV4, promoteMemoryRecordV4, rollbackMemoryRecordV4, sanitizeObservation, verifyToolCallback, verifyToolCallbackV4 } from "@safebrowse/core";
7
7
  import { buildRegistryDefaults, loadKnowledgeBaseContext, loadVerifiedRegistryBundle, loadPolicyPackFromPaths, resolvePolicyLayerFiles } from "./loaders.js";
8
+ import { compileObservationInIsolation, probeParserIsolation } from "./parserIsolation.js";
9
+ function hashValue(input) {
10
+ return createHash("sha256").update(JSON.stringify(input)).digest("hex");
11
+ }
8
12
  async function readJson(request) {
9
13
  const chunks = [];
10
14
  for await (const chunk of request) {
@@ -17,6 +21,17 @@ function writeJson(response, statusCode, payload) {
17
21
  response.setHeader("content-type", "application/json; charset=utf-8");
18
22
  response.end(JSON.stringify(payload, null, 2));
19
23
  }
24
+ function legacyResponseMeta(route) {
25
+ return {
26
+ deprecated: true,
27
+ telemetry: {
28
+ deprecated: true,
29
+ claimScope: "legacy_compatibility",
30
+ preventionClaim: false,
31
+ routeVersion: route
32
+ }
33
+ };
34
+ }
20
35
  async function fileExists(path) {
21
36
  try {
22
37
  await stat(path);
@@ -45,31 +60,122 @@ async function buildRuntimeContext(options) {
45
60
  return {
46
61
  policy: compilePolicy(policyPack),
47
62
  knowledgeBase,
48
- verifiedRegistry
63
+ verifiedRegistry,
64
+ parserAllowlistedEgress: options.parserAllowlistedEgress ?? []
49
65
  };
50
66
  }
51
67
  function plusMinutes(value, minutes) {
52
68
  return new Date(new Date(value).getTime() + minutes * 60_000).toISOString();
53
69
  }
54
- function createOnboardingSession(request, runtime, workflowBindingId) {
55
- const callbackUri = request.oauthContext?.callbackUri ??
70
+ function plusSeconds(value, seconds) {
71
+ return new Date(new Date(value).getTime() + seconds * 1_000).toISOString();
72
+ }
73
+ function createWorkflowHash(payload) {
74
+ return hashValue({
75
+ taskId: payload.taskId,
76
+ userGoal: payload.userGoal,
77
+ phase: payload.phase ?? "",
78
+ allowedOrigins: payload.allowedOrigins ?? [],
79
+ allowedVerbs: payload.allowedVerbs ?? [],
80
+ forbiddenSinks: payload.forbiddenSinks ?? []
81
+ });
82
+ }
83
+ function createSessionState(request, runtime) {
84
+ const createdAt = new Date().toISOString();
85
+ const allowedOrigins = request.allowedOrigins ??
86
+ [...runtime.policy.readOnlyOrigins, ...runtime.policy.writableOrigins];
87
+ const allowedVerbs = request.allowedVerbs ?? [...runtime.policy.allowedActions];
88
+ const forbiddenSinks = request.forbiddenSinks ?? [];
89
+ const session = {
90
+ sessionId: randomUUID(),
91
+ taskId: request.taskId,
92
+ userGoal: request.userGoal,
93
+ phase: request.phase,
94
+ allowedOrigins,
95
+ allowedVerbs,
96
+ forbiddenSinks,
97
+ workflowHash: createWorkflowHash({
98
+ taskId: request.taskId,
99
+ userGoal: request.userGoal,
100
+ phase: request.phase,
101
+ allowedOrigins,
102
+ allowedVerbs,
103
+ forbiddenSinks
104
+ }),
105
+ currentStep: 0,
106
+ createdAt,
107
+ expiresAt: plusSeconds(createdAt, request.expiresInSeconds ?? 1800)
108
+ };
109
+ return {
110
+ session,
111
+ capabilities: new Map(),
112
+ usedCapabilities: new Set(),
113
+ authorityReduced: false,
114
+ authorityReductionReasons: [],
115
+ approvalGrants: new Map(),
116
+ memoryRecords: new Map(),
117
+ memorySnapshots: new Map(),
118
+ onboardingSessions: new Map()
119
+ };
120
+ }
121
+ function shouldReduceAuthority(sessionState, observation) {
122
+ if (sessionState.authorityReduced) {
123
+ return true;
124
+ }
125
+ const hasPriorSurface = Boolean(sessionState.latestObservation);
126
+ const hasMeaningfulRisk = observation.parseStatus !== "compiled" ||
127
+ observation.riskFindings.length > 0 ||
128
+ observation.secretFindings.length > 0;
129
+ return hasPriorSurface && hasMeaningfulRisk;
130
+ }
131
+ function applyAuthorityReduction(sessionState, observation, plannerInput) {
132
+ const reasons = [
133
+ ...sessionState.authorityReductionReasons,
134
+ ...(observation.parseStatus !== "compiled"
135
+ ? [`parse_status_${observation.parseStatus}`]
136
+ : []),
137
+ ...observation.riskFindings,
138
+ ...(observation.secretFindings.length ? ["secret_redaction_boundary"] : [])
139
+ ];
140
+ sessionState.authorityReduced = true;
141
+ sessionState.authorityReductionReasons = [...new Set(reasons)];
142
+ sessionState.capabilities.clear();
143
+ return {
144
+ ...plannerInput,
145
+ candidateCapabilities: [],
146
+ riskMarkers: [
147
+ ...new Set([
148
+ ...plannerInput.riskMarkers,
149
+ "multimodal_reducer_active",
150
+ ...sessionState.authorityReductionReasons.map((reason) => `chain:${reason}`)
151
+ ])
152
+ ]
153
+ };
154
+ }
155
+ function createOnboardingSession(request, runtime, approvalGrant, verifiedRegistryEntry) {
156
+ const callbackUri = verifiedRegistryEntry?.allowedRedirectUris[0] ??
157
+ request.oauthContext?.callbackUri ??
56
158
  request.callbackUri ??
57
159
  request.oauthContext?.redirectUri ??
58
160
  request.requestedRedirectUri;
59
161
  if (!callbackUri) {
60
162
  return undefined;
61
163
  }
164
+ const callbackOrigin = verifiedRegistryEntry?.allowedCallbackOrigins[0] ?? new URL(callbackUri).origin;
62
165
  const createdAt = (runtime.now?.() ?? new Date()).toISOString();
63
166
  return {
64
167
  sessionId: randomUUID(),
65
- approvalBindingId: request.approvalBindingId ?? randomUUID(),
66
- workflowBindingId,
168
+ approvalBindingId: approvalGrant.approvalGrantId,
169
+ workflowBindingId: request.sourceObservationId,
67
170
  toolId: request.toolId,
68
- registryEntryId: request.registryEntryId ?? request.toolId,
69
- registryBundleId: request.registryBundleId ?? runtime.verifiedRegistry?.bundleId ?? "unverified-registry",
171
+ registryEntryId: verifiedRegistryEntry?.registryEntryId ?? request.registryEntryId ?? request.toolId,
172
+ registryBundleId: verifiedRegistryEntry?.bundleId ??
173
+ request.registryBundleId ??
174
+ runtime.verifiedRegistry?.bundleId ??
175
+ "unverified-registry",
70
176
  callbackUri,
71
- callbackOrigin: new URL(callbackUri).origin,
72
- requestedScopes: request.requestedScopes ?? request.oauthContext?.requestedScopes ?? [],
177
+ callbackOrigin,
178
+ requestedScopes: approvalGrant.scopes,
73
179
  state: randomUUID(),
74
180
  pkceMethod: "S256",
75
181
  createdAt,
@@ -77,9 +183,80 @@ function createOnboardingSession(request, runtime, workflowBindingId) {
77
183
  status: "prepared"
78
184
  };
79
185
  }
186
+ function issueApprovalGrant(request, sessionState) {
187
+ const issuedAt = new Date().toISOString();
188
+ const grantWithoutHash = {
189
+ approvalGrantId: randomUUID(),
190
+ sessionId: sessionState.session.sessionId,
191
+ workflowHash: sessionState.session.workflowHash,
192
+ connectorId: request.connectorId,
193
+ scopes: request.scopes ?? [],
194
+ sinkClass: request.sinkClass,
195
+ capabilityIds: request.capabilityIds ?? [],
196
+ targetOrigin: request.targetOrigin,
197
+ issuedAt,
198
+ expiresAt: plusSeconds(issuedAt, request.expiresInSeconds ?? 600)
199
+ };
200
+ return {
201
+ ...grantWithoutHash,
202
+ grantHash: createApprovalGrantHash(grantWithoutHash)
203
+ };
204
+ }
205
+ function findSessionState(sessions, sessionId) {
206
+ const sessionState = sessions.get(sessionId);
207
+ if (!sessionState) {
208
+ return undefined;
209
+ }
210
+ if (new Date(sessionState.session.expiresAt).getTime() <= Date.now()) {
211
+ sessions.delete(sessionId);
212
+ return undefined;
213
+ }
214
+ return sessionState;
215
+ }
216
+ function buildLegacyObservationCapture(payload) {
217
+ if (payload.surfaceType === "pdf") {
218
+ return {
219
+ mimeType: "application/pdf",
220
+ sourceOrigin: payload.url,
221
+ viewerOrigin: payload.frameUrl ?? payload.url,
222
+ renderedText: payload.renderedText,
223
+ extractedText: payload.extractedText,
224
+ ocrText: payload.ocrText,
225
+ annotations: payload.annotations,
226
+ metadataText: payload.metadataText,
227
+ extractionMethod: "download",
228
+ trustSignals: payload.trustSignals
229
+ };
230
+ }
231
+ if (payload.surfaceType === "image") {
232
+ return {
233
+ mimeType: "image/png",
234
+ sourceOrigin: payload.url,
235
+ viewerOrigin: payload.frameUrl ?? payload.url,
236
+ ocrText: payload.ocrText,
237
+ metadataText: payload.metadataText,
238
+ extractionMethod: "ocr",
239
+ trustSignals: payload.trustSignals
240
+ };
241
+ }
242
+ if (payload.surfaceType === "tool_manifest") {
243
+ return {
244
+ mimeType: "application/json",
245
+ surfaceKind: "tool_manifest",
246
+ sourceOrigin: payload.url,
247
+ viewerOrigin: payload.frameUrl ?? payload.url,
248
+ extractedText: payload.description,
249
+ metadataText: payload.schemaDescriptions,
250
+ extractionMethod: "api",
251
+ trustSignals: payload.trustSignals
252
+ };
253
+ }
254
+ return undefined;
255
+ }
80
256
  export async function createSafeBrowseServer(options = {}) {
81
257
  const runtime = await buildRuntimeContext(options);
82
258
  const onboardingSessions = new Map();
259
+ const sessions = new Map();
83
260
  return createServer(async (request, response) => {
84
261
  if (!request.url) {
85
262
  writeJson(response, 400, { error: "missing_url" });
@@ -87,6 +264,7 @@ export async function createSafeBrowseServer(options = {}) {
87
264
  }
88
265
  try {
89
266
  if (request.method === "GET" && request.url === "/health") {
267
+ const parserProbe = await probeParserIsolation();
90
268
  writeJson(response, 200, {
91
269
  status: "ok",
92
270
  profile: runtime.policy.profile,
@@ -99,7 +277,8 @@ export async function createSafeBrowseServer(options = {}) {
99
277
  signatureVerified: runtime.verifiedRegistry.signatureVerified,
100
278
  entryCount: runtime.verifiedRegistry.entries.length
101
279
  }
102
- : undefined
280
+ : undefined,
281
+ parserIsolation: parserProbe
103
282
  });
104
283
  return;
105
284
  }
@@ -109,39 +288,73 @@ export async function createSafeBrowseServer(options = {}) {
109
288
  }
110
289
  if (request.url === "/v1/observe") {
111
290
  const payload = await readJson(request);
112
- writeJson(response, 200, sanitizeObservation(payload, runtime));
291
+ writeJson(response, 200, {
292
+ ...sanitizeObservation(payload, runtime),
293
+ ...legacyResponseMeta("/v1/observe")
294
+ });
113
295
  return;
114
296
  }
115
297
  if (request.url === "/v1/action") {
116
298
  const payload = await readJson(request);
117
- writeJson(response, 200, evaluateAction(payload, runtime));
299
+ writeJson(response, 200, {
300
+ ...evaluateAction(payload, runtime),
301
+ ...legacyResponseMeta("/v1/action")
302
+ });
118
303
  return;
119
304
  }
120
305
  if (request.url === "/v1/artifact") {
121
306
  const payload = await readJson(request);
122
- writeJson(response, 200, brokerArtifact(payload, runtime));
307
+ writeJson(response, 200, {
308
+ ...brokerArtifact(payload, runtime),
309
+ ...legacyResponseMeta("/v1/artifact")
310
+ });
123
311
  return;
124
312
  }
125
313
  if (request.url === "/v1/tool") {
126
314
  const payload = await readJson(request);
127
- writeJson(response, 200, evaluateToolRequest(payload, runtime));
315
+ writeJson(response, 200, {
316
+ ...evaluateToolRequest(payload, runtime),
317
+ ...legacyResponseMeta("/v1/tool")
318
+ });
128
319
  return;
129
320
  }
130
321
  if (request.url === "/v1/memory") {
131
322
  const payload = await readJson(request);
132
- writeJson(response, 200, evaluateMemoryWrite(payload, runtime));
323
+ writeJson(response, 200, {
324
+ ...evaluateMemoryWrite(payload, runtime),
325
+ ...legacyResponseMeta("/v1/memory")
326
+ });
133
327
  return;
134
328
  }
135
329
  if (request.url === "/v1/replay") {
136
330
  const payload = await readJson(request);
137
- writeJson(response, 200, buildReplayBundle(payload.events, runtime));
331
+ writeJson(response, 200, {
332
+ ...buildReplayBundle(payload.events, runtime),
333
+ ...legacyResponseMeta("/v1/replay")
334
+ });
138
335
  return;
139
336
  }
140
337
  if (request.url === "/v2/tool/prepare") {
141
338
  const payload = await readJson(request);
142
339
  const prepared = prepareToolOnboarding(payload, runtime);
143
340
  const onboardingSession = prepared.verdict.decision === "ALLOW" && payload.authType === "oauth"
144
- ? createOnboardingSession(payload, runtime, prepared.workflowBinding?.bindingId)
341
+ ? createOnboardingSession(payload, runtime, {
342
+ approvalGrantId: payload.approvalBindingId ?? randomUUID(),
343
+ sessionId: "legacy-session",
344
+ workflowHash: hashValue(payload.sourceObservationId ?? payload.requestId),
345
+ connectorId: payload.toolId,
346
+ scopes: payload.requestedScopes ?? [],
347
+ sinkClass: "connector_oauth",
348
+ capabilityIds: payload.capabilityId ? [payload.capabilityId] : [],
349
+ targetOrigin: payload.callbackOrigin ??
350
+ payload.oauthContext?.callbackOrigin ??
351
+ payload.callbackUri ??
352
+ payload.requestedRedirectUri ??
353
+ "unknown",
354
+ issuedAt: new Date().toISOString(),
355
+ expiresAt: plusMinutes(new Date().toISOString(), 10),
356
+ grantHash: "legacy"
357
+ })
145
358
  : undefined;
146
359
  if (onboardingSession) {
147
360
  onboardingSessions.set(onboardingSession.sessionId, onboardingSession);
@@ -150,7 +363,8 @@ export async function createSafeBrowseServer(options = {}) {
150
363
  verdict: prepared.verdict,
151
364
  verifiedRegistryEntry: prepared.verifiedRegistryEntry,
152
365
  workflowBinding: prepared.workflowBinding,
153
- onboardingSession
366
+ onboardingSession,
367
+ ...legacyResponseMeta("/v2/tool/prepare")
154
368
  });
155
369
  return;
156
370
  }
@@ -164,12 +378,265 @@ export async function createSafeBrowseServer(options = {}) {
164
378
  status: result.verdict.decision === "ALLOW" ? "used" : session.status
165
379
  });
166
380
  }
167
- writeJson(response, 200, result);
381
+ writeJson(response, 200, {
382
+ ...result,
383
+ ...legacyResponseMeta("/v2/tool/callback/verify")
384
+ });
168
385
  return;
169
386
  }
170
387
  if (request.url === "/v2/artifact") {
171
388
  const payload = await readJson(request);
172
- writeJson(response, 200, brokerArtifactV2(payload, runtime));
389
+ writeJson(response, 200, {
390
+ ...brokerArtifactV2(payload, runtime),
391
+ ...legacyResponseMeta("/v2/artifact")
392
+ });
393
+ return;
394
+ }
395
+ if (request.url === "/v4/session/start") {
396
+ const payload = await readJson(request);
397
+ const sessionState = createSessionState(payload, runtime);
398
+ sessions.set(sessionState.session.sessionId, sessionState);
399
+ writeJson(response, 200, {
400
+ session: sessionState.session
401
+ });
402
+ return;
403
+ }
404
+ if (request.url === "/v4/observe") {
405
+ const payload = await readJson(request);
406
+ const sessionState = findSessionState(sessions, payload.sessionId);
407
+ if (!sessionState) {
408
+ writeJson(response, 404, { error: "unknown_session" });
409
+ return;
410
+ }
411
+ const capture = {
412
+ ...payload.capture,
413
+ sessionId: payload.sessionId,
414
+ taskId: sessionState.session.taskId
415
+ };
416
+ const observation = await compileObservationInIsolation({
417
+ capture,
418
+ workflowHash: sessionState.session.workflowHash,
419
+ allowlistedEgress: runtime.parserAllowlistedEgress,
420
+ runtime: {
421
+ knowledgeBase: runtime.knowledgeBase
422
+ }
423
+ });
424
+ const failClosedObservation = applyV4FailClosedMediation(observation.compiledObservation, observation.plannerInput, "observe");
425
+ let capabilities = mintCapabilitiesForObservation(sessionState.session, observation.compiledObservation, {
426
+ sourceObservationId: observation.compiledObservation.observationId
427
+ });
428
+ let plannerInput = failClosedObservation.plannerInput;
429
+ if (shouldReduceAuthority(sessionState, observation.compiledObservation)) {
430
+ plannerInput = applyAuthorityReduction(sessionState, observation.compiledObservation, plannerInput);
431
+ capabilities = [];
432
+ }
433
+ sessionState.capabilities.clear();
434
+ for (const capability of capabilities) {
435
+ sessionState.capabilities.set(capability.capabilityId, capability);
436
+ }
437
+ sessionState.latestObservation = observation.compiledObservation;
438
+ writeJson(response, 200, {
439
+ compiledObservation: observation.compiledObservation,
440
+ observationVerdict: failClosedObservation.verdict,
441
+ plannerInput: attachCapabilitiesToPlannerInput(plannerInput, capabilities)
442
+ });
443
+ return;
444
+ }
445
+ if (request.url === "/v4/action/evaluate") {
446
+ const payload = await readJson(request);
447
+ const sessionState = findSessionState(sessions, payload.sessionId);
448
+ const capability = sessionState?.capabilities.get(payload.capabilityId);
449
+ const verdict = evaluateCapabilityUse(payload, sessionState?.session, capability, {
450
+ alreadyUsed: sessionState?.usedCapabilities.has(payload.capabilityId) ?? false
451
+ });
452
+ if (verdict.decision === "ALLOW" && sessionState && capability) {
453
+ sessionState.capabilities.delete(payload.capabilityId);
454
+ sessionState.usedCapabilities.add(payload.capabilityId);
455
+ sessionState.session = {
456
+ ...sessionState.session,
457
+ currentStep: sessionState.session.currentStep + 1
458
+ };
459
+ }
460
+ writeJson(response, 200, {
461
+ verdict,
462
+ executionPlan: verdict.decision === "ALLOW" && capability
463
+ ? {
464
+ verb: capability.kind,
465
+ targetUrl: capability.targetUrl,
466
+ targetOrigin: capability.targetOrigin,
467
+ selector: capability.selector,
468
+ derivedSinkClass: capability.derivedSinkClass,
469
+ derivedSensitiveSink: capability.derivedSensitiveSink
470
+ }
471
+ : undefined
472
+ });
473
+ return;
474
+ }
475
+ if (request.url === "/v4/approval/grant") {
476
+ const payload = await readJson(request);
477
+ const sessionState = findSessionState(sessions, payload.sessionId);
478
+ if (!sessionState) {
479
+ writeJson(response, 404, { error: "unknown_session" });
480
+ return;
481
+ }
482
+ const unknownCapability = (payload.capabilityIds ?? []).find((capabilityId) => !sessionState.capabilities.has(capabilityId));
483
+ if (unknownCapability) {
484
+ writeJson(response, 400, {
485
+ error: "unknown_capability",
486
+ capabilityId: unknownCapability
487
+ });
488
+ return;
489
+ }
490
+ if (payload.sinkClass === "connector_oauth" &&
491
+ !(payload.capabilityIds ?? []).length) {
492
+ writeJson(response, 400, {
493
+ error: "capability_ids_required",
494
+ sinkClass: payload.sinkClass
495
+ });
496
+ return;
497
+ }
498
+ const approvalGrant = issueApprovalGrant(payload, sessionState);
499
+ sessionState.approvalGrants.set(approvalGrant.approvalGrantId, approvalGrant);
500
+ writeJson(response, 200, {
501
+ approvalGrant
502
+ });
503
+ return;
504
+ }
505
+ if (request.url === "/v4/tool/prepare") {
506
+ const payload = await readJson(request);
507
+ const sessionState = findSessionState(sessions, payload.sessionId);
508
+ const approvalGrant = sessionState?.approvalGrants.get(payload.approvalGrantId);
509
+ const prepared = prepareToolOnboardingV4({
510
+ ...payload.request,
511
+ approvalGrantId: payload.approvalGrantId
512
+ }, sessionState?.session, approvalGrant, runtime);
513
+ const onboardingSession = prepared.verdict.decision === "ALLOW" && payload.request.authType === "oauth" && approvalGrant
514
+ ? createOnboardingSession(payload.request, runtime, approvalGrant, prepared.verifiedRegistryEntry)
515
+ : undefined;
516
+ if (sessionState && onboardingSession) {
517
+ sessionState.onboardingSessions.set(onboardingSession.sessionId, onboardingSession);
518
+ }
519
+ writeJson(response, 200, {
520
+ verdict: prepared.verdict,
521
+ approvalVerdict: prepared.approvalVerdict,
522
+ verifiedRegistryEntry: prepared.verifiedRegistryEntry,
523
+ workflowBinding: prepared.workflowBinding,
524
+ onboardingSession
525
+ });
526
+ return;
527
+ }
528
+ if (request.url === "/v4/tool/callback/verify") {
529
+ const payload = await readJson(request);
530
+ const sessionState = findSessionState(sessions, payload.sessionId);
531
+ const approvalGrant = sessionState?.approvalGrants.get(payload.approvalGrantId);
532
+ const session = sessionState?.onboardingSessions.get(payload.request.sessionId);
533
+ const result = verifyToolCallbackV4(payload.request, sessionState?.session, session, approvalGrant, runtime);
534
+ if (sessionState && session) {
535
+ sessionState.onboardingSessions.set(payload.request.sessionId, {
536
+ ...session,
537
+ status: result.verdict.decision === "ALLOW" ? "used" : session.status
538
+ });
539
+ }
540
+ writeJson(response, 200, result);
541
+ return;
542
+ }
543
+ if (request.url === "/v4/artifact/ingest") {
544
+ const payload = await readJson(request);
545
+ const sessionState = findSessionState(sessions, payload.sessionId);
546
+ if (!sessionState) {
547
+ writeJson(response, 404, { error: "unknown_session" });
548
+ return;
549
+ }
550
+ const capture = {
551
+ ...payload.capture,
552
+ sessionId: payload.sessionId,
553
+ taskId: sessionState.session.taskId
554
+ };
555
+ const observation = await compileObservationInIsolation({
556
+ capture,
557
+ workflowHash: sessionState.session.workflowHash,
558
+ allowlistedEgress: runtime.parserAllowlistedEgress,
559
+ runtime: {
560
+ knowledgeBase: runtime.knowledgeBase
561
+ }
562
+ });
563
+ const failClosedArtifact = applyV4FailClosedMediation(observation.compiledObservation, observation.plannerInput, "artifact");
564
+ const legacyArtifact = buildLegacyObservationCapture(capture);
565
+ const artifactResult = legacyArtifact !== undefined ? brokerArtifact(legacyArtifact, runtime) : undefined;
566
+ sessionState.latestObservation = observation.compiledObservation;
567
+ const effectiveArtifactVerdict = failClosedArtifact.failClosed
568
+ ? failClosedArtifact.verdict
569
+ : artifactResult?.verdict;
570
+ const effectiveArtifact = failClosedArtifact.failClosed && artifactResult?.artifact
571
+ ? {
572
+ ...artifactResult.artifact,
573
+ toolActivationPolicy: "block",
574
+ approvalRequiredForFollowOn: true
575
+ }
576
+ : artifactResult?.artifact;
577
+ const plannerInput = shouldReduceAuthority(sessionState, observation.compiledObservation)
578
+ ? applyAuthorityReduction(sessionState, observation.compiledObservation, failClosedArtifact.plannerInput)
579
+ : failClosedArtifact.plannerInput;
580
+ writeJson(response, 200, {
581
+ compiledObservation: observation.compiledObservation,
582
+ plannerInput,
583
+ artifactVerdict: effectiveArtifactVerdict,
584
+ artifact: effectiveArtifact
585
+ });
586
+ return;
587
+ }
588
+ if (request.url === "/v4/memory/write") {
589
+ const payload = await readJson(request);
590
+ const sessionState = findSessionState(sessions, payload.sessionId);
591
+ const result = evaluateMemoryWriteV4(payload, sessionState?.session, runtime);
592
+ if (sessionState && result.record) {
593
+ sessionState.memoryRecords.set(result.record.recordId, result.record);
594
+ }
595
+ writeJson(response, 200, result);
596
+ return;
597
+ }
598
+ if (request.url === "/v4/memory/promote") {
599
+ const payload = await readJson(request);
600
+ const sessionState = findSessionState(sessions, payload.sessionId);
601
+ const record = sessionState?.memoryRecords.get(payload.recordId);
602
+ const approvalGrant = payload.approvalGrantId && sessionState
603
+ ? sessionState.approvalGrants.get(payload.approvalGrantId)
604
+ : undefined;
605
+ const result = promoteMemoryRecordV4(payload, sessionState?.session, record, approvalGrant);
606
+ if (sessionState && result.promotedRecord) {
607
+ if (record && result.promotedRecord.snapshotId) {
608
+ sessionState.memorySnapshots.set(result.promotedRecord.snapshotId, {
609
+ ...record,
610
+ tier: "trusted_durable",
611
+ summaryOnly: false,
612
+ snapshotId: result.promotedRecord.snapshotId,
613
+ rollbackPointId: result.promotedRecord.rollbackPointId
614
+ });
615
+ }
616
+ sessionState.memoryRecords.set(result.promotedRecord.recordId, result.promotedRecord);
617
+ }
618
+ writeJson(response, 200, result);
619
+ return;
620
+ }
621
+ if (request.url === "/v4/memory/rollback") {
622
+ const payload = await readJson(request);
623
+ const sessionState = findSessionState(sessions, payload.sessionId);
624
+ const record = sessionState?.memoryRecords.get(payload.recordId);
625
+ const snapshotRecord = sessionState?.memorySnapshots.get(payload.snapshotId);
626
+ const result = rollbackMemoryRecordV4(payload, sessionState?.session, record, snapshotRecord);
627
+ if (sessionState && result.restoredRecord) {
628
+ sessionState.memoryRecords.set(result.restoredRecord.recordId, result.restoredRecord);
629
+ }
630
+ writeJson(response, 200, {
631
+ ...result,
632
+ rollbackEvent: result.verdict.decision === "ALLOW"
633
+ ? {
634
+ recordId: payload.recordId,
635
+ snapshotId: payload.snapshotId,
636
+ appliedAt: new Date().toISOString()
637
+ }
638
+ : undefined
639
+ });
173
640
  return;
174
641
  }
175
642
  writeJson(response, 404, { error: "not_found" });