@axiom-lattice/gateway 4.1.2 → 4.1.4

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.mjs CHANGED
@@ -8750,6 +8750,16 @@ function verifySignedConsoleSession(authHeader, secret) {
8750
8750
  }
8751
8751
  }
8752
8752
  var PUBLIC_ROUTES = ["/api/auth/login", "/api/auth/register", "/health", "/api/stt/models"];
8753
+ function isStaleConsoleSessionCredential(authHeader) {
8754
+ if (!authHeader?.startsWith("Bearer ")) return false;
8755
+ const parts = authHeader.substring(7).split(".");
8756
+ if (parts.length !== 2 || !/^[A-Za-z0-9_-]{43}$/.test(parts[1])) return false;
8757
+ try {
8758
+ return isConsoleSessionPayload(JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8")));
8759
+ } catch {
8760
+ return false;
8761
+ }
8762
+ }
8753
8763
  function bypassesConsoleAuth(request) {
8754
8764
  const path4 = request.url.split("?")[0];
8755
8765
  const method = request.method;
@@ -8769,7 +8779,15 @@ function registerConsoleAuthHook(app2, options) {
8769
8779
  }
8770
8780
  if (bypassesConsoleAuth(request)) return;
8771
8781
  if (session) return;
8772
- if (!options.authRequired || request.method === "OPTIONS") return;
8782
+ if (!options.authRequired || request.method === "OPTIONS") {
8783
+ if (!PUBLIC_ROUTES.some((route) => request.url === route) && !request.url.startsWith("/s/") && isStaleConsoleSessionCredential(request.headers.authorization)) {
8784
+ await reply.status(401).send({
8785
+ success: false,
8786
+ error: "Unauthorized - Session token is invalid or expired. Please sign in again."
8787
+ });
8788
+ }
8789
+ return;
8790
+ }
8773
8791
  if (PUBLIC_ROUTES.some((route) => request.url === route)) return;
8774
8792
  if (request.url.startsWith("/s/")) return;
8775
8793
  const path4 = request.url.split("?")[0];
@@ -10948,14 +10966,6 @@ function header(request, name) {
10948
10966
  if (typeof value !== "string" || !value) throw new HttpTaskError(400, "INVALID_TASK_INPUT");
10949
10967
  return value;
10950
10968
  }
10951
- function assertHeaderScope(request, projectId, workspaceId) {
10952
- const requestedProject = header(request, "x-project-id");
10953
- const requestedWorkspace = header(request, "x-workspace-id");
10954
- const canonicalProject = projectId && projectId !== "default" ? projectId : void 0;
10955
- if (requestedProject && requestedProject !== "default" && requestedProject !== canonicalProject || requestedWorkspace !== void 0 && requestedWorkspace !== workspaceId) {
10956
- throw new HttpTaskError(409, "PROJECT_TASK_SCOPE_CONFLICT");
10957
- }
10958
- }
10959
10969
  function integer(value, minimum = 0) {
10960
10970
  if (value === void 0) return void 0;
10961
10971
  const parsed = typeof value === "number" ? value : typeof value === "string" && value ? Number(value) : NaN;
@@ -11210,27 +11220,50 @@ function createTaskController(deps) {
11210
11220
  }
11211
11221
  const metadata = query.metadata === void 0 ? void 0 : parseMetadata(query.metadata);
11212
11222
  if (!projectId || projectId === "default") {
11213
- assertHeaderScope(request);
11214
- const rows2 = await deps.tasks.list({
11223
+ const requestedWorkspace = header(request, "x-workspace-id");
11224
+ const status = typeof query.status === "string" ? query.status : void 0;
11225
+ const priority = typeof query.priority === "string" ? query.priority : void 0;
11226
+ const parentId = typeof query.parentId === "string" ? query.parentId : void 0;
11227
+ const ownerType = query.ownerType === "user" || query.ownerType === "agent" ? query.ownerType : void 0;
11228
+ const ownerId = typeof query.ownerId === "string" ? query.ownerId : void 0;
11229
+ const effectiveLimit = limit ?? 50;
11230
+ const effectiveOffset = offset ?? 0;
11231
+ const perQueryLimit = effectiveLimit + effectiveOffset;
11232
+ const queries = [{
11215
11233
  tenantId,
11216
11234
  ownerType: "user",
11217
11235
  ownerId: userId,
11218
11236
  projectId: null,
11219
- status: typeof query.status === "string" ? query.status : void 0,
11220
- priority: typeof query.priority === "string" ? query.priority : void 0,
11221
- parentId: typeof query.parentId === "string" ? query.parentId : void 0,
11237
+ status,
11238
+ priority,
11239
+ parentId,
11222
11240
  metadata,
11223
- limit,
11224
- offset
11225
- });
11241
+ limit: perQueryLimit,
11242
+ offset: 0
11243
+ }];
11244
+ if (requestedWorkspace && deps.readableProjects) {
11245
+ const readable = await deps.readableProjects({ tenantId, userId, workspaceId: requestedWorkspace });
11246
+ for (const project2 of readable) {
11247
+ queries.push({
11248
+ tenantId,
11249
+ workspaceId: project2.workspaceId,
11250
+ projectId: project2.id,
11251
+ status,
11252
+ priority,
11253
+ parentId,
11254
+ metadata,
11255
+ ownerType,
11256
+ ownerId,
11257
+ limit: perQueryLimit,
11258
+ offset: 0
11259
+ });
11260
+ }
11261
+ }
11262
+ const merged = (await Promise.all(queries.map((q) => deps.tasks.list(q)))).flat().sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime());
11263
+ const rows2 = merged.slice(effectiveOffset, effectiveOffset + effectiveLimit);
11226
11264
  return { success: true, data: rows2, count: rows2.length };
11227
11265
  }
11228
- const requestedProject = header(request, "x-project-id");
11229
- if (requestedProject && requestedProject !== "default" && requestedProject !== projectId) {
11230
- throw new HttpTaskError(409, "PROJECT_TASK_SCOPE_CONFLICT");
11231
- }
11232
11266
  const { project } = await authorize(request, projectId, "read");
11233
- assertHeaderScope(request, project.id, project.workspaceId);
11234
11267
  const rows = await deps.tasks.list({
11235
11268
  tenantId,
11236
11269
  workspaceId: project.workspaceId,
@@ -11250,11 +11283,6 @@ function createTaskController(deps) {
11250
11283
  await preauthorizeExplicitProject(request, authorize);
11251
11284
  const current = await deps.tasks.getById(getTenantId16(request), request.params.id);
11252
11285
  if (!current) return sendNotFound(reply);
11253
- try {
11254
- assertHeaderScope(request, current.projectId, current.workspaceId);
11255
- } catch {
11256
- throw notFound();
11257
- }
11258
11286
  await authorizeRead(request, current, authorize);
11259
11287
  return { success: true, data: current };
11260
11288
  });
@@ -11262,8 +11290,6 @@ function createTaskController(deps) {
11262
11290
  const body = snapshotCreate(request.body);
11263
11291
  const tenantId = getTenantId16(request);
11264
11292
  const userId = getUserId4(request);
11265
- const requestedProject = header(request, "x-project-id");
11266
- if (requestedProject && requestedProject !== "default" && requestedProject !== body.projectId) throw new HttpTaskError(409, "PROJECT_TASK_SCOPE_CONFLICT");
11267
11293
  const governed = governedCreate(body);
11268
11294
  if (governed) return reply.code(409).send({ success: false, code: governed });
11269
11295
  if (!body.projectId || body.projectId === "default") {
@@ -11273,11 +11299,8 @@ function createTaskController(deps) {
11273
11299
  }
11274
11300
  const capability2 = body.ownerType === "agent" ? "manage" : "send";
11275
11301
  const { project } = await authorize(request, body.projectId, capability2);
11276
- const requestedWorkspace = header(request, "x-workspace-id");
11277
- if (body.workspaceId !== void 0 && body.workspaceId !== project.workspaceId || requestedWorkspace !== void 0 && requestedWorkspace !== project.workspaceId) throw new HttpTaskError(409, "PROJECT_TASK_SCOPE_CONFLICT");
11278
11302
  return mutationMutex.runExclusive({ tenantId, projectId: project.id }, async () => {
11279
11303
  const lockedAccess = await authorize(request, project.id, capability2);
11280
- if (lockedAccess.project.workspaceId !== project.workspaceId) throw new HttpTaskError(409, "PROJECT_TASK_SCOPE_CONFLICT");
11281
11304
  const candidate = {
11282
11305
  id: body.id ?? "__new_task__",
11283
11306
  tenantId,
@@ -11295,13 +11318,8 @@ function createTaskController(deps) {
11295
11318
  let ownerType = "user";
11296
11319
  let ownerId = userId;
11297
11320
  if (body.ownerType === "agent") {
11298
- const target = await deps.bots.findByAssistant(tenantId, project.id, body.ownerId ?? "");
11299
- const room = await deps.rooms.getMainRoom(tenantId, project.id);
11300
- if (!target || !room || target.status !== "active" || target.workspaceId !== project.workspaceId || target.roomId !== room.id) {
11301
- return reply.code(409).send({ success: false, code: "PROJECT_TASK_OWNER_INACTIVE" });
11302
- }
11303
11321
  ownerType = "agent";
11304
- ownerId = target.assistantId;
11322
+ ownerId = body.ownerId ?? "";
11305
11323
  }
11306
11324
  const created = await deps.tasks.create({
11307
11325
  ...body,
@@ -11325,11 +11343,6 @@ function createTaskController(deps) {
11325
11343
  await preauthorizeExplicitProject(request, authorize);
11326
11344
  const current = await deps.tasks.getById(getTenantId16(request), request.params.id);
11327
11345
  if (!current) return sendNotFound(reply);
11328
- try {
11329
- assertHeaderScope(request, current.projectId, current.workspaceId);
11330
- } catch {
11331
- throw notFound();
11332
- }
11333
11346
  await authorizeMutation(request, current, authorize);
11334
11347
  return lockedTask(request, current, async (locked) => {
11335
11348
  const governed = governedUpdate(locked, body);
@@ -11348,11 +11361,6 @@ function createTaskController(deps) {
11348
11361
  await preauthorizeExplicitProject(request, authorize);
11349
11362
  const current = await deps.tasks.getById(getTenantId16(request), request.params.id);
11350
11363
  if (!current) return sendNotFound(reply);
11351
- try {
11352
- assertHeaderScope(request, current.projectId, current.workspaceId);
11353
- } catch {
11354
- throw notFound();
11355
- }
11356
11364
  await authorizeMutation(request, current, authorize);
11357
11365
  return lockedTask(request, current, async (locked) => {
11358
11366
  if (locked.ownerType === "agent") return reply.code(403).send({ success: false, code: "AGENT_RUNTIME_REQUIRED" });
@@ -11367,11 +11375,6 @@ function createTaskController(deps) {
11367
11375
  await preauthorizeExplicitProject(request, authorize);
11368
11376
  const current = await deps.tasks.getById(getTenantId16(request), request.params.id);
11369
11377
  if (!current) return sendNotFound(reply);
11370
- try {
11371
- assertHeaderScope(request, current.projectId, current.workspaceId);
11372
- } catch {
11373
- throw notFound();
11374
- }
11375
11378
  await authorizeMutation(request, current, authorize);
11376
11379
  return lockedTask(request, current, async (locked) => {
11377
11380
  if (locked.ownerType === "agent") return reply.code(403).send({ success: false, code: "AGENT_RUNTIME_REQUIRED" });
@@ -11389,11 +11392,6 @@ function createTaskController(deps) {
11389
11392
  await preauthorizeExplicitProject(request, authorize);
11390
11393
  const current = await deps.tasks.getById(getTenantId16(request), request.params.id);
11391
11394
  if (!current) return sendNotFound(reply);
11392
- try {
11393
- assertHeaderScope(request, current.projectId, current.workspaceId);
11394
- } catch {
11395
- throw notFound();
11396
- }
11397
11395
  await authorizeRead(request, current, authorize);
11398
11396
  const query = exactObject(request.query, ["action", "limit", "offset"]);
11399
11397
  const rows = await deps.workItems.list({
@@ -11411,8 +11409,6 @@ function createTaskController(deps) {
11411
11409
  const reassign = withErrors(async (request, reply) => {
11412
11410
  const body = snapshotReassign(request.body);
11413
11411
  const pathProject = request.params.projectId;
11414
- const requestedProject = header(request, "x-project-id");
11415
- if (requestedProject && requestedProject !== "default" && requestedProject !== pathProject) throw new HttpTaskError(409, "PROJECT_TASK_SCOPE_CONFLICT");
11416
11412
  const access = await authorize(request, pathProject, "manage");
11417
11413
  const snapshot = await deps.tasks.getById(getTenantId16(request), request.params.id);
11418
11414
  if (!snapshot || snapshot.projectId !== access.project.id || snapshot.workspaceId !== access.project.workspaceId || snapshot.ownerType !== "agent") throw notFound();
@@ -15471,6 +15467,7 @@ import {
15471
15467
  } from "@axiom-lattice/core";
15472
15468
  var SYSTEM_ACTOR2 = "system";
15473
15469
  var TERMINAL_ACTIONS = ["completed", "failed", "interrupted", "cancelled"];
15470
+ var MAX_LEARNING_ROUND_ANCESTORS = 8;
15474
15471
  var ProjectTaskObserver = class {
15475
15472
  /** @param deps Durable stores and Project Task runtime services. @param options Positive bounded scan limits. */
15476
15473
  constructor(deps, options) {
@@ -15628,6 +15625,7 @@ var ProjectTaskObserver = class {
15628
15625
  signal ?? (signal = this.deps.signal);
15629
15626
  if (signal?.aborted || this.disposed) return;
15630
15627
  if (!isProjectLifecycleTask4(task) || task.ownerType !== "agent") return;
15628
+ if (await this.belongsToLearningRound(task)) return;
15631
15629
  if (task.status === "pending") {
15632
15630
  if (await this.isReady(task) && !signal?.aborted && !this.disposed) await this.runStart(task, signal);
15633
15631
  return;
@@ -15640,6 +15638,22 @@ var ProjectTaskObserver = class {
15640
15638
  await this.resumeIfReady(task, signal);
15641
15639
  }
15642
15640
  }
15641
+ // SCOPE-COMPAT: learning rounds (metadata.trainingRound) and their subtrees execute in the
15642
+ // architect's builder thread (canvas CopilotPanel), not the Project Room runtime. Exempting
15643
+ // them here keeps admission hints, startup reconciliation, and dependent re-evaluation from
15644
+ // claiming these tasks — no roster warnings, no restart noise, and no double execution if
15645
+ // the architect later joins the room roster.
15646
+ async belongsToLearningRound(task) {
15647
+ let current = task;
15648
+ for (let depth = 0; current && depth < MAX_LEARNING_ROUND_ANCESTORS; depth += 1) {
15649
+ if (current.metadata?.trainingRound === true) return true;
15650
+ if (!current.parentId) return false;
15651
+ const parent = snapshotTaskItem(await this.deps.tasks.getById(current.tenantId, current.parentId));
15652
+ if (!parent || parent.id !== current.parentId || parent.tenantId !== current.tenantId) return false;
15653
+ current = parent;
15654
+ }
15655
+ return false;
15656
+ }
15643
15657
  /** Collects every bounded dependent ID before re-reading or scheduling any dependent. */
15644
15658
  async reevaluateDependents(task) {
15645
15659
  const { ids } = await this.collectDependentIds(task);
@@ -15745,6 +15759,7 @@ var ProjectTaskObserver = class {
15745
15759
  }
15746
15760
  async wakeParent(child) {
15747
15761
  if (!child.parentId) return;
15762
+ if (await this.belongsToLearningRound(child)) return;
15748
15763
  const parent = snapshotTaskItem(await this.deps.tasks.getById(child.tenantId, child.parentId));
15749
15764
  if (!parent || parent.id !== child.parentId || child.parentId !== parent.id || parent.ownerType !== "agent" || parent.workspaceId !== child.workspaceId || parent.projectId !== child.projectId || parent.status !== "interrupted" || !parent.dependencies?.includes(child.id)) return;
15750
15765
  const interruption = parseExternalDependency2(parent.context?.interruption);
@@ -16212,7 +16227,7 @@ var RoomProjectionService = class {
16212
16227
  * @returns Counts for inspected rows, projections, warnings, and bound truncation.
16213
16228
  */
16214
16229
  async repairProject(input) {
16215
- const result = { inspected: 0, projected: 0, warnings: 0, truncated: false };
16230
+ const result = { inspected: 0, projected: 0, warnings: 0, skipped: 0, truncated: false };
16216
16231
  let before;
16217
16232
  for (let page = 0; page < input.maxPages; page += 1) {
16218
16233
  const rows = await this.deps.events.listProjectLifecycleEvents({
@@ -16238,10 +16253,15 @@ var RoomProjectionService = class {
16238
16253
  if (safeIdentity) await this.warning("INVALID_PROJECTABLE_EVENT", safeIdentity, input);
16239
16254
  continue;
16240
16255
  }
16256
+ let stored;
16257
+ try {
16258
+ stored = await this.deps.tasks.getById(input.tenantId, row.taskId);
16259
+ } catch {
16260
+ stored = void 0;
16261
+ }
16241
16262
  let task;
16242
16263
  try {
16243
- const stored = await this.deps.tasks.getById(input.tenantId, row.taskId);
16244
- task = stored === null ? void 0 : snapshotProjectTask(stored);
16264
+ task = stored === null || stored === void 0 ? void 0 : snapshotTaskItem(stored);
16245
16265
  } catch {
16246
16266
  task = void 0;
16247
16267
  }
@@ -16250,6 +16270,10 @@ var RoomProjectionService = class {
16250
16270
  await this.warning("INVALID_PROJECTABLE_EVENT", { taskId: row.taskId, eventKey: row.eventKey }, input);
16251
16271
  continue;
16252
16272
  }
16273
+ if (task.ownerType !== "agent" || !isProjectLifecycleTask5(task)) {
16274
+ result.skipped += 1;
16275
+ continue;
16276
+ }
16253
16277
  const projected = await this.project({ task, room: input.room, workItem: row });
16254
16278
  if (projected.kind === "projected") result.projected += 1;
16255
16279
  else result.warnings += 1;
@@ -18052,6 +18076,18 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18052
18076
  projectMutationMutex: projectMutationMutex2,
18053
18077
  executionMutex: projectTaskComposition?.mutex ?? {
18054
18078
  runExclusive: (_scope, operation) => operation()
18079
+ },
18080
+ readableProjects: async ({ tenantId, userId, workspaceId }) => {
18081
+ const projects = await projectStore.getProjectsByWorkspace(tenantId, workspaceId);
18082
+ const readable = [];
18083
+ for (const project of projects) {
18084
+ try {
18085
+ await accessService.requireExistingProjectAccess({ tenantId, userId, projectId: project.id, capability: "read" });
18086
+ readable.push({ id: project.id, workspaceId: project.workspaceId });
18087
+ } catch {
18088
+ }
18089
+ }
18090
+ return readable;
18055
18091
  }
18056
18092
  }));
18057
18093
  app2.get(