@axiom-lattice/gateway 4.1.5 → 4.1.7

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/index.js CHANGED
@@ -1512,7 +1512,7 @@ function extractFailureReason(task) {
1512
1512
  return extractText2(task.status.message.parts);
1513
1513
  }
1514
1514
  function observationEventKey(task, result, failureReason) {
1515
- const digest = (0, import_node_crypto17.createHash)("sha256").update(JSON.stringify({
1515
+ const digest = (0, import_node_crypto21.createHash)("sha256").update(JSON.stringify({
1516
1516
  state: task.status.state,
1517
1517
  timestamp: task.status.timestamp,
1518
1518
  message: task.status.message,
@@ -1522,7 +1522,7 @@ function observationEventKey(task, result, failureReason) {
1522
1522
  return `a2a-status-observation:${task.id}:${digest}`;
1523
1523
  }
1524
1524
  function snapshotEventKey(task) {
1525
- const digest = (0, import_node_crypto17.createHash)("sha256").update(JSON.stringify(task)).digest("hex");
1525
+ const digest = (0, import_node_crypto21.createHash)("sha256").update(JSON.stringify(task)).digest("hex");
1526
1526
  return `a2a-task-snapshot:${task.id}:${digest}`;
1527
1527
  }
1528
1528
  function isGovernedObservation(state) {
@@ -1534,11 +1534,11 @@ function isA2ATaskSnapshot(value) {
1534
1534
  function snapshotSequence(value) {
1535
1535
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
1536
1536
  }
1537
- var import_node_crypto17, import_node_util, TASK_STATUS_TO_A2A_STATE, TITLE_MAX_LENGTH, A2ATaskScopeConflictError, A2ATaskStoreAdapter;
1537
+ var import_node_crypto21, import_node_util, TASK_STATUS_TO_A2A_STATE, TITLE_MAX_LENGTH, A2ATaskScopeConflictError, A2ATaskStoreAdapter;
1538
1538
  var init_A2ATaskStoreAdapter = __esm({
1539
1539
  "src/services/a2a/A2ATaskStoreAdapter.ts"() {
1540
1540
  "use strict";
1541
- import_node_crypto17 = require("crypto");
1541
+ import_node_crypto21 = require("crypto");
1542
1542
  import_node_util = require("util");
1543
1543
  TASK_STATUS_TO_A2A_STATE = {
1544
1544
  pending: "submitted",
@@ -1999,7 +1999,7 @@ ${formatFileRefsSection(inputFiles)}` : text;
1999
1999
  const messageText = [
2000
2000
  `A2A governed task instruction: get and update the existing TaskItem ${ctx.taskId}; do not create a replacement task.`,
2001
2001
  "Before doing the requested work, initialize its canonical description through manage_task with ## Objective, ## Acceptance Criteria, and ## Belief State sections.",
2002
- "Keep lifecycle status truthful. Complete only through manage_task update with a nonblank result and structured beliefImpact; use interrupted, failed, or cancelled when appropriate.",
2002
+ "Keep lifecycle status truthful. Change status only through manage_task set_status; complete with a nonblank result and structured beliefImpact, and use interrupted, failed, or cancelled when appropriate. Use manage_task update for content fields only.",
2003
2003
  "",
2004
2004
  requestText
2005
2005
  ].join("\n");
@@ -3313,7 +3313,7 @@ var import_static = __toESM(require("@fastify/static"));
3313
3313
  var import_path = __toESM(require("path"));
3314
3314
 
3315
3315
  // src/routes/index.ts
3316
- var import_node_crypto16 = require("crypto");
3316
+ var import_node_crypto20 = require("crypto");
3317
3317
  var import_protocols24 = require("@axiom-lattice/protocols");
3318
3318
 
3319
3319
  // src/controllers/assistant.ts
@@ -8485,6 +8485,113 @@ var import_uuid3 = require("uuid");
8485
8485
  init_mime();
8486
8486
  init_workspace_file_service();
8487
8487
  init_path_containment();
8488
+
8489
+ // src/services/collaboration/RoomCoordinatorProvisioner.ts
8490
+ var ROOM_COORDINATOR_KEY = "room-coordinator";
8491
+ var RoomCoordinatorProvisioner = class {
8492
+ /** Creates a provisioner over the tenant-scoped assistant store operations. */
8493
+ constructor(deps) {
8494
+ this.deps = deps;
8495
+ }
8496
+ /**
8497
+ * Ensures the Room Coordinator assistant exists, reusing an existing record
8498
+ * and reconciling concurrent create races by rereading the winner.
8499
+ *
8500
+ * @param tenantId Tenant to provision the coordinator for.
8501
+ * @returns The persisted Room Coordinator assistant.
8502
+ */
8503
+ async ensure(tenantId) {
8504
+ const existing = await this.deps.getAssistantById(tenantId, ROOM_COORDINATOR_KEY);
8505
+ if (existing) return existing;
8506
+ try {
8507
+ return await this.deps.createAssistant(tenantId, ROOM_COORDINATOR_KEY, {
8508
+ name: "Room Coordinator",
8509
+ description: "Coordinates project room conversations and organizes room work.",
8510
+ graphDefinition: {}
8511
+ });
8512
+ } catch (error) {
8513
+ const winner = await this.deps.getAssistantById(tenantId, ROOM_COORDINATOR_KEY);
8514
+ if (winner) return winner;
8515
+ throw error;
8516
+ }
8517
+ }
8518
+ };
8519
+
8520
+ // src/services/collaboration/WorkspacePublicRoomService.ts
8521
+ var import_node_crypto = require("crypto");
8522
+ var import_node_crypto2 = require("crypto");
8523
+ var WORKSPACE_PUBLIC_ROOM_PROJECT_PREFIX = "workspace_public_";
8524
+ var PUBLIC_ROOM_PROJECT_NAME = "General";
8525
+ var PUBLIC_ROOM_PROJECT_DESCRIPTION = "Public room for everyone in the workspace";
8526
+ function workspacePublicRoomProjectId(tenantId, workspaceId) {
8527
+ const hash = (0, import_node_crypto.createHash)("sha256").update([tenantId, workspaceId].join(":")).digest("hex");
8528
+ return `${WORKSPACE_PUBLIC_ROOM_PROJECT_PREFIX}${hash}`;
8529
+ }
8530
+ var WorkspacePublicRoomService = class {
8531
+ /** Creates the service from explicitly injected stores and provisioners. */
8532
+ constructor(deps) {
8533
+ this.deps = deps;
8534
+ this.idFactory = deps.idFactory ?? import_node_crypto2.randomUUID;
8535
+ }
8536
+ /**
8537
+ * Returns the workspace's public room project and room, creating them on first call.
8538
+ *
8539
+ * @param tenantId Tenant owning the workspace.
8540
+ * @param workspaceId Workspace hosting the public room.
8541
+ * @returns The ensured project and room.
8542
+ */
8543
+ async ensure(tenantId, workspaceId) {
8544
+ const projectId = workspacePublicRoomProjectId(tenantId, workspaceId);
8545
+ const project = await this.ensureProject(tenantId, workspaceId, projectId);
8546
+ const room = await this.deps.ensureMainRoom({
8547
+ id: this.idFactory(),
8548
+ tenantId,
8549
+ workspaceId,
8550
+ projectId,
8551
+ name: project.name
8552
+ });
8553
+ await this.ensureCoordinatorBot(tenantId, workspaceId, projectId, room.id);
8554
+ return { project, room };
8555
+ }
8556
+ async ensureProject(tenantId, workspaceId, projectId) {
8557
+ const existing = await this.deps.getProjectById(tenantId, projectId);
8558
+ if (existing && existing.kind === "public") return existing;
8559
+ try {
8560
+ return await this.deps.createProject(tenantId, workspaceId, projectId, {
8561
+ name: PUBLIC_ROOM_PROJECT_NAME,
8562
+ description: PUBLIC_ROOM_PROJECT_DESCRIPTION,
8563
+ kind: "public"
8564
+ });
8565
+ } catch (error) {
8566
+ const winner = await this.deps.getProjectById(tenantId, projectId);
8567
+ if (winner && winner.kind === "public") return winner;
8568
+ throw Object.assign(new Error("Workspace public room project could not be ensured"), { cause: error });
8569
+ }
8570
+ }
8571
+ async ensureCoordinatorBot(tenantId, workspaceId, projectId, roomId) {
8572
+ const coordinator = await this.deps.ensureCoordinator(tenantId);
8573
+ const existing = await this.deps.findBotByAssistant(tenantId, projectId, coordinator.id);
8574
+ if (existing && existing.status !== "removed") return;
8575
+ const saved = await this.deps.saveBot({
8576
+ id: this.idFactory(),
8577
+ tenantId,
8578
+ workspaceId,
8579
+ projectId,
8580
+ roomId,
8581
+ assistantId: coordinator.id,
8582
+ role: "coordinator",
8583
+ title: coordinator.name,
8584
+ mentionName: ROOM_COORDINATOR_KEY,
8585
+ status: "active",
8586
+ roomThreadId: this.idFactory()
8587
+ });
8588
+ if (saved.kind === "coordinator_conflict" || saved.kind === "mention_conflict") {
8589
+ throw new Error(`Workspace public room coordinator bot could not be ensured: ${saved.kind}`);
8590
+ }
8591
+ }
8592
+ };
8593
+
8594
+ // src/controllers/workspace.ts
8488
8595
  var MAX_ZIP_ENTRIES = 1e4;
8489
8596
  var MAX_ZIP_FILE_SIZE = 100 * 1024 * 1024;
8490
8597
  var MAX_ZIP_TOTAL_SIZE = 512 * 1024 * 1024;
@@ -8619,6 +8726,21 @@ var WorkspaceController = class {
8619
8726
  this.projectCollaboration = projectCollaboration;
8620
8727
  this.workspaceStore = (0, import_core24.getStoreLattice)("default", "workspace").store;
8621
8728
  this.projectStore = (0, import_core24.getStoreLattice)("default", "project").store;
8729
+ const roomStore = (0, import_core24.getStoreLattice)("default", "projectRoom").store;
8730
+ const botMembershipStore = (0, import_core24.getStoreLattice)("default", "projectBotMembership").store;
8731
+ const assistantStore = (0, import_core24.getStoreLattice)("default", "assistant").store;
8732
+ const coordinatorProvisioner = new RoomCoordinatorProvisioner({
8733
+ getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
8734
+ createAssistant: assistantStore.createAssistant.bind(assistantStore)
8735
+ });
8736
+ this.publicRoom = new WorkspacePublicRoomService({
8737
+ getProjectById: this.projectStore.getProjectById.bind(this.projectStore),
8738
+ createProject: this.projectStore.createProject.bind(this.projectStore),
8739
+ ensureMainRoom: roomStore.ensureMainRoom.bind(roomStore),
8740
+ findBotByAssistant: botMembershipStore.findByAssistant.bind(botMembershipStore),
8741
+ saveBot: botMembershipStore.save.bind(botMembershipStore),
8742
+ ensureCoordinator: (tenantId) => coordinatorProvisioner.ensure(tenantId)
8743
+ });
8622
8744
  }
8623
8745
  getTenantId(request) {
8624
8746
  const userTenantId = request.user?.tenantId;
@@ -8692,6 +8814,7 @@ var WorkspaceController = class {
8692
8814
  const { workspaceId } = request.params;
8693
8815
  const kindFilter = request.query.kind;
8694
8816
  await this.ensureTrainingProject(tenantId, workspaceId);
8817
+ await this.publicRoom.ensure(tenantId, workspaceId);
8695
8818
  const projects = kindFilter ? await this.projectStore.getProjectsByWorkspace(tenantId, workspaceId, {
8696
8819
  kind: kindFilter
8697
8820
  }) : await this.projectStore.getProjectsByWorkspace(tenantId, workspaceId);
@@ -8735,6 +8858,12 @@ var WorkspaceController = class {
8735
8858
  message: CAPABILITY_BUNDLE_CONFIG_MESSAGE
8736
8859
  });
8737
8860
  }
8861
+ if (data.kind === "public") {
8862
+ return reply.status(409).send({
8863
+ success: false,
8864
+ error: "The public room project is managed automatically"
8865
+ });
8866
+ }
8738
8867
  if (user?.id && this.projectCollaboration) {
8739
8868
  try {
8740
8869
  const authorized = await this.projectCollaboration.authorizeCreator({ tenantId, userId: user.id });
@@ -8810,6 +8939,21 @@ var WorkspaceController = class {
8810
8939
  message: CAPABILITY_BUNDLE_CONFIG_MESSAGE
8811
8940
  });
8812
8941
  }
8942
+ if (updates.kind !== void 0) {
8943
+ const existingProject = await this.projectStore.getProjectById(tenantId, projectId);
8944
+ if (existingProject && updates.kind === "public") {
8945
+ return reply.status(409).send({
8946
+ success: false,
8947
+ error: "kind public is reserved for the workspace public room"
8948
+ });
8949
+ }
8950
+ if (existingProject?.kind === "public") {
8951
+ return reply.status(409).send({
8952
+ success: false,
8953
+ error: "The public room project classification cannot be changed"
8954
+ });
8955
+ }
8956
+ }
8813
8957
  const project = await this.projectStore.updateProject(
8814
8958
  tenantId,
8815
8959
  projectId,
@@ -8840,6 +8984,12 @@ var WorkspaceController = class {
8840
8984
  });
8841
8985
  }
8842
8986
  }
8987
+ if (project.kind === "public") {
8988
+ return reply.status(409).send({
8989
+ success: false,
8990
+ error: "The public room project cannot be deleted"
8991
+ });
8992
+ }
8843
8993
  const deleted = await this.projectStore.deleteProject(tenantId, projectId);
8844
8994
  if (!deleted) {
8845
8995
  return reply.status(404).send({ success: false, error: "Project not found" });
@@ -9538,7 +9688,195 @@ function registerWorkspaceRoutes(app2, projectCollaboration) {
9538
9688
  "/api/filesystem/browse",
9539
9689
  controller.browseFilesystem.bind(controller)
9540
9690
  );
9691
+ return controller;
9692
+ }
9693
+
9694
+ // src/services/collaboration/ProjectAccessService.ts
9695
+ var import_node_crypto3 = require("crypto");
9696
+ var ProjectCollaborationError = class extends Error {
9697
+ /** Creates a project collaboration error. */
9698
+ constructor(code, statusCode, message) {
9699
+ super(message);
9700
+ this.code = code;
9701
+ this.statusCode = statusCode;
9702
+ this.name = "ProjectCollaborationError";
9703
+ }
9704
+ };
9705
+ var allowedRoles = {
9706
+ read: /* @__PURE__ */ new Set(["owner", "admin", "member", "viewer"]),
9707
+ send: /* @__PURE__ */ new Set(["owner", "admin", "member"]),
9708
+ manage: /* @__PURE__ */ new Set(["owner", "admin"])
9709
+ };
9710
+ var membershipConflictNames = /* @__PURE__ */ new Set([
9711
+ "DuplicateProjectMembershipError",
9712
+ "ProjectMembershipIdConflictError"
9713
+ ]);
9714
+ function isProjectMembershipConflict(error) {
9715
+ return error instanceof Error && membershipConflictNames.has(error.name);
9541
9716
  }
9717
+ var ProjectAccessService = class {
9718
+ /** Creates a project access service from explicitly injected stores. */
9719
+ constructor(deps) {
9720
+ this.deps = deps;
9721
+ this.idFactory = deps.idFactory ?? import_node_crypto3.randomUUID;
9722
+ }
9723
+ /** Returns the project and active membership when the user has the requested capability. */
9724
+ async requireProjectAccess(input) {
9725
+ const project = await this.deps.projectStore.getProjectById(input.tenantId, input.projectId);
9726
+ if (!project) {
9727
+ throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
9728
+ }
9729
+ if (project.kind === "public") {
9730
+ return this.requirePublicProjectAccess(project, input);
9731
+ }
9732
+ const membership = await this.deps.membershipStore.findByUser(
9733
+ input.tenantId,
9734
+ input.projectId,
9735
+ input.userId
9736
+ );
9737
+ if (membership?.status === "active") {
9738
+ this.requireCapability(membership, input.capability);
9739
+ return { project, membership };
9740
+ }
9741
+ const memberships = await this.deps.membershipStore.list(input.tenantId, input.projectId);
9742
+ if (memberships.length !== 0) this.accessDenied();
9743
+ if (project.kind === "personal") this.accessDenied();
9744
+ const tenantLink = await this.deps.userTenantLinkStore.getLink(input.userId, input.tenantId);
9745
+ if (tenantLink?.role !== "owner" && tenantLink?.role !== "admin") this.accessDenied();
9746
+ let claim;
9747
+ try {
9748
+ claim = await this.deps.membershipStore.createInitialOwner({
9749
+ id: this.idFactory(),
9750
+ tenantId: input.tenantId,
9751
+ projectId: input.projectId,
9752
+ userId: input.userId
9753
+ });
9754
+ } catch (error) {
9755
+ if (isProjectMembershipConflict(error)) {
9756
+ throw new ProjectCollaborationError(
9757
+ "PROJECT_MEMBERSHIP_CONFLICT",
9758
+ 409,
9759
+ "Project membership could not be initialized"
9760
+ );
9761
+ }
9762
+ throw error;
9763
+ }
9764
+ if (claim.kind === "created" || claim.kind === "existing") {
9765
+ return { project, membership: claim.membership };
9766
+ }
9767
+ const racedMembership = await this.deps.membershipStore.findByUser(
9768
+ input.tenantId,
9769
+ input.projectId,
9770
+ input.userId
9771
+ );
9772
+ if (racedMembership?.status !== "active") this.accessDenied();
9773
+ this.requireCapability(racedMembership, input.capability);
9774
+ return { project, membership: racedMembership };
9775
+ }
9776
+ /** Authorizes an already-existing membership, or any tenant member for public rooms; never mutates persistence. */
9777
+ async requireExistingProjectAccess(input) {
9778
+ const project = await this.deps.projectStore.getProjectById(input.tenantId, input.projectId);
9779
+ if (!project) throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
9780
+ if (project.kind === "public") {
9781
+ return this.requireExistingPublicProjectAccess(project, input);
9782
+ }
9783
+ const membership = await this.deps.membershipStore.findByUser(
9784
+ input.tenantId,
9785
+ input.projectId,
9786
+ input.userId
9787
+ );
9788
+ if (!membership || membership.tenantId !== input.tenantId || membership.projectId !== input.projectId || membership.userId !== input.userId || membership.status !== "active") this.accessDenied();
9789
+ this.requireCapability(membership, input.capability);
9790
+ return { project, membership };
9791
+ }
9792
+ /** Authorizes roster visibility: read for public rooms, manage elsewhere. */
9793
+ async requireRosterAccess(input) {
9794
+ const project = await this.deps.projectStore.getProjectById(input.tenantId, input.projectId);
9795
+ if (!project) throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
9796
+ return this.requireProjectAccess({
9797
+ ...input,
9798
+ capability: project.kind === "public" ? "read" : "manage"
9799
+ });
9800
+ }
9801
+ async requirePublicProjectAccess(project, input) {
9802
+ const membership = await this.deps.membershipStore.findByUser(
9803
+ input.tenantId,
9804
+ input.projectId,
9805
+ input.userId
9806
+ );
9807
+ if (membership?.status === "active") {
9808
+ this.requireCapability(membership, input.capability);
9809
+ return { project, membership };
9810
+ }
9811
+ const materialized = await this.materializePublicMembership(input);
9812
+ this.requireCapability(materialized, input.capability);
9813
+ return { project, membership: materialized };
9814
+ }
9815
+ async requireExistingPublicProjectAccess(project, input) {
9816
+ const membership = await this.deps.membershipStore.findByUser(
9817
+ input.tenantId,
9818
+ input.projectId,
9819
+ input.userId
9820
+ );
9821
+ if (membership?.status === "active") {
9822
+ this.requireCapability(membership, input.capability);
9823
+ return { project, membership };
9824
+ }
9825
+ if (membership) this.accessDenied();
9826
+ const tenantLink = await this.deps.userTenantLinkStore.getLink(input.userId, input.tenantId);
9827
+ if (!tenantLink) this.accessDenied();
9828
+ const role = tenantLink.role === "owner" || tenantLink.role === "admin" ? "admin" : "member";
9829
+ const synthetic = {
9830
+ id: `public:${input.userId}`,
9831
+ tenantId: input.tenantId,
9832
+ projectId: input.projectId,
9833
+ userId: input.userId,
9834
+ role,
9835
+ status: "active",
9836
+ joinedAt: project.createdAt,
9837
+ updatedAt: project.updatedAt
9838
+ };
9839
+ this.requireCapability(synthetic, input.capability);
9840
+ return { project, membership: synthetic };
9841
+ }
9842
+ async materializePublicMembership(input) {
9843
+ const tenantLink = await this.deps.userTenantLinkStore.getLink(input.userId, input.tenantId);
9844
+ if (!tenantLink) this.accessDenied();
9845
+ const existing = await this.deps.membershipStore.findByUser(
9846
+ input.tenantId,
9847
+ input.projectId,
9848
+ input.userId
9849
+ );
9850
+ if (existing) this.accessDenied();
9851
+ const role = tenantLink.role === "owner" || tenantLink.role === "admin" ? "admin" : "member";
9852
+ try {
9853
+ return await this.deps.membershipStore.create({
9854
+ id: this.idFactory(),
9855
+ tenantId: input.tenantId,
9856
+ projectId: input.projectId,
9857
+ userId: input.userId,
9858
+ role,
9859
+ status: "active"
9860
+ });
9861
+ } catch (error) {
9862
+ if (isProjectMembershipConflict(error)) {
9863
+ const raced = await this.deps.membershipStore.findByUser(
9864
+ input.tenantId,
9865
+ input.projectId,
9866
+ input.userId
9867
+ );
9868
+ if (raced?.status === "active") return raced;
9869
+ }
9870
+ throw error;
9871
+ }
9872
+ }
9873
+ requireCapability(membership, capability2) {
9874
+ if (!allowedRoles[capability2].has(membership.role)) this.accessDenied();
9875
+ }
9876
+ accessDenied() {
9877
+ throw new ProjectCollaborationError("PROJECT_ACCESS_DENIED", 403, "Project access denied");
9878
+ }
9879
+ };
9542
9880
 
9543
9881
  // src/controllers/database-configs.ts
9544
9882
  var import_core27 = require("@axiom-lattice/core");
@@ -11595,7 +11933,7 @@ function registerTenantRoutes(app2) {
11595
11933
  // src/controllers/auth.ts
11596
11934
  var import_core35 = require("@axiom-lattice/core");
11597
11935
  var import_uuid8 = require("uuid");
11598
- var import_node_crypto = require("crypto");
11936
+ var import_node_crypto4 = require("crypto");
11599
11937
  var defaultAuthConfig = {
11600
11938
  autoApproveUsers: true,
11601
11939
  allowTenantRegistration: true,
@@ -11950,7 +12288,7 @@ var AuthController = class {
11950
12288
  function resolveConsoleTokenSecret(_authRequired) {
11951
12289
  const configured = process.env.AUTH_TOKEN_SECRET || process.env.JWT_SECRET;
11952
12290
  if (configured) return configured;
11953
- devSessionSecret ?? (devSessionSecret = (0, import_node_crypto.randomBytes)(32).toString("hex"));
12291
+ devSessionSecret ?? (devSessionSecret = (0, import_node_crypto4.randomBytes)(32).toString("hex"));
11954
12292
  console.warn(
11955
12293
  "[auth] AUTH_TOKEN_SECRET/JWT_SECRET is not set; using an in-process generated signing secret (sessions are invalidated on restart). Set AUTH_TOKEN_SECRET for production."
11956
12294
  );
@@ -11959,7 +12297,7 @@ function resolveConsoleTokenSecret(_authRequired) {
11959
12297
  var devSessionSecret;
11960
12298
  function createConsoleSessionToken(payload, secret) {
11961
12299
  const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url");
11962
- const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(encodedPayload).digest("base64url");
12300
+ const signature = (0, import_node_crypto4.createHmac)("sha256", secret).update(encodedPayload).digest("base64url");
11963
12301
  return `${encodedPayload}.${signature}`;
11964
12302
  }
11965
12303
  function isConsoleSessionPayload(value) {
@@ -11986,8 +12324,8 @@ function verifySignedConsoleSession(authHeader, secret) {
11986
12324
  try {
11987
12325
  const actualSignature = Buffer.from(encodedSignature, "base64url");
11988
12326
  if (actualSignature.toString("base64url") !== encodedSignature) return void 0;
11989
- const expectedSignature = (0, import_node_crypto.createHmac)("sha256", secret).update(encodedPayload).digest();
11990
- if (actualSignature.length !== expectedSignature.length || !(0, import_node_crypto.timingSafeEqual)(actualSignature, expectedSignature)) {
12327
+ const expectedSignature = (0, import_node_crypto4.createHmac)("sha256", secret).update(encodedPayload).digest();
12328
+ if (actualSignature.length !== expectedSignature.length || !(0, import_node_crypto4.timingSafeEqual)(actualSignature, expectedSignature)) {
11991
12329
  return void 0;
11992
12330
  }
11993
12331
  const payload = JSON.parse(
@@ -13568,7 +13906,7 @@ async function getLocalA2ATemplates(_request, reply) {
13568
13906
  }
13569
13907
 
13570
13908
  // src/routes/a2a-test-connection.ts
13571
- var import_node_crypto2 = require("crypto");
13909
+ var import_node_crypto5 = require("crypto");
13572
13910
  function describeTestConnectionError(error, agentCardUrl) {
13573
13911
  if (!(error instanceof Error)) {
13574
13912
  return `Failed to fetch agent card from ${agentCardUrl}: ${String(error)}`;
@@ -13608,7 +13946,7 @@ async function ensureLocalRuntimeForConnectionTest(localRuntime, agentCardUrl) {
13608
13946
  return { error: portValidation.error };
13609
13947
  }
13610
13948
  const state = await localA2AService.startProvider(
13611
- `test-${(0, import_node_crypto2.randomUUID)()}`,
13949
+ `test-${(0, import_node_crypto5.randomUUID)()}`,
13612
13950
  testRuntime
13613
13951
  );
13614
13952
  if (state.status === "running") return { runtimeId: state.runtimeId };
@@ -13685,106 +14023,6 @@ var import_core51 = require("@axiom-lattice/core");
13685
14023
  var import_protocols10 = require("@axiom-lattice/protocols");
13686
14024
  var import_core42 = require("@axiom-lattice/core");
13687
14025
 
13688
- // src/services/collaboration/ProjectAccessService.ts
13689
- var import_node_crypto3 = require("crypto");
13690
- var ProjectCollaborationError = class extends Error {
13691
- /** Creates a project collaboration error. */
13692
- constructor(code, statusCode, message) {
13693
- super(message);
13694
- this.code = code;
13695
- this.statusCode = statusCode;
13696
- this.name = "ProjectCollaborationError";
13697
- }
13698
- };
13699
- var allowedRoles = {
13700
- read: /* @__PURE__ */ new Set(["owner", "admin", "member", "viewer"]),
13701
- send: /* @__PURE__ */ new Set(["owner", "admin", "member"]),
13702
- manage: /* @__PURE__ */ new Set(["owner", "admin"])
13703
- };
13704
- var membershipConflictNames = /* @__PURE__ */ new Set([
13705
- "DuplicateProjectMembershipError",
13706
- "ProjectMembershipIdConflictError"
13707
- ]);
13708
- function isProjectMembershipConflict(error) {
13709
- return error instanceof Error && membershipConflictNames.has(error.name);
13710
- }
13711
- var ProjectAccessService = class {
13712
- /** Creates a project access service from explicitly injected stores. */
13713
- constructor(deps) {
13714
- this.deps = deps;
13715
- this.idFactory = deps.idFactory ?? import_node_crypto3.randomUUID;
13716
- }
13717
- /** Returns the project and active membership when the user has the requested capability. */
13718
- async requireProjectAccess(input) {
13719
- const project = await this.deps.projectStore.getProjectById(input.tenantId, input.projectId);
13720
- if (!project) {
13721
- throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
13722
- }
13723
- const membership = await this.deps.membershipStore.findByUser(
13724
- input.tenantId,
13725
- input.projectId,
13726
- input.userId
13727
- );
13728
- if (membership?.status === "active") {
13729
- this.requireCapability(membership, input.capability);
13730
- return { project, membership };
13731
- }
13732
- const memberships = await this.deps.membershipStore.list(input.tenantId, input.projectId);
13733
- if (memberships.length !== 0) this.accessDenied();
13734
- if (project.kind === "personal") this.accessDenied();
13735
- const tenantLink = await this.deps.userTenantLinkStore.getLink(input.userId, input.tenantId);
13736
- if (tenantLink?.role !== "owner" && tenantLink?.role !== "admin") this.accessDenied();
13737
- let claim;
13738
- try {
13739
- claim = await this.deps.membershipStore.createInitialOwner({
13740
- id: this.idFactory(),
13741
- tenantId: input.tenantId,
13742
- projectId: input.projectId,
13743
- userId: input.userId
13744
- });
13745
- } catch (error) {
13746
- if (isProjectMembershipConflict(error)) {
13747
- throw new ProjectCollaborationError(
13748
- "PROJECT_MEMBERSHIP_CONFLICT",
13749
- 409,
13750
- "Project membership could not be initialized"
13751
- );
13752
- }
13753
- throw error;
13754
- }
13755
- if (claim.kind === "created" || claim.kind === "existing") {
13756
- return { project, membership: claim.membership };
13757
- }
13758
- const racedMembership = await this.deps.membershipStore.findByUser(
13759
- input.tenantId,
13760
- input.projectId,
13761
- input.userId
13762
- );
13763
- if (racedMembership?.status !== "active") this.accessDenied();
13764
- this.requireCapability(racedMembership, input.capability);
13765
- return { project, membership: racedMembership };
13766
- }
13767
- /** Authorizes only an already-existing active membership and never mutates persistence. */
13768
- async requireExistingProjectAccess(input) {
13769
- const project = await this.deps.projectStore.getProjectById(input.tenantId, input.projectId);
13770
- if (!project) throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
13771
- const membership = await this.deps.membershipStore.findByUser(
13772
- input.tenantId,
13773
- input.projectId,
13774
- input.userId
13775
- );
13776
- if (!membership || membership.tenantId !== input.tenantId || membership.projectId !== input.projectId || membership.userId !== input.userId || membership.status !== "active") this.accessDenied();
13777
- this.requireCapability(membership, input.capability);
13778
- return { project, membership };
13779
- }
13780
- requireCapability(membership, capability2) {
13781
- if (!allowedRoles[capability2].has(membership.role)) this.accessDenied();
13782
- }
13783
- accessDenied() {
13784
- throw new ProjectCollaborationError("PROJECT_ACCESS_DENIED", 403, "Project access denied");
13785
- }
13786
- };
13787
-
13788
14026
  // src/services/collaboration/ProjectTaskThreadService.ts
13789
14027
  var import_core40 = require("@axiom-lattice/core");
13790
14028
  var import_protocols9 = require("@axiom-lattice/protocols");
@@ -14316,9 +14554,6 @@ function governedUpdate(task, body) {
14316
14554
  if (has("ownerType") && body.ownerType !== task.ownerType || has("ownerId") && body.ownerId !== task.ownerId) {
14317
14555
  return "TASK_OWNERSHIP_IMMUTABLE";
14318
14556
  }
14319
- if (task.ownerType !== "agent") return void 0;
14320
- if (has("parentId") && body.parentId !== task.parentId || has("dependencies") || has("requireReview") || has("result") || has("failureReason") || has("status")) return "TASK_LIFECYCLE_TRANSITION_REQUIRED";
14321
- if (has("context") || has("description")) return "COMPLETION_CONTEXT_MUTATION";
14322
14557
  return void 0;
14323
14558
  }
14324
14559
  function lifecycleStatus(code) {
@@ -14759,7 +14994,8 @@ var AgentWebAppService = class {
14759
14994
  integration: current.integration,
14760
14995
  scope: { ...current.scope, ...patch.scope },
14761
14996
  features: { ...current.features, ...patch.features },
14762
- appearance: { ...current.appearance, ...patch.appearance }
14997
+ appearance: { ...current.appearance, ...patch.appearance },
14998
+ identity: patch.identity ?? current.identity
14763
14999
  };
14764
15000
  await this.validateAssistant(context, current.assistantId);
14765
15001
  const validated = await this.validateConfig(context.tenantId, effective);
@@ -14768,7 +15004,8 @@ var AgentWebAppService = class {
14768
15004
  ...Object.prototype.hasOwnProperty.call(patch, "description") ? { description: validated.description } : {},
14769
15005
  ...patch.scope ? { scope: validated.scope } : {},
14770
15006
  ...patch.features ? { features: validated.features } : {},
14771
- ...patch.appearance ? { appearance: validated.appearance } : {}
15007
+ ...patch.appearance ? { appearance: validated.appearance } : {},
15008
+ ...patch.identity !== void 0 ? { identity: patch.identity } : {}
14772
15009
  };
14773
15010
  return this.writeSnapshot(context, current, update);
14774
15011
  }
@@ -14881,13 +15118,28 @@ var agentWebAppFeaturesSchema = import_zod4.z.object({
14881
15118
  threadManagement: import_zod4.z.boolean(),
14882
15119
  attachments: import_zod4.z.boolean(),
14883
15120
  hitl: import_zod4.z.boolean(),
14884
- genUI: import_zod4.z.boolean()
15121
+ genUI: import_zod4.z.boolean(),
15122
+ files: import_zod4.z.boolean().optional()
14885
15123
  }).strict();
14886
15124
  var agentWebAppAppearanceSchema = import_zod4.z.object({
14887
15125
  title: import_zod4.z.string().max(120).optional(),
14888
15126
  welcomeMessage: import_zod4.z.string().max(1e3).optional(),
14889
15127
  primaryColor: import_zod4.z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional()
14890
15128
  }).strict();
15129
+ var agentWebAppIssuerSchema = import_zod4.z.object({
15130
+ iss: import_zod4.z.string().min(1).max(255),
15131
+ aud: import_zod4.z.string().min(1).max(255),
15132
+ alg: import_zod4.z.enum(["HS256", "RS256"]),
15133
+ secret: import_zod4.z.string().min(1).max(512).optional(),
15134
+ jwksUrl: import_zod4.z.string().url().max(2048).optional()
15135
+ }).strict().refine(
15136
+ (value) => value.alg === "HS256" ? value.secret !== void 0 : value.jwksUrl !== void 0,
15137
+ { message: "HS256 requires secret; RS256 requires jwksUrl" }
15138
+ );
15139
+ var agentWebAppIdentitySchema = import_zod4.z.object({
15140
+ policy: import_zod4.z.enum(["unverified", "verified", "both"]),
15141
+ issuers: import_zod4.z.array(agentWebAppIssuerSchema).min(1).max(10)
15142
+ }).strict();
14891
15143
  var createAgentWebAppSchema = import_zod4.z.object({
14892
15144
  assistantId: externalId,
14893
15145
  name: import_zod4.z.string().min(1).max(120),
@@ -14895,14 +15147,16 @@ var createAgentWebAppSchema = import_zod4.z.object({
14895
15147
  integration: import_zod4.z.object({ type: import_zod4.z.literal("react_sdk") }).strict(),
14896
15148
  scope: agentWebAppScopeSchema,
14897
15149
  features: agentWebAppFeaturesSchema,
14898
- appearance: agentWebAppAppearanceSchema
15150
+ appearance: agentWebAppAppearanceSchema,
15151
+ identity: agentWebAppIdentitySchema.optional()
14899
15152
  }).strict();
14900
15153
  var updateAgentWebAppSchema = import_zod4.z.object({
14901
15154
  name: import_zod4.z.string().min(1).max(120).optional(),
14902
15155
  description: import_zod4.z.string().max(1e3).optional(),
14903
15156
  scope: agentWebAppScopeSchema.partial().strict().optional(),
14904
15157
  features: agentWebAppFeaturesSchema.partial().strict().optional(),
14905
- appearance: agentWebAppAppearanceSchema.partial().strict().optional()
15158
+ appearance: agentWebAppAppearanceSchema.partial().strict().optional(),
15159
+ identity: agentWebAppIdentitySchema.optional()
14906
15160
  }).strict().refine((value) => Object.keys(value).length > 0, "Patch must not be empty");
14907
15161
  var listAgentWebAppsQuerySchema = import_zod4.z.object({
14908
15162
  assistantId: externalId.optional()
@@ -14989,10 +15243,11 @@ function invalidRequest(reply, statusCode) {
14989
15243
  }
14990
15244
 
14991
15245
  // src/routes/agent-web-app-runtime.ts
15246
+ var import_node_crypto8 = require("crypto");
14992
15247
  var import_zod6 = require("zod");
14993
15248
 
14994
15249
  // src/services/agent-web-app/AgentWebAppRuntimeService.ts
14995
- var import_node_crypto4 = require("crypto");
15250
+ var import_node_crypto6 = require("crypto");
14996
15251
  var import_protocols11 = require("@axiom-lattice/protocols");
14997
15252
  init_task_file_refs();
14998
15253
  function isRecord2(value) {
@@ -15010,7 +15265,16 @@ var AgentWebAppRuntimeService = class {
15010
15265
  constructor(deps) {
15011
15266
  this.deps = deps;
15012
15267
  }
15013
- async bootstrap(webAppId, userId, selection) {
15268
+ /** Identity policy configuration for the web app, or null when the app is missing/unconfigured. */
15269
+ async getIdentityConfig(webAppId) {
15270
+ try {
15271
+ const runtime = await this.resolveRuntime(webAppId, {});
15272
+ return runtime.webApp.identity ?? null;
15273
+ } catch {
15274
+ return null;
15275
+ }
15276
+ }
15277
+ async bootstrap(webAppId, userId, selection, identity) {
15014
15278
  const runtime = await this.resolveRuntime(webAppId, selection);
15015
15279
  const assistant = await this.deps.assistantResolver.resolve(
15016
15280
  runtime.webApp.tenantId,
@@ -15043,7 +15307,7 @@ var AgentWebAppRuntimeService = class {
15043
15307
  defaultModelKey: runtime.webApp.scope.defaultModelKey,
15044
15308
  features: runtime.webApp.features,
15045
15309
  appearance: runtime.webApp.appearance,
15046
- identityAssurance: "unverified"
15310
+ identityAssurance: identity?.assurance ?? "unverified"
15047
15311
  },
15048
15312
  projects,
15049
15313
  models
@@ -15064,6 +15328,14 @@ var AgentWebAppRuntimeService = class {
15064
15328
  this.requireFeature(runtime.webApp.features.threadManagement);
15065
15329
  return this.createOwnedThread(runtime, userId, input.label);
15066
15330
  }
15331
+ /** Fetch one owned thread. */
15332
+ async getThread(webAppId, userId, threadId, selection) {
15333
+ const runtime = await this.resolveRuntime(webAppId, selection);
15334
+ await this.requireOwnedThread(runtime, userId, threadId);
15335
+ const thread = await this.deps.threadStore.getThreadById(runtime.webApp.tenantId, threadId);
15336
+ if (!thread) this.threadNotFound();
15337
+ return toRuntimeThread(thread);
15338
+ }
15067
15339
  /** Update a thread label; omitted or blank labels preserve the existing label. */
15068
15340
  async updateThread(webAppId, userId, threadId, input) {
15069
15341
  const runtime = await this.resolveRuntime(webAppId, input);
@@ -15098,6 +15370,60 @@ var AgentWebAppRuntimeService = class {
15098
15370
  return [{ id: message.id, role: message.role, ...content && (typeof content === "string" || content.length > 0) ? { content } : {} }];
15099
15371
  });
15100
15372
  }
15373
+ async getState(webAppId, userId, threadId, selection) {
15374
+ const runtime = await this.resolveRuntime(webAppId, selection);
15375
+ await this.requireOwnedThread(runtime, userId, threadId);
15376
+ const agent = this.resolveAgent(runtime, threadId);
15377
+ const state = await agent.getCurrentState();
15378
+ const pendingMessages = await agent.getPendingMessages();
15379
+ const normalized = pendingMessages.map((message) => ({
15380
+ ...message,
15381
+ content: typeof message.content === "object" && message.content !== null ? "message" in message.content ? message.content.message : JSON.stringify(message.content) : String(message.content ?? "")
15382
+ }));
15383
+ return { ...state && typeof state === "object" ? state : {}, pendingMessages: normalized };
15384
+ }
15385
+ /** Raw stored messages (Console memory contract, tool/interrupt preserved). */
15386
+ async getConsoleMessages(webAppId, userId, threadId, selection) {
15387
+ const runtime = await this.resolveRuntime(webAppId, selection);
15388
+ await this.requireOwnedThread(runtime, userId, threadId);
15389
+ return this.deps.messageReader.getCurrentMessages({
15390
+ tenantId: runtime.webApp.tenantId,
15391
+ assistantId: runtime.webApp.assistantId,
15392
+ threadId,
15393
+ workspaceId: runtime.project.workspaceId,
15394
+ projectId: runtime.project.id
15395
+ });
15396
+ }
15397
+ /** Background enqueue only (ADR-026): returns the message id; delivery happens over resume_stream. */
15398
+ async enqueueMessage(webAppId, userId, threadId, content, selection) {
15399
+ const runtime = await this.resolveOwnedRuntime(webAppId, userId, threadId, selection);
15400
+ const agent = this.resolveAgent(runtime, threadId);
15401
+ const execution = await this.execute(agent, runtime, { input: { message: content } });
15402
+ return { messageId: execution.messageId };
15403
+ }
15404
+ async removePendingMessage(webAppId, userId, threadId, messageId, selection) {
15405
+ const runtime = await this.resolveRuntime(webAppId, selection);
15406
+ await this.requireOwnedThread(runtime, userId, threadId);
15407
+ const agent = this.resolveAgent(runtime, threadId);
15408
+ if (!await agent.removePendingMessage(messageId)) this.threadNotFound();
15409
+ }
15410
+ /** Thread-lifetime SSE resume from the last known client message id. */
15411
+ async resumeStream(webAppId, userId, threadId, messageId, selection) {
15412
+ const runtime = await this.resolveRuntime(webAppId, selection);
15413
+ await this.requireOwnedThread(runtime, userId, threadId);
15414
+ const agent = this.resolveAgent(runtime, threadId);
15415
+ return { stream: agent.chunkStream(messageId, []) };
15416
+ }
15417
+ async resolveFileScope(webAppId, selection) {
15418
+ const runtime = await this.resolveRuntime(webAppId, selection);
15419
+ return {
15420
+ tenantId: runtime.webApp.tenantId,
15421
+ workspaceId: runtime.project.workspaceId,
15422
+ projectId: runtime.project.id,
15423
+ assistantId: runtime.webApp.assistantId,
15424
+ enabled: runtime.webApp.features.files === true
15425
+ };
15426
+ }
15101
15427
  async streamMessage(webAppId, userId, threadId, input) {
15102
15428
  const runtime = await this.resolveOwnedRuntime(webAppId, userId, threadId, input);
15103
15429
  const refs = this.resolveAttachmentRefs(runtime, userId, threadId, input.attachmentRefs ?? []);
@@ -15116,10 +15442,10 @@ ${formatFileRefsSection(refs)}` : input.content;
15116
15442
  const runtime = await this.resolveOwnedRuntime(webAppId, userId, threadId, selection);
15117
15443
  this.requireNamedFeature(runtime.webApp.features.attachments, "Attachments are disabled");
15118
15444
  if (!this.deps.saveProjectFile || !this.deps.attachmentRefs) this.fail("INTERNAL_ERROR", 500, "Attachment service is unavailable");
15119
- const userDigest = (0, import_node_crypto4.createHash)("sha256").update(userId, "utf8").digest("hex");
15120
- const contentDigest = (0, import_node_crypto4.createHash)("sha256").update(file.bytes).digest("hex");
15445
+ const userDigest = (0, import_node_crypto6.createHash)("sha256").update(userId, "utf8").digest("hex");
15446
+ const contentDigest = (0, import_node_crypto6.createHash)("sha256").update(file.bytes).digest("hex");
15121
15447
  const displayName = file.name;
15122
- const physicalName = `${(0, import_node_crypto4.randomUUID)()}-${sanitizePhysicalFilename(file.name)}`;
15448
+ const physicalName = `${(0, import_node_crypto6.randomUUID)()}-${sanitizePhysicalFilename(file.name)}`;
15123
15449
  const saved = await this.deps.saveProjectFile({
15124
15450
  tenantId: runtime.webApp.tenantId,
15125
15451
  workspaceId: runtime.project.workspaceId,
@@ -15271,7 +15597,7 @@ ${formatFileRefsSection(refs)}` : input.content;
15271
15597
  const thread = await this.deps.threadStore.createThread(
15272
15598
  runtime.webApp.tenantId,
15273
15599
  runtime.webApp.assistantId,
15274
- (0, import_node_crypto4.randomUUID)(),
15600
+ (0, import_node_crypto6.randomUUID)(),
15275
15601
  { metadata: this.metadata(runtime, userId, normalizeLabel(rawLabel)) }
15276
15602
  );
15277
15603
  return toRuntimeThread(thread);
@@ -15323,7 +15649,7 @@ function implicitThreadId(runtime, userId) {
15323
15649
  userId,
15324
15650
  runtime.project.id
15325
15651
  ]);
15326
- const bytes = (0, import_node_crypto4.createHash)("sha256").update(canonicalTuple, "utf8").digest().subarray(0, 16);
15652
+ const bytes = (0, import_node_crypto6.createHash)("sha256").update(canonicalTuple, "utf8").digest().subarray(0, 16);
15327
15653
  bytes[6] = bytes[6] & 15 | 128;
15328
15654
  bytes[8] = bytes[8] & 63 | 128;
15329
15655
  const hex = bytes.toString("hex");
@@ -15342,93 +15668,102 @@ function toRuntimeThread(thread) {
15342
15668
  };
15343
15669
  }
15344
15670
 
15345
- // src/services/agent-web-app/AgentWebAppStreamProjector.ts
15346
- var import_protocols12 = require("@axiom-lattice/protocols");
15347
- function createAgentWebAppStreamProjectionContext(options = {}) {
15348
- return { startedToolIds: /* @__PURE__ */ new Set(), allowInterrupts: options.allowInterrupts ?? true, allowGenUI: options.allowGenUI ?? false };
15349
- }
15350
- function projectAgentWebAppChunk(chunk, context) {
15351
- if (chunk.type === import_protocols12.MessageChunkTypes.AI) {
15352
- const events = [];
15353
- const startedToolIds = context?.startedToolIds ?? /* @__PURE__ */ new Set();
15354
- if (typeof chunk.data.content === "string" && chunk.data.content) events.push({ type: "message.delta", text: chunk.data.content });
15355
- if (context?.allowGenUI && Array.isArray(chunk.data.content)) {
15356
- for (const value of chunk.data.content) {
15357
- const block = (0, import_protocols12.parseAgentWebAppGenUIBlock)(value);
15358
- if (block) events.push({ type: "genui.render", block });
15359
- }
15360
- }
15361
- for (const call of chunk.data.tool_call_chunks ?? []) {
15362
- if (call.id && call.name && !startedToolIds.has(call.id)) {
15363
- startedToolIds.add(call.id);
15364
- events.push({ type: "tool.started", id: call.id, name: call.name });
15365
- }
15366
- }
15367
- for (const call of chunk.data.tool_calls ?? []) {
15368
- if (!startedToolIds.has(call.id)) {
15369
- startedToolIds.add(call.id);
15370
- events.push({ type: "tool.started", id: call.id, name: call.name });
15371
- }
15372
- }
15373
- return events;
15374
- }
15375
- if (chunk.type === import_protocols12.MessageChunkTypes.TOOL && chunk.data.tool_call_id) {
15376
- return [{ type: "tool.completed", id: chunk.data.tool_call_id }];
15377
- }
15378
- if (chunk.type === import_protocols12.MessageChunkTypes.INTERRUPT) {
15379
- if (context && !context.allowInterrupts) return [{ type: "stream.completed" }];
15380
- const interrupt = publicInterrupt(chunk);
15381
- return interrupt ? [{ type: "interrupt.created", interrupt }, { type: "stream.completed" }] : [{ type: "stream.completed" }];
15382
- }
15383
- if (chunk.type === import_protocols12.MessageChunkTypes.MESSAGE_COMPLETED) {
15384
- return [
15385
- { type: "message.completed", messageId: chunk.data.id },
15386
- { type: "stream.completed" }
15387
- ];
15388
- }
15389
- if (chunk.type === import_protocols12.MessageChunkTypes.MESSAGE_FAILED) {
15390
- return [
15391
- { type: "error", error: { code: "STREAM_FAILED", message: "Stream failed", retryable: true } },
15392
- { type: "stream.completed" }
15393
- ];
15671
+ // src/services/agent-web-app/AgentWebAppTokenVerifier.ts
15672
+ var import_node_crypto7 = require("crypto");
15673
+ var WebAppTokenError = class extends Error {
15674
+ constructor(message) {
15675
+ super(message);
15676
+ this.name = "WebAppTokenError";
15394
15677
  }
15395
- return [];
15396
- }
15397
- function publicInterrupt(chunk) {
15398
- const value = parseInterruptContent(chunk.data.content);
15399
- if (!value) return void 0;
15400
- const topLevelId = chunk.id;
15401
- const id3 = typeof topLevelId === "string" ? topLevelId : chunk.data.id;
15402
- if (!id3) return void 0;
15403
- return {
15404
- id: id3,
15405
- type: typeof value.type === "string" ? value.type : "input",
15406
- prompt: typeof value.prompt === "string" ? value.prompt : typeof value.message === "string" ? value.message : "Input required",
15407
- ...isRecord3(value.data) ? { data: value.data } : {}
15408
- };
15409
- }
15410
- function parseInterruptContent(content) {
15411
- if (!content) return {};
15412
- if (isRecord3(content)) return content;
15413
- if (typeof content !== "string") return void 0;
15678
+ };
15679
+ var MAX_TOKEN_BYTES = 8 * 1024;
15680
+ var CLOCK_SKEW_SECONDS = 60;
15681
+ var JWKS_TTL_MS = 10 * 60 * 1e3;
15682
+ var jwksCache = /* @__PURE__ */ new Map();
15683
+ function decodeSegment(segment) {
15684
+ const json = Buffer.from(segment, "base64url").toString("utf8");
15685
+ let value;
15414
15686
  try {
15415
- const parsed = JSON.parse(content);
15416
- return isRecord3(parsed) ? parsed : { prompt: content };
15687
+ value = JSON.parse(json);
15417
15688
  } catch {
15418
- return { prompt: content };
15689
+ throw new WebAppTokenError("Malformed token segment");
15419
15690
  }
15691
+ if (typeof value !== "object" || value === null) throw new WebAppTokenError("Malformed token segment");
15692
+ return value;
15420
15693
  }
15421
- function isRecord3(value) {
15422
- return typeof value === "object" && value !== null && !Array.isArray(value);
15694
+ async function resolveKey(issuer, kid, fetchImpl) {
15695
+ const cacheKey = issuer.jwksUrl ?? "";
15696
+ const load = async () => {
15697
+ const response = await fetchImpl(issuer.jwksUrl ?? "");
15698
+ if (!response.ok) throw new WebAppTokenError("JWKS fetch failed");
15699
+ const document = await response.json();
15700
+ const keys2 = document.keys;
15701
+ const entry2 = { keys: Array.isArray(keys2) ? keys2 : [], fetchedAt: Date.now() };
15702
+ jwksCache.set(cacheKey, entry2);
15703
+ return entry2;
15704
+ };
15705
+ const cached = jwksCache.get(cacheKey);
15706
+ let entry = cached && Date.now() - cached.fetchedAt < JWKS_TTL_MS ? cached : await load();
15707
+ let jwk = entry.keys.find((key) => kid === void 0 ? true : key.kid === kid);
15708
+ if (!jwk && kid !== void 0) {
15709
+ entry = await load();
15710
+ jwk = entry.keys.find((key) => key.kid === kid);
15711
+ }
15712
+ if (!jwk) throw new WebAppTokenError("No matching JWKS key");
15713
+ return (0, import_node_crypto7.createPublicKey)({ key: jwk, format: "jwk" });
15714
+ }
15715
+ async function verifyWebAppToken(token, issuer, options = {}) {
15716
+ const nowMs = options.now ? options.now() : Date.now();
15717
+ const now = Math.floor(nowMs / 1e3);
15718
+ if (token.length === 0 || Buffer.byteLength(token) > MAX_TOKEN_BYTES) throw new WebAppTokenError("Token size invalid");
15719
+ const parts = token.split(".");
15720
+ if (parts.length !== 3) throw new WebAppTokenError("Token structure invalid");
15721
+ const [rawHeader, rawPayload, rawSignature] = parts;
15722
+ const header2 = decodeSegment(rawHeader);
15723
+ const payload = decodeSegment(rawPayload);
15724
+ if (header2.alg !== issuer.alg) throw new WebAppTokenError("Token algorithm mismatch");
15725
+ if (payload.iss !== issuer.iss) throw new WebAppTokenError("Token issuer mismatch");
15726
+ if (payload.aud !== issuer.aud) throw new WebAppTokenError("Token audience mismatch");
15727
+ const exp = typeof payload.exp === "number" ? payload.exp : void 0;
15728
+ if (exp === void 0 || exp + CLOCK_SKEW_SECONDS < now) throw new WebAppTokenError("Token expired");
15729
+ const nbf = typeof payload.nbf === "number" ? payload.nbf : void 0;
15730
+ if (nbf !== void 0 && nbf - CLOCK_SKEW_SECONDS > now) throw new WebAppTokenError("Token not yet valid");
15731
+ const signature = Buffer.from(rawSignature, "base64url");
15732
+ const data = Buffer.from(`${rawHeader}.${rawPayload}`);
15733
+ if (issuer.alg === "HS256") {
15734
+ if (typeof issuer.secret !== "string" || issuer.secret.length === 0) throw new WebAppTokenError("Issuer secret missing");
15735
+ const expected = (0, import_node_crypto7.createHmac)("sha256", issuer.secret).update(data).digest();
15736
+ if (signature.length !== expected.length || !(0, import_node_crypto7.timingSafeEqual)(signature, expected)) {
15737
+ throw new WebAppTokenError("Token signature invalid");
15738
+ }
15739
+ } else {
15740
+ if (typeof issuer.jwksUrl !== "string") throw new WebAppTokenError("Issuer JWKS URL missing");
15741
+ const key = await resolveKey(issuer, typeof header2.kid === "string" ? header2.kid : void 0, options.fetchImpl ?? fetch);
15742
+ let valid = false;
15743
+ try {
15744
+ valid = (0, import_node_crypto7.verify)("RSA-SHA256", data, key, signature);
15745
+ } catch {
15746
+ valid = false;
15747
+ }
15748
+ if (!valid) throw new WebAppTokenError("Token signature invalid");
15749
+ }
15750
+ const sub = payload.sub;
15751
+ if (typeof sub !== "string" || sub.length === 0 || sub.length > 128 || !/^[A-Za-z0-9._:@-]+$/.test(sub)) {
15752
+ throw new WebAppTokenError("Token subject invalid");
15753
+ }
15754
+ return sub;
15423
15755
  }
15424
15756
 
15425
15757
  // src/routes/agent-web-app-runtime.ts
15758
+ var import_protocols12 = require("@axiom-lattice/protocols");
15426
15759
  var idSchema = import_zod6.z.string().min(1).max(128);
15427
15760
  var paramsSchema = import_zod6.z.object({ webAppId: idSchema }).strict();
15428
15761
  var threadParamsSchema = import_zod6.z.object({ webAppId: idSchema, threadId: idSchema }).strict();
15429
15762
  var interruptParamsSchema = import_zod6.z.object({ webAppId: idSchema, threadId: idSchema, interruptId: idSchema }).strict();
15763
+ var pendingMessageParamsSchema = import_zod6.z.object({ webAppId: idSchema, threadId: idSchema, messageId: idSchema }).strict();
15430
15764
  var bootstrapQuerySchema = import_zod6.z.object({ projectId: idSchema.optional(), modelKey: idSchema.optional() }).strict();
15431
15765
  var projectQuerySchema = import_zod6.z.object({ projectId: idSchema.optional() }).strict();
15766
+ var projectQueryLenientSchema = projectQuerySchema.passthrough();
15432
15767
  var threadBodySchema = import_zod6.z.object({ projectId: idSchema.optional(), label: import_zod6.z.string().max(128).optional() }).strict();
15433
15768
  var streamBodySchema = import_zod6.z.object({
15434
15769
  content: import_zod6.z.string().trim().min(1).max(32768),
@@ -15447,8 +15782,27 @@ var resumeBodySchema = import_zod6.z.object({
15447
15782
  ])
15448
15783
  }).strict();
15449
15784
  var MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
15785
+ var stateQuerySchema = import_zod6.z.object({
15786
+ threadId: idSchema,
15787
+ projectId: idSchema.optional(),
15788
+ modelKey: idSchema.optional()
15789
+ }).strict();
15790
+ var resumeSchema = import_zod6.z.object({
15791
+ messageId: idSchema,
15792
+ projectId: idSchema.optional(),
15793
+ modelKey: idSchema.optional()
15794
+ }).strict();
15795
+ var devTokenSchema = import_zod6.z.object({
15796
+ iss: import_zod6.z.string().min(1).max(255).optional(),
15797
+ sub: import_zod6.z.string().regex(/^[A-Za-z0-9._:@-]+$/).max(128).optional(),
15798
+ hours: import_zod6.z.number().gt(0).max(720).optional()
15799
+ }).strict();
15800
+ var fileQuerySchema = import_zod6.z.object({
15801
+ projectId: idSchema.optional(),
15802
+ modelKey: idSchema.optional()
15803
+ }).passthrough();
15450
15804
  function createExternalHeaderIdentityResolver() {
15451
- return { resolve(request) {
15805
+ return { async resolve(request) {
15452
15806
  const raw = request.headers["x-axiom-external-user-id"];
15453
15807
  if (raw === void 0) throw new AgentWebAppRuntimeError("USER_ID_REQUIRED", 401, "External user id is required");
15454
15808
  if (Array.isArray(raw) || typeof raw !== "string") throw new AgentWebAppRuntimeError("INVALID_USER_ID", 400, "External user id is invalid");
@@ -15457,47 +15811,109 @@ function createExternalHeaderIdentityResolver() {
15457
15811
  return { userId, assurance: "unverified" };
15458
15812
  } };
15459
15813
  }
15814
+ function createWebAppIdentityResolver(deps) {
15815
+ const headerResolver = createExternalHeaderIdentityResolver();
15816
+ return {
15817
+ async resolve(request, webAppId) {
15818
+ let config;
15819
+ try {
15820
+ config = await deps.getIdentityConfig(webAppId);
15821
+ } catch {
15822
+ config = void 0;
15823
+ }
15824
+ const policy = config?.policy ?? "unverified";
15825
+ const authHeader = request.headers.authorization;
15826
+ const bearer = typeof authHeader === "string" && authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : void 0;
15827
+ if (policy !== "unverified" && bearer !== void 0) {
15828
+ const issuers = config?.issuers ?? [];
15829
+ if (issuers.length === 0) throw new AgentWebAppRuntimeError("INVALID_REQUEST", 401, "No trusted issuer configured");
15830
+ let lastError = new AgentWebAppRuntimeError("INVALID_REQUEST", 401, "Invalid identity token");
15831
+ for (const issuer of issuers) {
15832
+ try {
15833
+ const sub = await verifyWebAppToken(bearer, issuer);
15834
+ return { userId: sub, assurance: "verified" };
15835
+ } catch (error) {
15836
+ if (error instanceof WebAppTokenError) {
15837
+ lastError = error;
15838
+ continue;
15839
+ }
15840
+ throw error;
15841
+ }
15842
+ }
15843
+ const message = lastError instanceof WebAppTokenError ? `Invalid identity token: ${lastError.message}` : "Invalid identity token";
15844
+ throw new AgentWebAppRuntimeError("INVALID_REQUEST", 401, message);
15845
+ }
15846
+ if (policy === "verified") throw new AgentWebAppRuntimeError("USER_ID_REQUIRED", 401, "A verified identity token is required");
15847
+ return headerResolver.resolve(request, webAppId);
15848
+ }
15849
+ };
15850
+ }
15460
15851
  function registerAgentWebAppRuntimeRoutes(app2, deps) {
15461
- const identities = deps.identityResolver ?? createExternalHeaderIdentityResolver();
15852
+ const identities = deps.identityResolver ?? createWebAppIdentityResolver({ getIdentityConfig: (webAppId) => deps.service.getIdentityConfig(webAppId) });
15462
15853
  app2.register(async (routes) => {
15463
15854
  routes.setErrorHandler((error, _request, reply) => normalizeError2(reply, error));
15464
15855
  routes.get("/api/web-apps/:webAppId/runtime/bootstrap", async (request, reply) => handle2(reply, async () => {
15465
15856
  const { webAppId } = paramsSchema.parse(request.params);
15466
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
15467
- return { data: await deps.service.bootstrap(webAppId, identity.userId, withPreview(bootstrapQuerySchema.parse(request.query), identity.preview)) };
15857
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15858
+ return { data: await deps.service.bootstrap(webAppId, identity.userId, withPreview(bootstrapQuerySchema.parse(request.query), identity.preview), { assurance: identity.assurance ?? "unverified" }) };
15468
15859
  }));
15469
15860
  routes.get("/api/web-apps/:webAppId/runtime/threads", async (request, reply) => handle2(reply, async () => {
15470
15861
  const { webAppId } = paramsSchema.parse(request.params);
15471
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
15862
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15472
15863
  const records = await deps.service.listThreads(webAppId, identity.userId, withPreview(projectQuerySchema.parse(request.query), identity.preview));
15473
15864
  return { data: { records, total: records.length } };
15474
15865
  }));
15475
15866
  routes.post("/api/web-apps/:webAppId/runtime/threads", async (request, reply) => handle2(reply, async () => {
15476
15867
  const { webAppId } = paramsSchema.parse(request.params);
15477
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
15868
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15478
15869
  return { statusCode: 201, data: await deps.service.createThread(webAppId, identity.userId, withPreview(threadBodySchema.parse(request.body), identity.preview)) };
15479
15870
  }));
15480
15871
  routes.patch("/api/web-apps/:webAppId/runtime/threads/:threadId", async (request, reply) => handle2(reply, async () => {
15481
15872
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
15482
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
15873
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15483
15874
  return { data: await deps.service.updateThread(webAppId, identity.userId, threadId, withPreview(threadBodySchema.parse(request.body), identity.preview)) };
15484
15875
  }));
15485
15876
  routes.delete("/api/web-apps/:webAppId/runtime/threads/:threadId", async (request, reply) => handle2(reply, async () => {
15486
15877
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
15487
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
15878
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15488
15879
  await deps.service.deleteThread(webAppId, identity.userId, threadId, withPreview(projectQuerySchema.parse(request.query), identity.preview));
15489
15880
  return { data: { id: threadId } };
15490
15881
  }));
15491
15882
  routes.get("/api/web-apps/:webAppId/runtime/threads/:threadId/messages", async (request, reply) => handle2(reply, async () => {
15492
15883
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
15493
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
15884
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15494
15885
  const messages = await deps.service.getMessages(webAppId, identity.userId, threadId, withPreview(projectQuerySchema.parse(request.query), identity.preview));
15495
15886
  return { data: { messages } };
15496
15887
  }));
15888
+ routes.get("/api/web-apps/:webAppId/runtime/state", async (request, reply) => handle2(reply, async () => {
15889
+ const { webAppId } = paramsSchema.parse(request.params);
15890
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15891
+ const { threadId, projectId, modelKey: modelKey2 } = stateQuerySchema.parse(request.query);
15892
+ const state = await deps.service.getState(webAppId, identity.userId, threadId, withPreview({ projectId, modelKey: modelKey2 }, identity.preview));
15893
+ return { data: state };
15894
+ }));
15895
+ routes.delete("/api/web-apps/:webAppId/runtime/threads/:threadId/pending-messages/:messageId", async (request, reply) => handle2(reply, async () => {
15896
+ const { webAppId, threadId, messageId } = pendingMessageParamsSchema.parse(request.params);
15897
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15898
+ await deps.service.removePendingMessage(webAppId, identity.userId, threadId, messageId, withPreview(projectQuerySchema.parse(request.query), identity.preview));
15899
+ return { data: { removed: true } };
15900
+ }));
15497
15901
  routes.post("/api/web-apps/:webAppId/runtime/threads/:threadId/messages/stream", async (request, reply) => {
15498
15902
  try {
15499
15903
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
15500
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
15904
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15905
+ const input = streamBodySchema.parse(request.body);
15906
+ const result = await deps.service.streamMessage(webAppId, identity.userId, threadId, withPreview(input, identity.preview));
15907
+ beginSse(reply);
15908
+ await pipeNativeWebAppStream(reply.raw, result.stream);
15909
+ } catch (error) {
15910
+ return normalizeError2(reply, error);
15911
+ }
15912
+ });
15913
+ routes.post("/api/web-apps/:webAppId/runtime/v1/threads/:threadId/messages/stream", async (request, reply) => {
15914
+ try {
15915
+ const { webAppId, threadId } = threadParamsSchema.parse(request.params);
15916
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15501
15917
  const input = streamBodySchema.parse(request.body);
15502
15918
  const result = await deps.service.streamMessage(webAppId, identity.userId, threadId, withPreview(input, identity.preview));
15503
15919
  beginSse(reply);
@@ -15506,15 +15922,27 @@ function registerAgentWebAppRuntimeRoutes(app2, deps) {
15506
15922
  return normalizeError2(reply, error);
15507
15923
  }
15508
15924
  });
15925
+ routes.post("/api/web-apps/:webAppId/runtime/threads/:threadId/resume", async (request, reply) => {
15926
+ try {
15927
+ const { webAppId, threadId } = threadParamsSchema.parse(request.params);
15928
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15929
+ const { messageId, projectId, modelKey: modelKey2 } = resumeSchema.parse(request.body);
15930
+ const result = await deps.service.resumeStream(webAppId, identity.userId, threadId, messageId, withPreview({ projectId, modelKey: modelKey2 }, identity.preview));
15931
+ beginSse(reply);
15932
+ await pipeNativeWebAppStream(reply.raw, result.stream);
15933
+ } catch (error) {
15934
+ return normalizeError2(reply, error);
15935
+ }
15936
+ });
15509
15937
  routes.post("/api/web-apps/:webAppId/runtime/threads/:threadId/abort", async (request, reply) => handle2(reply, async () => {
15510
15938
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
15511
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
15939
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15512
15940
  await deps.service.abort(webAppId, identity.userId, threadId, withPreview(projectQuerySchema.parse(request.query), identity.preview));
15513
15941
  return { data: { aborted: true } };
15514
15942
  }));
15515
15943
  routes.post("/api/web-apps/:webAppId/runtime/threads/:threadId/attachments", async (request, reply) => handle2(reply, async () => {
15516
15944
  const { webAppId, threadId } = threadParamsSchema.parse(request.params);
15517
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
15945
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15518
15946
  if (!request.isMultipart()) throw new import_zod6.ZodError([]);
15519
15947
  const part = await request.file({ limits: { files: 1, fileSize: MAX_ATTACHMENT_BYTES } });
15520
15948
  if (!part || !part.filename || part.filename.length > 255) throw new import_zod6.ZodError([]);
@@ -15530,7 +15958,7 @@ function registerAgentWebAppRuntimeRoutes(app2, deps) {
15530
15958
  routes.post("/api/web-apps/:webAppId/runtime/threads/:threadId/interrupts/:interruptId/resume", async (request, reply) => {
15531
15959
  try {
15532
15960
  const { webAppId, threadId, interruptId } = interruptParamsSchema.parse(request.params);
15533
- const identity = resolveRuntimeIdentity(request, webAppId, identities);
15961
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15534
15962
  const { projectId, modelKey: modelKey2, message, response } = resumeBodySchema.parse(request.body);
15535
15963
  const result = await deps.service.resumeInterrupt(webAppId, identity.userId, threadId, interruptId, withPreview({ projectId, modelKey: modelKey2 }, identity.preview), { message, response });
15536
15964
  beginSse(reply);
@@ -15539,34 +15967,287 @@ function registerAgentWebAppRuntimeRoutes(app2, deps) {
15539
15967
  return normalizeError2(reply, error);
15540
15968
  }
15541
15969
  });
15970
+ routes.post("/api/web-apps/:webAppId/dev/token", async (request, reply) => handle2(reply, async () => {
15971
+ if (process.env.WEB_APP_DEV_TOKEN_ENABLED !== "true") {
15972
+ throw new AgentWebAppRuntimeError("FEATURE_DISABLED", 403, "Dev token issuance is disabled (set WEB_APP_DEV_TOKEN_ENABLED=true)");
15973
+ }
15974
+ const { webAppId } = paramsSchema.parse(request.params);
15975
+ const { iss, sub, hours } = devTokenSchema.parse(request.body ?? {});
15976
+ const config = await deps.service.getIdentityConfig(webAppId);
15977
+ const issuer = (config?.issuers ?? []).find((entry) => entry.alg === "HS256" && (iss === void 0 || entry.iss === iss));
15978
+ if (!issuer || typeof issuer.secret !== "string") {
15979
+ throw new AgentWebAppRuntimeError("INVALID_REQUEST", 400, "Web app has no HS256 issuer configured");
15980
+ }
15981
+ const now = Math.floor(Date.now() / 1e3);
15982
+ const expiresIn = (hours ?? 24) * 3600;
15983
+ const subject = sub ?? "dev-user";
15984
+ const b64url = (input) => Buffer.from(input).toString("base64url");
15985
+ const body = `${b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }))}.${b64url(JSON.stringify({ sub: subject, iss: issuer.iss, aud: issuer.aud, iat: now, exp: now + expiresIn }))}`;
15986
+ const token = `${body}.${(0, import_node_crypto8.createHmac)("sha256", issuer.secret).update(body).digest("base64url")}`;
15987
+ return { data: { token, tokenType: "Bearer", expiresIn, subject } };
15988
+ }));
15989
+ if (deps.taskController) {
15990
+ const tasks = deps.taskController;
15991
+ const withSubject = (handler) => async (request, reply) => {
15992
+ const { webAppId } = paramsSchema.parse(request.params);
15993
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
15994
+ const proxy = Object.create(request);
15995
+ proxy.headers = { ...request.headers, "x-user-id": identity.userId };
15996
+ return handler(proxy, reply);
15997
+ };
15998
+ routes.get("/api/web-apps/:webAppId/runtime/tasks", withSubject((request, reply) => tasks.list(request, reply)));
15999
+ routes.post("/api/web-apps/:webAppId/runtime/tasks", withSubject((request, reply) => tasks.create(request, reply)));
16000
+ routes.get("/api/web-apps/:webAppId/runtime/tasks/:taskId", withSubject((request, reply) => tasks.get(request, reply)));
16001
+ routes.put("/api/web-apps/:webAppId/runtime/tasks/:taskId", withSubject((request, reply) => tasks.update(request, reply)));
16002
+ routes.patch("/api/web-apps/:webAppId/runtime/tasks/:taskId", withSubject((request, reply) => tasks.update(request, reply)));
16003
+ routes.delete("/api/web-apps/:webAppId/runtime/tasks/:taskId", withSubject((request, reply) => tasks.remove(request, reply)));
16004
+ routes.patch("/api/web-apps/:webAppId/runtime/tasks/:taskId/complete", withSubject((request, reply) => tasks.complete(request, reply)));
16005
+ routes.get("/api/web-apps/:webAppId/runtime/tasks/:taskId/work-items", withSubject((request, reply) => tasks.workItems(request, reply)));
16006
+ }
16007
+ if (deps.workspaceController) {
16008
+ const workspace = deps.workspaceController;
16009
+ const fileScope = async (webAppId, selection) => {
16010
+ const scope = await deps.service.resolveFileScope(webAppId, selection);
16011
+ if (!scope.enabled) throw new AgentWebAppRuntimeError("FEATURE_DISABLED", 403, "Files are disabled for this web app");
16012
+ return scope;
16013
+ };
16014
+ const scopedRequest = (request, scope, params, query) => {
16015
+ const proxy = Object.create(request);
16016
+ proxy.params = params;
16017
+ proxy.query = query;
16018
+ proxy.headers = { ...request.headers, "x-tenant-id": scope.tenantId };
16019
+ return proxy;
16020
+ };
16021
+ const unwrap = (result) => ({ data: result.data });
16022
+ routes.get("/api/web-apps/:webAppId/runtime/files/listpath", async (request) => {
16023
+ const { webAppId } = paramsSchema.parse(request.params);
16024
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16025
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
16026
+ const query = { ...request.query, assistantId: scope.assistantId };
16027
+ const result = await workspace.listPath(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query));
16028
+ return unwrap(result);
16029
+ });
16030
+ routes.get("/api/web-apps/:webAppId/runtime/files/readfile", async (request) => {
16031
+ const { webAppId } = paramsSchema.parse(request.params);
16032
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16033
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
16034
+ const query = { ...request.query, assistantId: scope.assistantId };
16035
+ const result = await workspace.readFile(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query));
16036
+ return unwrap(result);
16037
+ });
16038
+ routes.post("/api/web-apps/:webAppId/runtime/files/uploadfile", async (request, reply) => {
16039
+ const { webAppId } = paramsSchema.parse(request.params);
16040
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16041
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
16042
+ const scoped = scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, { assistantId: scope.assistantId });
16043
+ await workspace.uploadFile(scoped, reply);
16044
+ return reply;
16045
+ });
16046
+ routes.get("/api/web-apps/:webAppId/runtime/files/viewfile", async (request, reply) => {
16047
+ const { webAppId } = paramsSchema.parse(request.params);
16048
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16049
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
16050
+ const query = { ...request.query, assistantId: scope.assistantId };
16051
+ await workspace.viewFile(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query), reply);
16052
+ return reply;
16053
+ });
16054
+ routes.get("/api/web-apps/:webAppId/runtime/files/downloadfile", async (request, reply) => {
16055
+ const { webAppId } = paramsSchema.parse(request.params);
16056
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16057
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
16058
+ const query = { ...request.query, assistantId: scope.assistantId };
16059
+ await workspace.downloadFile(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query), reply);
16060
+ return reply;
16061
+ });
16062
+ routes.get("/api/web-apps/:webAppId/runtime/files/downloadfolder", async (request, reply) => {
16063
+ const { webAppId } = paramsSchema.parse(request.params);
16064
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16065
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
16066
+ const query = { ...request.query, assistantId: scope.assistantId };
16067
+ await workspace.downloadFolder(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query), reply);
16068
+ return reply;
16069
+ });
16070
+ const genuiSandboxParams = import_zod6.z.object({ webAppId: idSchema, assistantId: idSchema, threadId: idSchema }).strict();
16071
+ const delegateGenuiFile = (method) => async (request, reply) => {
16072
+ const { webAppId } = genuiSandboxParams.parse(request.params);
16073
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16074
+ const scope = await fileScope(webAppId, fileQuerySchema.parse(request.query));
16075
+ const query = { ...request.query, assistantId: scope.assistantId };
16076
+ await method(scopedRequest(request, scope, { workspaceId: scope.workspaceId, projectId: scope.projectId }, query), reply);
16077
+ return reply;
16078
+ };
16079
+ routes.get("/api/web-apps/:webAppId/runtime/assistants/:assistantId/threads/:threadId/sandbox/downloadfile", delegateGenuiFile(workspace.downloadFile.bind(workspace)));
16080
+ routes.get("/api/web-apps/:webAppId/runtime/assistants/:assistantId/threads/:threadId/sandbox/viewfile", delegateGenuiFile(workspace.viewFile.bind(workspace)));
16081
+ }
16082
+ const mirrorThreadParams = import_zod6.z.object({ webAppId: idSchema, assistantId: idSchema, threadId: idSchema }).strict();
16083
+ const mirrorBody = import_zod6.z.object({ metadata: import_zod6.z.object({ label: import_zod6.z.string().max(300).optional() }).strict().optional(), projectId: idSchema.optional(), modelKey: idSchema.optional() }).strict();
16084
+ const toConsoleThread = (thread, assistantId) => ({
16085
+ id: thread.id,
16086
+ assistantId,
16087
+ metadata: thread.label ? { label: thread.label } : {},
16088
+ createdAt: thread.createdAt,
16089
+ updatedAt: thread.updatedAt
16090
+ });
16091
+ const mirrorAssistantParams = import_zod6.z.object({ webAppId: idSchema, assistantId: idSchema }).strict();
16092
+ const mirrorPendingParams = import_zod6.z.object({ webAppId: idSchema, assistantId: idSchema, threadId: idSchema, messageId: idSchema }).strict();
16093
+ const registerMirror = (prefix) => {
16094
+ routes.get(`${prefix}/assistants/:assistantId/threads`, async (request, reply) => handle2(reply, async () => {
16095
+ const { webAppId, assistantId } = mirrorAssistantParams.parse(request.params);
16096
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16097
+ const threads = await deps.service.listThreads(webAppId, identity.userId, projectQueryLenientSchema.parse(request.query));
16098
+ return { data: { records: threads.map((thread) => toConsoleThread(thread, assistantId)) } };
16099
+ }));
16100
+ routes.post(`${prefix}/assistants/:assistantId/threads`, async (request, reply) => handle2(reply, async () => {
16101
+ const { webAppId, assistantId } = mirrorAssistantParams.parse(request.params);
16102
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16103
+ const body = mirrorBody.parse(request.body ?? {});
16104
+ const created = await deps.service.createThread(webAppId, identity.userId, { label: body.metadata?.label, ...body.projectId ? { projectId: body.projectId } : {} });
16105
+ return { statusCode: 201, data: { id: created.id } };
16106
+ }));
16107
+ routes.get(`${prefix}/assistants/:assistantId/threads/:threadId`, async (request, reply) => handle2(reply, async () => {
16108
+ const { webAppId, assistantId, threadId } = mirrorThreadParams.parse(request.params);
16109
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16110
+ const thread = await deps.service.getThread(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query));
16111
+ return { data: toConsoleThread(thread, assistantId) };
16112
+ }));
16113
+ routes.put(`${prefix}/assistants/:assistantId/threads/:threadId`, async (request, reply) => handle2(reply, async () => {
16114
+ const { webAppId, assistantId, threadId } = mirrorThreadParams.parse(request.params);
16115
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16116
+ const body = mirrorBody.parse(request.body ?? {});
16117
+ const updated = await deps.service.updateThread(webAppId, identity.userId, threadId, { label: body.metadata?.label, ...body.projectId ? { projectId: body.projectId } : {} });
16118
+ return { data: toConsoleThread(updated, assistantId) };
16119
+ }));
16120
+ routes.delete(`${prefix}/assistants/:assistantId/threads/:threadId`, async (request, reply) => handle2(reply, async () => {
16121
+ const { webAppId, threadId } = mirrorThreadParams.parse(request.params);
16122
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16123
+ await deps.service.deleteThread(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query));
16124
+ return { data: { success: true, message: "Thread deleted" } };
16125
+ }));
16126
+ routes.post(`${prefix}/assistants/:assistantId/threads/:threadId/sandbox/uploadfile`, async (request, reply) => handle2(reply, async () => {
16127
+ const { webAppId, threadId } = mirrorThreadParams.parse(request.params);
16128
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16129
+ if (!request.isMultipart()) throw new import_zod6.ZodError([]);
16130
+ const part = await request.file({ limits: { files: 1, fileSize: MAX_ATTACHMENT_BYTES } });
16131
+ if (!part || !part.filename || part.filename.length > 255) throw new import_zod6.ZodError([]);
16132
+ const bytes = await part.toBuffer();
16133
+ if (bytes.length === 0 || part.file.truncated) throw new import_zod6.ZodError([]);
16134
+ const uploaded = await deps.service.uploadAttachment(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query), {
16135
+ name: part.filename,
16136
+ mimeType: part.mimetype || "application/octet-stream",
16137
+ bytes
16138
+ });
16139
+ return { statusCode: 201, data: { id: uploaded.ref, path: uploaded.ref, name: uploaded.name, mimeType: uploaded.mimeType, size: uploaded.size } };
16140
+ }));
16141
+ routes.get(`${prefix}/assistants/:assistantId/:threadId/memory`, async (request) => {
16142
+ const { webAppId, threadId } = mirrorThreadParams.parse(request.params);
16143
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16144
+ const messages = await deps.service.getConsoleMessages(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query));
16145
+ return messages;
16146
+ });
16147
+ routes.get(`${prefix}/assistants/:assistantId/:threadId/state`, async (request) => {
16148
+ const { webAppId, threadId } = mirrorThreadParams.parse(request.params);
16149
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16150
+ return deps.service.getState(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query));
16151
+ });
16152
+ routes.delete(`${prefix}/assistants/:assistantId/threads/:threadId/pending-messages/:messageId`, async (request, reply) => handle2(reply, async () => {
16153
+ const { webAppId, threadId, messageId } = mirrorPendingParams.parse(request.params);
16154
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16155
+ await deps.service.removePendingMessage(webAppId, identity.userId, threadId, messageId, projectQueryLenientSchema.parse(request.query));
16156
+ return { data: { removed: true } };
16157
+ }));
16158
+ routes.post(`${prefix}/assistants/:assistantId/threads/:threadId/abort`, async (request, reply) => handle2(reply, async () => {
16159
+ const { webAppId, threadId } = mirrorThreadParams.parse(request.params);
16160
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16161
+ await deps.service.abort(webAppId, identity.userId, threadId, projectQueryLenientSchema.parse(request.query));
16162
+ return { data: { aborted: true } };
16163
+ }));
16164
+ const runsBodySchema = import_zod6.z.object({
16165
+ thread_id: idSchema.optional(),
16166
+ assistant_id: idSchema.optional(),
16167
+ messages: import_zod6.z.array(import_zod6.z.object({ content: import_zod6.z.string().min(1).max(32768) }).passthrough()).min(1).max(20),
16168
+ background: import_zod6.z.boolean().optional(),
16169
+ streaming: import_zod6.z.boolean().optional(),
16170
+ projectId: idSchema.optional(),
16171
+ modelKey: idSchema.optional()
16172
+ }).passthrough();
16173
+ routes.post(`${prefix}/runs`, async (request, reply) => {
16174
+ try {
16175
+ const { webAppId } = paramsSchema.parse(request.params);
16176
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16177
+ const body = runsBodySchema.parse(request.body ?? {});
16178
+ if (!body.background || body.streaming) {
16179
+ throw new AgentWebAppRuntimeError("INVALID_REQUEST", 400, "Mirror runs supports only background enqueue (stream via resume_stream)");
16180
+ }
16181
+ const threadId = body.thread_id ?? "";
16182
+ if (!threadId) throw new AgentWebAppRuntimeError("INVALID_REQUEST", 400, "thread_id is required");
16183
+ const content = body.messages.map((message) => message.content).join("\n");
16184
+ const { messageId } = await deps.service.enqueueMessage(webAppId, identity.userId, threadId, content, { projectId: body.projectId, modelKey: body.modelKey });
16185
+ return reply.code(202).send({ success: true, messageId, queued: true });
16186
+ } catch (error) {
16187
+ return normalizeError2(reply, error);
16188
+ }
16189
+ });
16190
+ const resumeStreamSchema2 = import_zod6.z.object({
16191
+ assistant_id: idSchema.optional(),
16192
+ thread_id: idSchema,
16193
+ message_id: idSchema,
16194
+ known_content: import_zod6.z.string().optional(),
16195
+ poll_interval: import_zod6.z.number().optional(),
16196
+ projectId: idSchema.optional(),
16197
+ modelKey: idSchema.optional()
16198
+ }).strict();
16199
+ routes.post(`${prefix}/resume_stream`, async (request, reply) => {
16200
+ try {
16201
+ const { webAppId } = paramsSchema.parse(request.params);
16202
+ const identity = await resolveRuntimeIdentity(request, webAppId, identities);
16203
+ const body = resumeStreamSchema2.parse(request.body ?? {});
16204
+ const result = await deps.service.resumeStream(webAppId, identity.userId, body.thread_id, body.message_id, { projectId: body.projectId, modelKey: body.modelKey });
16205
+ beginSse(reply);
16206
+ await pipeNativeWebAppStream(reply.raw, result.stream);
16207
+ } catch (error) {
16208
+ return normalizeError2(reply, error);
16209
+ }
16210
+ });
16211
+ };
16212
+ registerMirror("/api/web-apps/:webAppId/runtime");
16213
+ registerMirror("/api/web-apps/:webAppId/runtime/api");
15542
16214
  });
15543
16215
  }
15544
- function resolveRuntimeIdentity(request, webAppId, identities) {
15545
- if (request.headers["x-axiom-web-app-preview"] !== "true") return identities.resolve(request);
16216
+ async function resolveRuntimeIdentity(request, webAppId, identities) {
16217
+ if (request.headers["x-axiom-web-app-preview"] !== "true") return identities.resolve(request, webAppId);
15546
16218
  const user = request.user;
15547
16219
  if (!user?.id || !user.tenantId) throw new AgentWebAppRuntimeError("USER_ID_REQUIRED", 401, "Authenticated Console preview is required");
15548
- return { userId: `console-preview:${user.id}:${webAppId}`, preview: { tenantId: user.tenantId, userId: user.id } };
16220
+ return { userId: `console-preview:${user.id}:${webAppId}`, assurance: "unverified", preview: { tenantId: user.tenantId, userId: user.id } };
15549
16221
  }
15550
16222
  function withPreview(selection, preview) {
15551
16223
  return { ...selection, ...preview ? { preview } : {} };
15552
16224
  }
15553
16225
  async function pipeAgentWebAppStream(raw, stream, options = {}) {
15554
- const projection = createAgentWebAppStreamProjectionContext(options);
16226
+ const projection = (0, import_protocols12.createAgentWebAppStreamProjectionContext)(options);
15555
16227
  await pipeAsyncIterableToSse(raw, stream, {
15556
- project: (chunk) => projectAgentWebAppChunk(chunk, projection),
16228
+ project: (chunk) => (0, import_protocols12.projectAgentWebAppChunk)(chunk, projection),
15557
16229
  onError: () => [
15558
16230
  { type: "error", error: { code: "STREAM_FAILED", message: "Stream failed", retryable: true } },
15559
16231
  { type: "stream.completed" }
15560
16232
  ]
15561
16233
  });
15562
16234
  }
16235
+ async function pipeNativeWebAppStream(raw, stream) {
16236
+ await pipeAsyncIterableToSse(raw, stream, {
16237
+ onError: () => [{
16238
+ type: "error",
16239
+ data: { id: "", content: "Stream failed" }
16240
+ }]
16241
+ });
16242
+ }
15563
16243
  function beginSse(reply) {
15564
16244
  reply.hijack();
15565
16245
  reply.raw.writeHead(200, {
15566
16246
  "Content-Type": "text/event-stream; charset=utf-8",
15567
16247
  "Cache-Control": "no-cache, no-transform",
15568
16248
  Connection: "keep-alive",
15569
- "X-Accel-Buffering": "no"
16249
+ "X-Accel-Buffering": "no",
16250
+ "Access-Control-Allow-Origin": "*"
15570
16251
  });
15571
16252
  }
15572
16253
  async function handle2(reply, operation) {
@@ -15588,7 +16269,7 @@ function isMultipartRequestError(error) {
15588
16269
  }
15589
16270
 
15590
16271
  // src/services/agent-web-app/AgentWebAppAttachmentRef.ts
15591
- var import_node_crypto5 = require("crypto");
16272
+ var import_node_crypto9 = require("crypto");
15592
16273
  var TTL_MS = 15 * 6e4;
15593
16274
  function resolveAgentWebAppAttachmentSecret(consoleSecret) {
15594
16275
  return process.env.WEB_APP_ATTACHMENT_SECRET || process.env.AUTH_TOKEN_SECRET || process.env.JWT_SECRET || consoleSecret;
@@ -15597,7 +16278,7 @@ var AgentWebAppAttachmentRefService = class {
15597
16278
  constructor(secret, now = Date.now) {
15598
16279
  this.now = now;
15599
16280
  if (!secret) throw new Error("Attachment signing secret is required");
15600
- this.key = (0, import_node_crypto5.createHash)("sha256").update(secret, "utf8").digest();
16281
+ this.key = (0, import_node_crypto9.createHash)("sha256").update(secret, "utf8").digest();
15601
16282
  }
15602
16283
  sign(input) {
15603
16284
  const payload = {
@@ -15612,8 +16293,8 @@ var AgentWebAppAttachmentRefService = class {
15612
16293
  contentDigest: input.contentDigest,
15613
16294
  exp: this.now() + TTL_MS
15614
16295
  };
15615
- const iv = (0, import_node_crypto5.randomBytes)(12);
15616
- const cipher = (0, import_node_crypto5.createCipheriv)("aes-256-gcm", this.key, iv);
16296
+ const iv = (0, import_node_crypto9.randomBytes)(12);
16297
+ const cipher = (0, import_node_crypto9.createCipheriv)("aes-256-gcm", this.key, iv);
15617
16298
  const ciphertext = Buffer.concat([
15618
16299
  cipher.update(JSON.stringify(payload), "utf8"),
15619
16300
  cipher.final()
@@ -15628,7 +16309,7 @@ var AgentWebAppAttachmentRefService = class {
15628
16309
  const iv = decodeCanonical(encodedIv, 12);
15629
16310
  const ciphertext = decodeCanonical(encodedCiphertext);
15630
16311
  const tag = decodeCanonical(encodedTag, 16);
15631
- const decipher = (0, import_node_crypto5.createDecipheriv)("aes-256-gcm", this.key, iv);
16312
+ const decipher = (0, import_node_crypto9.createDecipheriv)("aes-256-gcm", this.key, iv);
15632
16313
  decipher.setAuthTag(tag);
15633
16314
  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
15634
16315
  const value = JSON.parse(plaintext.toString("utf8"));
@@ -15659,7 +16340,7 @@ function decodeCanonical(value, expectedBytes) {
15659
16340
  return decoded;
15660
16341
  }
15661
16342
  function digestUser(userId) {
15662
- return (0, import_node_crypto5.createHash)("sha256").update(userId, "utf8").digest("hex");
16343
+ return (0, import_node_crypto9.createHash)("sha256").update(userId, "utf8").digest("hex");
15663
16344
  }
15664
16345
  function isAttachmentPayload(value) {
15665
16346
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
@@ -16023,12 +16704,12 @@ function registerCapabilityBundleRoutes(app2, controller) {
16023
16704
  }
16024
16705
 
16025
16706
  // src/services/collaboration/ProjectRoomService.ts
16026
- var import_node_crypto6 = require("crypto");
16707
+ var import_node_crypto10 = require("crypto");
16027
16708
  var ProjectRoomService = class {
16028
16709
  /** Creates a project room service from explicitly injected collaborators. */
16029
16710
  constructor(deps) {
16030
16711
  this.deps = deps;
16031
- this.idFactory = deps.idFactory ?? import_node_crypto6.randomUUID;
16712
+ this.idFactory = deps.idFactory ?? import_node_crypto10.randomUUID;
16032
16713
  }
16033
16714
  /** Authorizes read access and atomically ensures the project's main room. */
16034
16715
  async ensureMainRoom(input) {
@@ -16047,7 +16728,7 @@ var ProjectRoomService = class {
16047
16728
  };
16048
16729
 
16049
16730
  // src/services/collaboration/ProjectMembershipService.ts
16050
- var import_node_crypto7 = require("crypto");
16731
+ var import_node_crypto11 = require("crypto");
16051
16732
  var ProjectMembershipServiceError = class extends Error {
16052
16733
  constructor(code, statusCode, message) {
16053
16734
  super(message);
@@ -16063,11 +16744,13 @@ function isMembershipConflict(error) {
16063
16744
  var ProjectMembershipService = class {
16064
16745
  constructor(deps) {
16065
16746
  this.deps = deps;
16066
- this.idFactory = deps.idFactory ?? import_node_crypto7.randomUUID;
16747
+ this.idFactory = deps.idFactory ?? import_node_crypto11.randomUUID;
16067
16748
  this.mutationMutex = deps.projectMutationMutex ?? import_core41.projectMutationMutex;
16068
16749
  }
16069
16750
  async list(actor2) {
16070
- await this.authorize(actor2);
16751
+ await this.deps.accessService.requireRosterAccess(actor2);
16752
+ const membership = await this.deps.membershipStore.findByUser(actor2.tenantId, actor2.projectId, actor2.userId);
16753
+ if (!membership || membership.status !== "active") this.accessDenied();
16071
16754
  return this.deps.membershipStore.list(actor2.tenantId, actor2.projectId);
16072
16755
  }
16073
16756
  async add(actor2, input) {
@@ -16187,7 +16870,7 @@ var ProjectMembershipService = class {
16187
16870
  };
16188
16871
 
16189
16872
  // src/services/collaboration/ProjectRosterService.ts
16190
- var import_node_crypto8 = require("crypto");
16873
+ var import_node_crypto12 = require("crypto");
16191
16874
  var ProjectRosterServiceError = class extends Error {
16192
16875
  /** Creates a stable roster service error. */
16193
16876
  constructor(code, statusCode, message) {
@@ -16204,7 +16887,7 @@ var ProjectRosterService = class {
16204
16887
  /** Creates a roster service from explicitly injected collaborators. */
16205
16888
  constructor(deps) {
16206
16889
  this.deps = deps;
16207
- this.idFactory = deps.idFactory ?? import_node_crypto8.randomUUID;
16890
+ this.idFactory = deps.idFactory ?? import_node_crypto12.randomUUID;
16208
16891
  this.mutationMutex = deps.projectMutationMutex ?? import_core41.projectMutationMutex;
16209
16892
  }
16210
16893
  /** Lists all bot memberships visible to a project reader. */
@@ -16424,7 +17107,7 @@ var ProjectRosterService = class {
16424
17107
  };
16425
17108
 
16426
17109
  // src/services/collaboration/RoomMessageService.ts
16427
- var import_node_crypto9 = require("crypto");
17110
+ var import_node_crypto13 = require("crypto");
16428
17111
 
16429
17112
  // src/services/collaboration/ProjectBotMembershipSnapshot.ts
16430
17113
  var import_protocols13 = require("@axiom-lattice/protocols");
@@ -16513,7 +17196,7 @@ var RoomMessageService = class {
16513
17196
  constructor(deps, options = {}) {
16514
17197
  this.deps = deps;
16515
17198
  this.idempotentOutcomes = /* @__PURE__ */ new Map();
16516
- this.idFactory = deps.idFactory ?? import_node_crypto9.randomUUID;
17199
+ this.idFactory = deps.idFactory ?? import_node_crypto13.randomUUID;
16517
17200
  const limit = options.idempotentOutcomeLimit ?? 1e3;
16518
17201
  if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("idempotentOutcomeLimit must be a positive safe integer");
16519
17202
  this.idempotentOutcomeLimit = limit;
@@ -16694,7 +17377,7 @@ var RoomMessageService = class {
16694
17377
  let idempotencyKey;
16695
17378
  if (value.requestIdempotencyKey !== void 0) {
16696
17379
  if (typeof value.requestIdempotencyKey !== "string" || !/^[\x21-\x7e]{1,200}$/.test(value.requestIdempotencyKey.trim())) throw new RoomMessageServiceError("INVALID_MESSAGE", 400, "Invalid idempotency key");
16697
- idempotencyKey = `room-human:${actor2.userId}:${(0, import_node_crypto9.createHash)("sha256").update(value.requestIdempotencyKey.trim(), "utf8").digest("hex")}`;
17380
+ idempotencyKey = `room-human:${actor2.userId}:${(0, import_node_crypto13.createHash)("sha256").update(value.requestIdempotencyKey.trim(), "utf8").digest("hex")}`;
16698
17381
  }
16699
17382
  return { text, mentions, botIds, ...value.replyToMessageId === void 0 ? {} : { replyToMessageId: value.replyToMessageId }, ...idempotencyKey === void 0 ? {} : { idempotencyKey } };
16700
17383
  }
@@ -16751,7 +17434,7 @@ function cloneRoomMessage(message) {
16751
17434
  };
16752
17435
  }
16753
17436
  function fingerprintRequest(input) {
16754
- return (0, import_node_crypto9.createHash)("sha256").update(JSON.stringify({
17437
+ return (0, import_node_crypto13.createHash)("sha256").update(JSON.stringify({
16755
17438
  text: input.text,
16756
17439
  mentions: input.mentions,
16757
17440
  replyToMessageId: input.replyToMessageId ?? null
@@ -16920,42 +17603,9 @@ async function execute(reply, operation) {
16920
17603
  }
16921
17604
 
16922
17605
  // src/routes/cospace.ts
16923
- var import_node_crypto10 = require("crypto");
17606
+ var import_node_crypto14 = require("crypto");
16924
17607
  var import_zod12 = require("zod");
16925
17608
  var import_core44 = require("@axiom-lattice/core");
16926
-
16927
- // src/services/collaboration/RoomCoordinatorProvisioner.ts
16928
- var ROOM_COORDINATOR_KEY = "room-coordinator";
16929
- var RoomCoordinatorProvisioner = class {
16930
- /** Creates a provisioner over the tenant-scoped assistant store operations. */
16931
- constructor(deps) {
16932
- this.deps = deps;
16933
- }
16934
- /**
16935
- * Ensures the Room Coordinator assistant exists, reusing an existing record
16936
- * and reconciling concurrent create races by rereading the winner.
16937
- *
16938
- * @param tenantId Tenant to provision the coordinator for.
16939
- * @returns The persisted Room Coordinator assistant.
16940
- */
16941
- async ensure(tenantId) {
16942
- const existing = await this.deps.getAssistantById(tenantId, ROOM_COORDINATOR_KEY);
16943
- if (existing) return existing;
16944
- try {
16945
- return await this.deps.createAssistant(tenantId, ROOM_COORDINATOR_KEY, {
16946
- name: "Room Coordinator",
16947
- description: "Coordinates project room conversations and organizes room work.",
16948
- graphDefinition: {}
16949
- });
16950
- } catch (error) {
16951
- const winner = await this.deps.getAssistantById(tenantId, ROOM_COORDINATOR_KEY);
16952
- if (winner) return winner;
16953
- throw error;
16954
- }
16955
- }
16956
- };
16957
-
16958
- // src/routes/cospace.ts
16959
17609
  var CoSpaceAuthError = class extends Error {
16960
17610
  constructor() {
16961
17611
  super(...arguments);
@@ -16971,7 +17621,7 @@ function getActor2(request) {
16971
17621
  return { tenantId: user.tenantId, userId: user.id };
16972
17622
  }
16973
17623
  function registerCoSpaceRoutes(app2, deps) {
16974
- const idFactory = deps.idFactory ?? (() => (0, import_node_crypto10.randomUUID)());
17624
+ const idFactory = deps.idFactory ?? (() => (0, import_node_crypto14.randomUUID)());
16975
17625
  app2.get(
16976
17626
  "/api/workspaces/:workspaceId/rooms",
16977
17627
  async (request, reply) => {
@@ -16996,6 +17646,7 @@ function registerCoSpaceRoutes(app2, deps) {
16996
17646
  );
16997
17647
  const accessibleProjects = [];
16998
17648
  for (const project of allProjects) {
17649
+ if (project.kind === "public") continue;
16999
17650
  const membership = await deps.membershipStore.findByUser(
17000
17651
  actor2.tenantId,
17001
17652
  project.id,
@@ -17049,6 +17700,34 @@ function registerCoSpaceRoutes(app2, deps) {
17049
17700
  const bTime = b.lastMessage?.createdAt?.getTime() ?? 0;
17050
17701
  return bTime - aTime;
17051
17702
  });
17703
+ const publicRoom = await deps.publicRoom.ensure(actor2.tenantId, params.workspaceId);
17704
+ if (publicRoom) {
17705
+ const publicMessages = await deps.messageStore.list({
17706
+ tenantId: actor2.tenantId,
17707
+ roomId: publicRoom.room.id,
17708
+ limit: 1
17709
+ });
17710
+ const publicMemberships = await deps.membershipStore.list(
17711
+ actor2.tenantId,
17712
+ publicRoom.project.id
17713
+ );
17714
+ const publicParticipantNames = [];
17715
+ for (const m of publicMemberships) {
17716
+ if (m.status !== "active") continue;
17717
+ const user = await deps.userStore.getUserById(m.userId);
17718
+ if (user) {
17719
+ publicParticipantNames.push(user.name);
17720
+ }
17721
+ }
17722
+ rooms.unshift({
17723
+ project: publicRoom.project,
17724
+ room: publicRoom.room,
17725
+ lastMessage: publicMessages[0],
17726
+ participantCount: publicMemberships.filter((m) => m.status === "active").length,
17727
+ participantNames: publicParticipantNames,
17728
+ isPublic: true
17729
+ });
17730
+ }
17052
17731
  return { data: { rooms } };
17053
17732
  });
17054
17733
  }
@@ -17805,7 +18484,7 @@ function exactValues(value, required, optional = []) {
17805
18484
  }
17806
18485
 
17807
18486
  // src/services/collaboration/CoordinatorWakeNotifier.ts
17808
- var import_node_crypto11 = require("crypto");
18487
+ var import_node_crypto15 = require("crypto");
17809
18488
  var import_core45 = require("@axiom-lattice/core");
17810
18489
  var import_protocols15 = require("@axiom-lattice/protocols");
17811
18490
  var SYNTHETIC_WAKE_AUTHOR = "coordinator-wake";
@@ -17820,7 +18499,7 @@ var CoordinatorWakeNotifier = class {
17820
18499
  this.accepted = /* @__PURE__ */ new Set();
17821
18500
  this.maxPending = deps.maxPending ?? 1024;
17822
18501
  if (!Number.isSafeInteger(this.maxPending) || this.maxPending < 1) throw new RangeError("maxPending must be a positive safe integer");
17823
- this.idFactory = deps.idFactory ?? import_node_crypto11.randomUUID;
18502
+ this.idFactory = deps.idFactory ?? import_node_crypto15.randomUUID;
17824
18503
  }
17825
18504
  /** Admits one notification without awaiting any store or dispatch operation. */
17826
18505
  handle(value) {
@@ -18683,7 +19362,7 @@ function validRecoveryRow(row, task, thread) {
18683
19362
  }
18684
19363
 
18685
19364
  // src/services/collaboration/ProjectTaskObserver.ts
18686
- var import_node_crypto12 = require("crypto");
19365
+ var import_node_crypto16 = require("crypto");
18687
19366
  var import_core47 = require("@axiom-lattice/core");
18688
19367
  var SYSTEM_ACTOR2 = "system";
18689
19368
  var TERMINAL_ACTIONS = ["completed", "failed", "interrupted", "cancelled"];
@@ -19220,7 +19899,7 @@ var ProjectTaskObserver = class {
19220
19899
  action: "activity",
19221
19900
  summary: "Project task recovery warning",
19222
19901
  detail: { type: "project_task_recovery_warning", phase, message },
19223
- eventKey: `project-task-recovery-warning:${task.id}:${phase}:${(0, import_node_crypto12.createHash)("sha256").update(message).digest("hex")}`
19902
+ eventKey: `project-task-recovery-warning:${task.id}:${phase}:${(0, import_node_crypto16.createHash)("sha256").update(message).digest("hex")}`
19224
19903
  });
19225
19904
  }
19226
19905
  async safeWarning(task, phase, error) {
@@ -19287,7 +19966,7 @@ var ProjectTaskObserver = class {
19287
19966
  }
19288
19967
  }
19289
19968
  async persistOverflowWarning(event) {
19290
- const scopeDigest = (0, import_node_crypto12.createHash)("sha256").update(JSON.stringify([event.tenantId, event.workspaceId, event.projectId])).digest("hex");
19969
+ const scopeDigest = (0, import_node_crypto16.createHash)("sha256").update(JSON.stringify([event.tenantId, event.workspaceId, event.projectId])).digest("hex");
19291
19970
  try {
19292
19971
  await this.deps.workItems.createIfAbsentByEventKey({
19293
19972
  tenantId: event.tenantId,
@@ -19388,7 +20067,7 @@ function combineSignals(left, right) {
19388
20067
  }
19389
20068
 
19390
20069
  // src/services/collaboration/RoomProjectionService.ts
19391
- var import_node_crypto13 = require("crypto");
20070
+ var import_node_crypto17 = require("crypto");
19392
20071
  var import_core48 = require("@axiom-lattice/core");
19393
20072
  var import_protocols18 = require("@axiom-lattice/protocols");
19394
20073
  var RoomProjectionService = class {
@@ -19557,7 +20236,7 @@ function safeRepairIdentity(value, scope) {
19557
20236
  return taskId && eventKey && tenantId === scope.tenantId && workspaceId === scope.workspaceId && projectId === scope.projectId ? { taskId, eventKey } : void 0;
19558
20237
  }
19559
20238
  function tupleKey(prefix, values) {
19560
- const digest = (0, import_node_crypto13.createHash)("sha256").update(JSON.stringify(values)).digest("hex");
20239
+ const digest = (0, import_node_crypto17.createHash)("sha256").update(JSON.stringify(values)).digest("hex");
19561
20240
  return `${prefix}:${digest}`;
19562
20241
  }
19563
20242
  function parseProjectionInput(taskValue, itemValue) {
@@ -19585,7 +20264,7 @@ function snapshotProjectableWorkItem(value) {
19585
20264
  if (!row || !createdAt || typeof row.id !== "string" || !row.id || typeof row.taskId !== "string" || !row.taskId || typeof row.tenantId !== "string" || !row.tenantId || typeof row.workspaceId !== "string" || !row.workspaceId || typeof row.projectId !== "string" || !row.projectId || typeof row.actor !== "string" || !row.actor || typeof row.eventKey !== "string" || !row.eventKey || !import_protocols18.PROJECT_TASK_LIFECYCLE_ACTIONS.includes(row.action) || row.threadId !== void 0 && typeof row.threadId !== "string" || row.summary !== void 0 && typeof row.summary !== "string" || row.attempt !== void 0 && (typeof row.attempt !== "number" || !Number.isFinite(row.attempt))) return void 0;
19586
20265
  try {
19587
20266
  const detail = row.detail === void 0 ? void 0 : snapshotJsonValue(row.detail);
19588
- if (detail !== void 0 && !isRecord4(detail)) return void 0;
20267
+ if (detail !== void 0 && !isRecord3(detail)) return void 0;
19589
20268
  return {
19590
20269
  id: row.id,
19591
20270
  taskId: row.taskId,
@@ -19732,7 +20411,7 @@ function snapshotLifecycleCursor(value) {
19732
20411
  const createdAt = cloneExactDate(row?.createdAt);
19733
20412
  return row && typeof row.id === "string" && row.id.length > 0 && createdAt ? { id: row.id, createdAt } : void 0;
19734
20413
  }
19735
- function isRecord4(value) {
20414
+ function isRecord3(value) {
19736
20415
  return typeof value === "object" && value !== null && !Array.isArray(value);
19737
20416
  }
19738
20417
 
@@ -19740,7 +20419,7 @@ function isRecord4(value) {
19740
20419
  var import_core50 = require("@axiom-lattice/core");
19741
20420
 
19742
20421
  // src/services/collaboration/ProjectRoomEventBroker.ts
19743
- var import_node_crypto14 = require("crypto");
20422
+ var import_node_crypto18 = require("crypto");
19744
20423
  var import_protocols19 = require("@axiom-lattice/protocols");
19745
20424
  var DEFAULT_MAX_EVENTS_PER_ROOM = 1e3;
19746
20425
  var DEFAULT_MAX_EVENT_AGE_MS = 10 * 60 * 1e3;
@@ -19765,7 +20444,7 @@ var ProjectRoomEventBroker = class {
19765
20444
  this.subscriberCount = 0;
19766
20445
  this.usageClock = 0;
19767
20446
  this.closed = false;
19768
- this.epoch = options.epoch ?? (0, import_node_crypto14.randomUUID)();
20447
+ this.epoch = options.epoch ?? (0, import_node_crypto18.randomUUID)();
19769
20448
  if (!UUID_PATTERN.test(this.epoch)) {
19770
20449
  throw new TypeError("Project Room event epoch must be a lowercase canonical UUID");
19771
20450
  }
@@ -20083,7 +20762,7 @@ function snapshotJson(value, ancestors = /* @__PURE__ */ new Set()) {
20083
20762
  }
20084
20763
 
20085
20764
  // src/services/collaboration/ProjectRoomRealtimePublisher.ts
20086
- var import_node_crypto15 = require("crypto");
20765
+ var import_node_crypto19 = require("crypto");
20087
20766
  var import_protocols20 = require("@axiom-lattice/protocols");
20088
20767
  var BrokerProjectRoomRealtimePublisher = class {
20089
20768
  /** Creates a publisher around a broker and structured warning logger. */
@@ -20159,7 +20838,7 @@ var BrokerProjectRoomRealtimePublisher = class {
20159
20838
  try {
20160
20839
  event = build();
20161
20840
  if (!event) throw new Error("Invalid Project Room realtime projection");
20162
- const dedupeKey = (0, import_node_crypto15.createHash)("sha256").update(JSON.stringify([type, event.scope.tenantId, event.scope.projectId, event.scope.roomId, event.sourceKey])).digest("hex");
20841
+ const dedupeKey = (0, import_node_crypto19.createHash)("sha256").update(JSON.stringify([type, event.scope.tenantId, event.scope.projectId, event.scope.roomId, event.sourceKey])).digest("hex");
20163
20842
  if (this.sourceKeys.has(dedupeKey)) return;
20164
20843
  this.deps.broker.publish(event.scope, event.draft);
20165
20844
  this.sourceKeys.set(dedupeKey, true);
@@ -21098,12 +21777,24 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
21098
21777
  });
21099
21778
  }
21100
21779
  if (projectTaskComposition) app2.addHook("onClose", async () => projectTaskComposition.lease.release());
21101
- const taskThreadService = projectTaskComposition?.lease.runtime.threads ?? new ProjectTaskThreadService({ threads: threadStore, idFactory: import_node_crypto16.randomUUID });
21780
+ const taskThreadService = projectTaskComposition?.lease.runtime.threads ?? new ProjectTaskThreadService({ threads: threadStore, idFactory: import_node_crypto20.randomUUID });
21102
21781
  const taskLifecycle = projectTaskComposition?.lease.runtime.lifecycle ?? (0, import_core51.createTaskLifecycleService)({ taskStore, workItemStore: taskWorkItemStore });
21103
21782
  const reassignmentAuthorizer = (0, import_core51.createProjectTaskReassignmentAuthorizer)(
21104
21783
  new import_core51.ProjectTaskOwnerPolicy(projectBotMembershipStore),
21105
21784
  (input) => validateReassignmentThread(threadStore, input)
21106
21785
  );
21786
+ const coordinatorProvisioner = new RoomCoordinatorProvisioner({
21787
+ getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
21788
+ createAssistant: assistantStore.createAssistant.bind(assistantStore)
21789
+ });
21790
+ const publicRoomService = new WorkspacePublicRoomService({
21791
+ getProjectById: projectStore.getProjectById.bind(projectStore),
21792
+ createProject: projectStore.createProject.bind(projectStore),
21793
+ ensureMainRoom: projectRoomStore.ensureMainRoom.bind(projectRoomStore),
21794
+ findBotByAssistant: projectBotMembershipStore.findByAssistant.bind(projectBotMembershipStore),
21795
+ saveBot: projectBotMembershipStore.save.bind(projectBotMembershipStore),
21796
+ ensureCoordinator: (tenantId) => coordinatorProvisioner.ensure(tenantId)
21797
+ });
21107
21798
  registerProjectRoomRoutes(app2, { roomService, membershipService, messageService });
21108
21799
  registerProjectRosterRoutes(app2, { service: rosterService });
21109
21800
  registerCoSpaceRoutes(app2, {
@@ -21142,14 +21833,14 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
21142
21833
  findByAssistant: projectBotMembershipStore.findByAssistant.bind(projectBotMembershipStore),
21143
21834
  save: projectBotMembershipStore.save.bind(projectBotMembershipStore)
21144
21835
  },
21145
- coordinatorProvisioner: new RoomCoordinatorProvisioner({
21146
- getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
21147
- createAssistant: assistantStore.createAssistant.bind(assistantStore)
21148
- }),
21836
+ coordinatorProvisioner,
21149
21837
  rosterService: {
21150
21838
  addBot: async (actor2, input) => {
21151
21839
  await rosterService.add(actor2, input);
21152
21840
  }
21841
+ },
21842
+ publicRoom: {
21843
+ ensure: (tenantId, workspaceId) => publicRoomService.ensure(tenantId, workspaceId)
21153
21844
  }
21154
21845
  });
21155
21846
  registerCapabilityBundleRoutes(app2, new CapabilityBundleController({
@@ -21169,6 +21860,46 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
21169
21860
  })
21170
21861
  });
21171
21862
  registerAgentWebAppRuntimeRoutes(app2, {
21863
+ workspaceController: new WorkspaceController(createProjectCollaborationInitializer({
21864
+ membershipStore: projectMembershipStore,
21865
+ roomStore: projectRoomStore,
21866
+ userTenantLinkStore
21867
+ })),
21868
+ taskController: createTaskController({
21869
+ access: {
21870
+ requireProjectAccess: async (input) => {
21871
+ const apps = await webAppStore.list(input.tenantId);
21872
+ const allowed = apps.some((app3) => app3.status === "active" && app3.scope.allowedProjectIds.includes(input.projectId));
21873
+ if (!allowed) throw new ProjectCollaborationError("PROJECT_ACCESS_DENIED", 403, "Project is not exposed through this web app");
21874
+ const project = await projectStore.getProjectById(input.tenantId, input.projectId);
21875
+ if (!project) throw new ProjectCollaborationError("PROJECT_NOT_FOUND", 404, "Project not found");
21876
+ return {
21877
+ project,
21878
+ membership: {
21879
+ id: `webapp_guest_${input.projectId}_${input.userId}`,
21880
+ tenantId: input.tenantId,
21881
+ projectId: input.projectId,
21882
+ userId: input.userId,
21883
+ role: "member",
21884
+ status: "active",
21885
+ joinedAt: /* @__PURE__ */ new Date(),
21886
+ updatedAt: /* @__PURE__ */ new Date()
21887
+ }
21888
+ };
21889
+ }
21890
+ },
21891
+ tasks: taskStore,
21892
+ workItems: taskWorkItemStore,
21893
+ bots: projectBotMembershipStore,
21894
+ rooms: projectRoomStore,
21895
+ threadService: taskThreadService,
21896
+ reassignmentAuthorizer,
21897
+ lifecycle: taskLifecycle,
21898
+ projectMutationMutex: import_core51.projectMutationMutex,
21899
+ executionMutex: projectTaskComposition?.mutex ?? {
21900
+ runExclusive: (_scope, operation) => operation()
21901
+ }
21902
+ }),
21172
21903
  service: new AgentWebAppRuntimeService({
21173
21904
  webAppStore,
21174
21905
  threadStore: (0, import_core51.getStoreLattice)("default", "thread").store,
@@ -21176,7 +21907,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
21176
21907
  assistantResolver: {
21177
21908
  resolve: async (tenantId, assistantId) => {
21178
21909
  const stored = await assistantStore.getAssistantById(tenantId, assistantId);
21179
- if (stored) return stored;
21910
+ if (stored) return { id: stored.id, name: stored.name, description: stored.description };
21180
21911
  await import_core51.agentLatticeManager.initializeStoredAssistantsForTenant(tenantId);
21181
21912
  const config = import_core51.agentLatticeManager.getAgentConfigWithTenant(tenantId, assistantId);
21182
21913
  return config ? { id: assistantId, name: config.name, description: config.description } : null;
@@ -21590,7 +22321,7 @@ function tryCreateProjectTaskRuntime(input) {
21590
22321
  function createRuntime(input, stores, log2) {
21591
22322
  const controller = new AbortController();
21592
22323
  const mutex = new ProjectTaskExecutionMutex();
21593
- const threads = new ProjectTaskThreadService({ threads: input.threadStore, idFactory: import_node_crypto16.randomUUID });
22324
+ const threads = new ProjectTaskThreadService({ threads: input.threadStore, idFactory: import_node_crypto20.randomUUID });
21594
22325
  const lifecycle = (0, import_core51.createTaskLifecycleService)({ taskStore: input.taskStore, workItemStore: input.taskWorkItemStore });
21595
22326
  const projection = new RoomProjectionService({
21596
22327
  messages: input.projectRoomMessageStore,
@@ -21599,7 +22330,7 @@ function createRuntime(input, stores, log2) {
21599
22330
  warnings: input.taskWorkItemStore,
21600
22331
  log: log2,
21601
22332
  realtime: input.realtime,
21602
- idFactory: import_node_crypto16.randomUUID
22333
+ idFactory: import_node_crypto20.randomUUID
21603
22334
  });
21604
22335
  const agentFor = (task, thread) => import_core51.agentInstanceManager.getAgent({
21605
22336
  tenant_id: task.tenantId,