forge-openclaw-plugin 0.2.43 → 0.2.45

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.
@@ -56,7 +56,7 @@ import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, cre
56
56
  import { createQuestionnaireInstrumentSchema, publishQuestionnaireVersionSchema, startQuestionnaireRunSchema, updateQuestionnaireRunSchema, updateQuestionnaireVersionSchema } from "./questionnaire-types.js";
57
57
  import { createPreferenceCatalogItemSchema, createPreferenceCatalogSchema, createPreferenceContextSchema, createPreferenceItemSchema, enqueueEntityPreferenceItemSchema, mergePreferenceContextsSchema, preferenceWorkspaceQuerySchema, startPreferenceGameSchema, submitAbsoluteSignalSchema, submitPairwiseJudgmentSchema, updatePreferenceCatalogItemSchema, updatePreferenceCatalogSchema, updatePreferenceContextSchema, updatePreferenceItemSchema, updatePreferenceScoreSchema } from "./preferences-types.js";
58
58
  import { createDataBackupSchema, dataExportQuerySchema, restoreDataBackupSchema, switchDataRootSchema, updateDataManagementSettingsSchema } from "./data-management-types.js";
59
- import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentRuntimeSessionEventSchema, createAgentRuntimeSessionSchema, createAgentTokenSchema, createAiConnectorSchema, createAiProcessorLinkSchema, createAiProcessorSchema, runAiConnectorSchema, writeSurfaceLayoutSchema, upsertAiModelConnectionSchema, testAiModelConnectionSchema, submitOpenAiCodexOauthManualCodeSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, createDiagnosticLogSchema, discoverCalendarConnectionSchema, startGoogleCalendarOauthSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, psycheObservationCalendarExportQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, diagnosticLogListQuerySchema, disconnectAgentRuntimeSessionSchema, eventsListQuerySchema, heartbeatAgentRuntimeSessionSchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, reconnectAgentRuntimeSessionSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskSplitCreateSchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, lifeForceProfilePatchSchema, lifeForceTemplateUpdateSchema, fatigueSignalCreateSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, updateAiConnectorSchema, updateAiProcessorSchema, runAiProcessorSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
59
+ import { activityListQuerySchema, activitySourceSchema, defaultAgentBootstrapPolicy, defaultAgentScopePolicy, createAgentActionSchema, createAgentRuntimeSessionEventSchema, createAgentRuntimeSessionSchema, createAgentTokenSchema, createAiConnectorSchema, createAiProcessorLinkSchema, createAiProcessorSchema, runAiConnectorSchema, writeSurfaceLayoutSchema, upsertAiModelConnectionSchema, testAiModelConnectionSchema, submitOpenAiCodexOauthManualCodeSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, createDiagnosticLogSchema, discoverCalendarConnectionSchema, startGoogleCalendarOauthSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, psycheObservationCalendarExportQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, diagnosticLogListQuerySchema, disconnectAgentRuntimeSessionSchema, eventsListQuerySchema, heartbeatAgentRuntimeSessionSchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, reconnectAgentRuntimeSessionSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskSplitCreateSchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, lifeForceProfilePatchSchema, lifeForceTemplateUpdateSchema, fatigueSignalCreateSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, updateAiConnectorSchema, updateAiProcessorSchema, runAiProcessorSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
60
60
  import { buildOpenApiDocument } from "./openapi.js";
61
61
  import { registerWebRoutes } from "./web.js";
62
62
  import { createManagerRuntime } from "./managers/runtime.js";
@@ -1980,6 +1980,29 @@ function buildPreferredMutationPath(entityType) {
1980
1980
  return "Read-only surface.";
1981
1981
  }
1982
1982
  }
1983
+ function buildPreferredMutationTool(entityType) {
1984
+ if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
1985
+ return "forge_create_entities | forge_update_entities | forge_delete_entities | forge_search_entities";
1986
+ }
1987
+ switch (entityType) {
1988
+ case "wiki_page":
1989
+ return "forge_upsert_wiki_page";
1990
+ case "calendar_connection":
1991
+ return "forge_connect_calendar_provider | forge_sync_calendar_connection";
1992
+ case "task_run":
1993
+ return "forge_start_task_run | forge_heartbeat_task_run | forge_focus_task_run | forge_complete_task_run | forge_release_task_run";
1994
+ case "questionnaire_run":
1995
+ return "forge_start_questionnaire_run | forge_update_questionnaire_run | forge_complete_questionnaire_run";
1996
+ case "preference_judgment":
1997
+ return "forge_submit_preferences_judgment";
1998
+ case "preference_signal":
1999
+ return "forge_submit_preferences_signal";
2000
+ case "work_adjustment":
2001
+ return "forge_adjust_work_minutes";
2002
+ default:
2003
+ return null;
2004
+ }
2005
+ }
1983
2006
  function buildPreferredReadPath(entityType) {
1984
2007
  if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
1985
2008
  return AGENT_ONBOARDING_BATCH_ROUTE_BASES[entityType];
@@ -2024,11 +2047,10 @@ function enrichOnboardingEntityGuide(entry) {
2024
2047
  : null,
2025
2048
  preferredMutationPath: buildPreferredMutationPath(entry.entityType),
2026
2049
  preferredReadPath: buildPreferredReadPath(entry.entityType),
2027
- preferredMutationTool: classification === "batch_crud_entity"
2028
- ? "forge_create_entities | forge_update_entities | forge_delete_entities | forge_search_entities"
2029
- : classification === "specialized_domain_surface"
2050
+ preferredMutationTool: buildPreferredMutationTool(entry.entityType) ??
2051
+ (classification === "specialized_domain_surface"
2030
2052
  ? "Follow forge_get_agent_onboarding.entityRouteModel.specializedDomainSurfaces for the dedicated route family."
2031
- : null
2053
+ : null)
2032
2054
  };
2033
2055
  }
2034
2056
  const AGENT_ONBOARDING_ENTITY_CATALOG = [
@@ -2758,7 +2780,8 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
2758
2780
  "For specialized surfaces, ask what would make the answer or change useful before you ask route-shaped details such as provider, weekday, flow id, run id, or trip id.",
2759
2781
  "When the user already named a precise correction or review target, do not widen back out into a meta lane question. Confirm only the missing route-selecting detail and then act.",
2760
2782
  "Once the route family is clear, say it plainly enough that another agent could follow the same path without guessing.",
2761
- "For Movement specifically, treat missing-data corrections as user-defined overlay boxes unless the user is editing an already-recorded stay or trip. When the user already gave a clear instruction like 'that missing block was home', act after only the last ambiguity is resolved."
2783
+ "For Movement specifically, treat missing-data corrections as user-defined overlay boxes unless the user is editing an already-recorded stay or trip. When the user already gave a clear instruction like 'that missing block was home', act after only the last ambiguity is resolved.",
2784
+ "For meaning-bearing updates, especially in Psyche, briefly say what feels newly true before you ask for the one structural detail that still changes the save."
2762
2785
  ];
2763
2786
  const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
2764
2787
  {
@@ -2917,7 +2940,8 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
2917
2940
  "Confirm the task.",
2918
2941
  "Confirm the actor only if it is not already obvious.",
2919
2942
  "Ask whether the run should be planned or unlimited only if that changes the action.",
2920
- "Start the run instead of turning it into a longer intake."
2943
+ "Start the run instead of turning it into a longer intake.",
2944
+ "Use the dedicated task-run tool for start, heartbeat, focus, complete, and release work instead of bouncing to the UI or a generic web route."
2921
2945
  ]
2922
2946
  },
2923
2947
  {
@@ -3071,6 +3095,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3071
3095
  "Ask whether the focus is a stay, a trip, a place, a timeline window, or a selected span.",
3072
3096
  "Ask for the time window, place, or movement item that makes the question concrete.",
3073
3097
  "Ask what they are trying to notice, preserve, or answer through that movement context.",
3098
+ "Choose the dedicated day, month, all-time, timeline, places, trip-detail, or selection route once the question shape is clear.",
3074
3099
  "Skip the meta lane question when the user already named the exact correction or review target and only one ambiguity remains.",
3075
3100
  "If the request is filling a missing-data gap, use a user-defined movement box rather than a raw stay or trip patch.",
3076
3101
  "If the request is repairing already-saved movement data, use the repair route that matches the saved object instead of treating it like a missing span.",
@@ -3104,6 +3129,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3104
3129
  "Ask whether they need the flow contract, a run result, a published output, or a node result.",
3105
3130
  "Ask what the user is trying to learn, repair, or publish through that flow.",
3106
3131
  "If the user already named the flow and action clearly, skip the meta lane question and ask only for the missing run, node, or output scope.",
3132
+ "If the user wants a stable public input contract or published output, prefer those dedicated reads instead of detouring through run history first.",
3107
3133
  "If the user is still shaping a payload or edit, prefer flow detail or box catalog reads before asking for structured inputs.",
3108
3134
  "Prefer flow detail or published-output reads for stable contracts, and use run or node-result routes only when the user is asking about execution history or debugging.",
3109
3135
  "Route to the dedicated workbench route family once the execution lane is clear."
@@ -3853,8 +3879,66 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
3853
3879
  example: '{"taskRunId":"run_123","actor":"aurel","note":"Stopping for now; blocked on feedback","closeoutNote":{"contentMarkdown":"Blocked on feedback from design before I can continue."}}'
3854
3880
  }
3855
3881
  ];
3882
+ const VALIDATION_ROUTE_TOOL_MAP = {
3883
+ "POST /api/v1/entities/search": "forge_search_entities",
3884
+ "POST /api/v1/entities/create": "forge_create_entities",
3885
+ "POST /api/v1/entities/update": "forge_update_entities",
3886
+ "POST /api/v1/entities/delete": "forge_delete_entities",
3887
+ "POST /api/v1/entities/restore": "forge_restore_entities",
3888
+ "POST /api/v1/tasks/:id/runs": "forge_start_task_run",
3889
+ "POST /api/v1/task-runs/:id/heartbeat": "forge_heartbeat_task_run",
3890
+ "POST /api/v1/task-runs/:id/focus": "forge_focus_task_run",
3891
+ "POST /api/v1/task-runs/:id/complete": "forge_complete_task_run",
3892
+ "POST /api/v1/task-runs/:id/release": "forge_release_task_run",
3893
+ "POST /api/tasks/:id/runs": "forge_start_task_run",
3894
+ "POST /api/task-runs/:id/heartbeat": "forge_heartbeat_task_run",
3895
+ "POST /api/task-runs/:id/focus": "forge_focus_task_run",
3896
+ "POST /api/task-runs/:id/complete": "forge_complete_task_run",
3897
+ "POST /api/task-runs/:id/release": "forge_release_task_run"
3898
+ };
3899
+ function formatValidationSummary(issues) {
3900
+ return issues
3901
+ .slice(0, 3)
3902
+ .map((issue) => `${issue.path}: ${issue.message}`)
3903
+ .join("; ");
3904
+ }
3905
+ function buildValidationHelp(method, routeUrl, issues) {
3906
+ const route = routeUrl || "unknown_route";
3907
+ const toolName = VALIDATION_ROUTE_TOOL_MAP[`${method.toUpperCase()} ${route}`];
3908
+ const toolInput = toolName
3909
+ ? AGENT_ONBOARDING_TOOL_INPUT_CATALOG.find((entry) => entry.toolName === toolName)
3910
+ : undefined;
3911
+ return {
3912
+ route,
3913
+ validationSummary: formatValidationSummary(issues),
3914
+ ...(toolInput
3915
+ ? {
3916
+ expectedShape: {
3917
+ toolName: toolInput.toolName,
3918
+ inputShape: toolInput.inputShape,
3919
+ requiredFields: [...toolInput.requiredFields],
3920
+ example: toolInput.example,
3921
+ notes: [...toolInput.notes]
3922
+ }
3923
+ }
3924
+ : {
3925
+ expectedShape: {
3926
+ inputShape: "See details[] for the rejected fields on this route.",
3927
+ requiredFields: [],
3928
+ example: null,
3929
+ notes: [
3930
+ "Retry with only the documented top-level keys for this route.",
3931
+ "Omit optional fields instead of sending null unless the route contract explicitly allows null."
3932
+ ]
3933
+ }
3934
+ })
3935
+ };
3936
+ }
3856
3937
  function buildAgentOnboardingPayload(request) {
3857
3938
  const origin = getRequestOrigin(request);
3939
+ const auth = parseRequestAuth(request.headers);
3940
+ const effectiveBootstrapPolicy = auth.token?.bootstrapPolicy ?? defaultAgentBootstrapPolicy;
3941
+ const effectiveScopePolicy = auth.token?.scopePolicy ?? defaultAgentScopePolicy;
3858
3942
  return {
3859
3943
  forgeBaseUrl: origin,
3860
3944
  webAppUrl: `${origin}/forge/`,
@@ -3881,6 +3965,10 @@ function buildAgentOnboardingPayload(request) {
3881
3965
  recommendedTrustLevel: "trusted",
3882
3966
  recommendedAutonomyMode: "approval_required",
3883
3967
  recommendedApprovalMode: "approval_by_default",
3968
+ defaultBootstrapPolicy: defaultAgentBootstrapPolicy,
3969
+ effectiveBootstrapPolicy,
3970
+ defaultScopePolicy: defaultAgentScopePolicy,
3971
+ effectiveScopePolicy,
3884
3972
  authModes: {
3885
3973
  operatorSession: {
3886
3974
  label: "Quick connect",
@@ -3890,7 +3978,7 @@ function buildAgentOnboardingPayload(request) {
3890
3978
  },
3891
3979
  managedToken: {
3892
3980
  label: "Managed token",
3893
- summary: "Use a long-lived token when you want explicit scoped auth, remote non-Tailscale access, or durable agent credentials.",
3981
+ summary: "Use a long-lived token when you want explicit scoped auth, remote non-Tailscale access, durable agent credentials, a custom bootstrap budget, or default user/project/tag read boundaries per agent.",
3894
3982
  tokenRequired: true
3895
3983
  }
3896
3984
  },
@@ -4471,6 +4559,11 @@ function parseRequestAuth(headers) {
4471
4559
  const source = token ? "agent" : activity.source;
4472
4560
  return {
4473
4561
  token,
4562
+ scope: {
4563
+ userIds: token?.scopePolicy.userIds ?? [],
4564
+ projectIds: token?.scopePolicy.projectIds ?? [],
4565
+ tagIds: token?.scopePolicy.tagIds ?? []
4566
+ },
4474
4567
  actor,
4475
4568
  source,
4476
4569
  activity: {
@@ -4560,6 +4653,105 @@ function resolveScopedUserIds(query) {
4560
4653
  const unique = Array.from(new Set(values));
4561
4654
  return unique.length > 0 ? unique : undefined;
4562
4655
  }
4656
+ const EMPTY_SCOPED_USER_IDS = ["__forge_scope_none__"];
4657
+ function normalizeScopedUserIdsForReads(options) {
4658
+ const validUserIdSet = new Set(options.validUserIds);
4659
+ const validScopedUserIds = options.scope.userIds !== undefined
4660
+ ? options.scope.userIds.filter((userId) => validUserIdSet.has(userId))
4661
+ : undefined;
4662
+ const scopedUserIdsForReads = options.scope.enforceUserIds &&
4663
+ validScopedUserIds !== undefined &&
4664
+ validScopedUserIds.length === 0
4665
+ ? EMPTY_SCOPED_USER_IDS
4666
+ : validScopedUserIds && validScopedUserIds.length > 0
4667
+ ? validScopedUserIds
4668
+ : undefined;
4669
+ return {
4670
+ validScopedUserIds,
4671
+ scopedUserIdsForReads
4672
+ };
4673
+ }
4674
+ function resolveEffectiveReadScope(query, auth) {
4675
+ const requestedUserIds = resolveScopedUserIds(query);
4676
+ const tokenUserIds = auth.token?.scopePolicy.userIds ?? [];
4677
+ const effectiveUserIds = tokenUserIds.length > 0
4678
+ ? requestedUserIds
4679
+ ? requestedUserIds.filter((userId) => tokenUserIds.includes(userId))
4680
+ : [...tokenUserIds]
4681
+ : requestedUserIds;
4682
+ return {
4683
+ userIds: effectiveUserIds !== undefined ? Array.from(new Set(effectiveUserIds)) : undefined,
4684
+ enforceUserIds: tokenUserIds.length > 0,
4685
+ projectIds: auth.token?.scopePolicy.projectIds ?? [],
4686
+ tagIds: auth.token?.scopePolicy.tagIds ?? []
4687
+ };
4688
+ }
4689
+ function hasScopeFilters(scope) {
4690
+ return scope.projectIds.length > 0 || scope.tagIds.length > 0;
4691
+ }
4692
+ function intersects(values, allowed) {
4693
+ if (!values || values.length === 0) {
4694
+ return false;
4695
+ }
4696
+ return values.some((value) => allowed.includes(value));
4697
+ }
4698
+ function applyTaskScope(tasks, scope) {
4699
+ if (!hasScopeFilters(scope)) {
4700
+ return tasks;
4701
+ }
4702
+ return tasks.filter((task) => {
4703
+ if (scope.projectIds.length > 0 &&
4704
+ (!task.projectId || !scope.projectIds.includes(task.projectId))) {
4705
+ return false;
4706
+ }
4707
+ if (scope.tagIds.length > 0 && !intersects(task.tagIds, scope.tagIds)) {
4708
+ return false;
4709
+ }
4710
+ return true;
4711
+ });
4712
+ }
4713
+ function applyProjectScope(projects, scope) {
4714
+ if (!hasScopeFilters(scope)) {
4715
+ return projects;
4716
+ }
4717
+ return projects.filter((project) => {
4718
+ if (scope.projectIds.length > 0 &&
4719
+ !scope.projectIds.includes(project.id)) {
4720
+ return false;
4721
+ }
4722
+ if (scope.tagIds.length > 0 &&
4723
+ !intersects(project.tagIds ?? undefined, scope.tagIds)) {
4724
+ return false;
4725
+ }
4726
+ return true;
4727
+ });
4728
+ }
4729
+ function applyGoalScope(goals, scope, allowedGoalIds) {
4730
+ if (!hasScopeFilters(scope)) {
4731
+ return goals;
4732
+ }
4733
+ return goals.filter((goal) => (scope.tagIds.length > 0 &&
4734
+ intersects(goal.tagIds ?? undefined, scope.tagIds)) ||
4735
+ allowedGoalIds.has(goal.id));
4736
+ }
4737
+ function applyHabitScope(habits, scope, allowedGoalIds, allowedTaskIds) {
4738
+ if (!hasScopeFilters(scope)) {
4739
+ return habits;
4740
+ }
4741
+ return habits.filter((habit) => intersects(habit.linkedProjectIds, scope.projectIds) ||
4742
+ intersects(habit.linkedTaskIds, [...allowedTaskIds]) ||
4743
+ intersects(habit.linkedGoalIds, [...allowedGoalIds]));
4744
+ }
4745
+ function applyStrategyScope(strategies, scope, allowedGoalIds) {
4746
+ if (!hasScopeFilters(scope)) {
4747
+ return strategies;
4748
+ }
4749
+ return strategies.filter((strategy) => intersects(strategy.targetProjectIds, scope.projectIds) ||
4750
+ intersects(strategy.targetGoalIds, [...allowedGoalIds]) ||
4751
+ strategy.linkedEntities.some((link) => (link.entityType === "project" &&
4752
+ scope.projectIds.includes(link.entityId)) ||
4753
+ (link.entityType === "goal" && allowedGoalIds.has(link.entityId))));
4754
+ }
4563
4755
  function attachMovementTimelineSleepOverlays(movement, userIds) {
4564
4756
  const rangeStart = movement.segments.reduce((earliest, segment) => {
4565
4757
  if (!earliest || Date.parse(segment.startedAt) < Date.parse(earliest)) {
@@ -4606,18 +4798,26 @@ function syncEntityOwnerFromBody(options) {
4606
4798
  const owner = resolveUserForMutation(requestedUserId, options.fallbackLabel);
4607
4799
  setEntityOwner(options.entityType, options.entityId, owner.id);
4608
4800
  }
4609
- function buildV1Context(userIds) {
4801
+ function buildV1Context(scope = {
4802
+ userIds: undefined,
4803
+ enforceUserIds: false,
4804
+ projectIds: [],
4805
+ tagIds: []
4806
+ }) {
4610
4807
  const users = listUsers();
4611
- const validUserIdSet = new Set(users.map((user) => user.id));
4612
- const scopedUserIds = userIds && userIds.length > 0
4613
- ? userIds.filter((userId) => validUserIdSet.has(userId))
4614
- : undefined;
4615
- const goals = filterOwnedEntities("goal", listGoals(), scopedUserIds);
4616
- const tasks = filterOwnedEntities("task", listTasks(), scopedUserIds);
4617
- const habits = filterOwnedEntities("habit", listHabits(), scopedUserIds);
4618
- const dashboard = getDashboard({ userIds: scopedUserIds });
4619
- const selectedUsers = scopedUserIds && scopedUserIds.length > 0
4620
- ? users.filter((user) => scopedUserIds.includes(user.id))
4808
+ const { validScopedUserIds, scopedUserIdsForReads } = normalizeScopedUserIdsForReads({
4809
+ scope,
4810
+ validUserIds: users.map((user) => user.id)
4811
+ });
4812
+ const projects = applyProjectScope(listProjectSummaries({ userIds: scopedUserIdsForReads }), scope);
4813
+ const tasks = applyTaskScope(filterOwnedEntities("task", listTasks(), scopedUserIdsForReads), scope);
4814
+ const goals = applyGoalScope(filterOwnedEntities("goal", listGoals(), scopedUserIdsForReads), scope, new Set([...projects.map((project) => project.goalId), ...tasks.map((task) => task.goalId)]
4815
+ .filter((value) => typeof value === "string" && value.length > 0)));
4816
+ const habits = applyHabitScope(filterOwnedEntities("habit", listHabits(), scopedUserIdsForReads), scope, new Set(goals.map((goal) => goal.id)), new Set(tasks.map((task) => task.id)));
4817
+ const strategies = applyStrategyScope(listStrategies({ userIds: scopedUserIdsForReads }), scope, new Set(goals.map((goal) => goal.id)));
4818
+ const dashboard = getDashboard({ userIds: scopedUserIdsForReads });
4819
+ const selectedUsers = validScopedUserIds !== undefined
4820
+ ? users.filter((user) => validScopedUserIds.includes(user.id))
4621
4821
  : users;
4622
4822
  return {
4623
4823
  meta: {
@@ -4629,29 +4829,33 @@ function buildV1Context(userIds) {
4629
4829
  },
4630
4830
  metrics: buildGamificationProfile(goals, tasks, habits),
4631
4831
  dashboard,
4632
- overview: getOverviewContext(new Date(), { userIds: scopedUserIds }),
4633
- today: getTodayContext(new Date(), { userIds: scopedUserIds }),
4634
- risk: getRiskContext(new Date(), { userIds: scopedUserIds }),
4832
+ overview: getOverviewContext(new Date(), { userIds: scopedUserIdsForReads }),
4833
+ today: getTodayContext(new Date(), { userIds: scopedUserIdsForReads }),
4834
+ risk: getRiskContext(new Date(), { userIds: scopedUserIdsForReads }),
4635
4835
  goals,
4636
- projects: listProjectSummaries({ userIds: scopedUserIds }),
4836
+ projects,
4637
4837
  tags: listTags(),
4638
4838
  tasks,
4639
4839
  habits,
4640
4840
  users,
4641
- strategies: listStrategies({ userIds: scopedUserIds }),
4841
+ strategies,
4642
4842
  userScope: {
4643
- selectedUserIds: scopedUserIds ?? [],
4843
+ selectedUserIds: validScopedUserIds ?? [],
4644
4844
  selectedUsers
4645
4845
  },
4646
- activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
4846
+ activeTaskRuns: scope.enforceUserIds &&
4847
+ validScopedUserIds !== undefined &&
4848
+ validScopedUserIds.length === 0
4849
+ ? []
4850
+ : listTaskRuns({ active: true, limit: 25, userIds: scopedUserIdsForReads }),
4647
4851
  activity: dashboard.recentActivity,
4648
- lifeForce: buildLifeForcePayload(new Date(), scopedUserIds)
4852
+ lifeForce: buildLifeForcePayload(new Date(), scopedUserIdsForReads)
4649
4853
  };
4650
4854
  }
4651
- function buildXpMetricsPayload() {
4652
- const goals = listGoals();
4653
- const tasks = listTasks();
4654
- const habits = listHabits();
4855
+ function buildXpMetricsPayload(input = {}) {
4856
+ const goals = input.goals ?? listGoals();
4857
+ const tasks = input.tasks ?? listTasks();
4858
+ const habits = input.habits ?? listHabits();
4655
4859
  const rules = listRewardRules();
4656
4860
  const gamificationOverview = buildGamificationOverview(goals, tasks, habits);
4657
4861
  const dailyAmbientCap = rules
@@ -4710,13 +4914,26 @@ function describeWorkAdjustment(input) {
4710
4914
  : `${appliedLabel} ${direction} from the tracked work total.`
4711
4915
  };
4712
4916
  }
4713
- function buildOperatorContext(userIds) {
4714
- const tasks = filterOwnedEntities("task", listTasks(), userIds);
4715
- const dueHabits = filterOwnedEntities("habit", listHabits({ dueToday: true }), userIds).slice(0, 12);
4716
- const activeProjects = listProjectSummaries({
4917
+ function buildOperatorContext(scope = {
4918
+ userIds: undefined,
4919
+ enforceUserIds: false,
4920
+ projectIds: [],
4921
+ tagIds: []
4922
+ }) {
4923
+ const users = listUsers();
4924
+ const { scopedUserIdsForReads } = normalizeScopedUserIdsForReads({
4925
+ scope,
4926
+ validUserIds: users.map((user) => user.id)
4927
+ });
4928
+ const tasks = applyTaskScope(filterOwnedEntities("task", listTasks(), scopedUserIdsForReads), scope);
4929
+ const dueHabits = filterOwnedEntities("habit", listHabits({ dueToday: true }), scopedUserIdsForReads);
4930
+ const activeProjects = applyProjectScope(listProjectSummaries({
4717
4931
  status: "active",
4718
- userIds
4719
- }).filter((project) => project.activeTaskCount > 0 || project.completedTaskCount > 0);
4932
+ userIds: scopedUserIdsForReads
4933
+ }), scope).filter((project) => project.activeTaskCount > 0 || project.completedTaskCount > 0);
4934
+ const goals = applyGoalScope(filterOwnedEntities("goal", listGoals(), scopedUserIdsForReads), scope, new Set([...activeProjects.map((project) => project.goalId), ...tasks.map((task) => task.goalId)]
4935
+ .filter((value) => typeof value === "string" && value.length > 0)));
4936
+ const scopedHabits = applyHabitScope(dueHabits, scope, new Set(goals.map((goal) => goal.id)), new Set(tasks.map((task) => task.id))).slice(0, 12);
4720
4937
  const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
4721
4938
  const recommendedNextTask = focusTasks[0] ??
4722
4939
  tasks.find((task) => task.status === "backlog") ??
@@ -4726,7 +4943,7 @@ function buildOperatorContext(userIds) {
4726
4943
  generatedAt: new Date().toISOString(),
4727
4944
  activeProjects: activeProjects.slice(0, 8),
4728
4945
  focusTasks: focusTasks.slice(0, 12),
4729
- dueHabits,
4946
+ dueHabits: scopedHabits,
4730
4947
  currentBoard: {
4731
4948
  backlog: tasks.filter((task) => task.status === "backlog").slice(0, 20),
4732
4949
  focus: tasks.filter((task) => task.status === "focus").slice(0, 20),
@@ -4736,10 +4953,16 @@ function buildOperatorContext(userIds) {
4736
4953
  blocked: tasks.filter((task) => task.status === "blocked").slice(0, 20),
4737
4954
  done: tasks.filter((task) => task.status === "done").slice(0, 20)
4738
4955
  },
4739
- recentActivity: listActivityEvents({ limit: 20, userIds }),
4740
- recentTaskRuns: listTaskRuns({ limit: 12, userIds }),
4956
+ recentActivity: listActivityEvents({
4957
+ limit: 20,
4958
+ userIds: scopedUserIdsForReads
4959
+ }),
4960
+ recentTaskRuns: listTaskRuns({
4961
+ limit: 12,
4962
+ userIds: scopedUserIdsForReads
4963
+ }),
4741
4964
  recommendedNextTask,
4742
- xp: buildXpMetricsPayload()
4965
+ xp: buildXpMetricsPayload({ goals, tasks, habits: scopedHabits })
4743
4966
  };
4744
4967
  }
4745
4968
  function buildUserDirectoryPayload() {
@@ -4867,7 +5090,13 @@ function buildOperatorOverviewRouteGuide() {
4867
5090
  }
4868
5091
  function buildOperatorOverview(request) {
4869
5092
  const auth = parseRequestAuth(request.headers);
4870
- const userIds = resolveScopedUserIds(request.query);
5093
+ const readScope = resolveEffectiveReadScope(request.query, auth);
5094
+ const users = listUsers();
5095
+ const { scopedUserIdsForReads } = normalizeScopedUserIdsForReads({
5096
+ scope: readScope,
5097
+ validUserIds: users.map((user) => user.id)
5098
+ });
5099
+ const userIds = scopedUserIdsForReads;
4871
5100
  const canReadPsyche = auth.token
4872
5101
  ? hasTokenScope(auth.token, "psyche.read")
4873
5102
  : true;
@@ -4878,8 +5107,8 @@ function buildOperatorOverview(request) {
4878
5107
  ];
4879
5108
  return {
4880
5109
  generatedAt: new Date().toISOString(),
4881
- snapshot: buildV1Context(userIds),
4882
- operator: buildOperatorContext(userIds),
5110
+ snapshot: buildV1Context(readScope),
5111
+ operator: buildOperatorContext(readScope),
4883
5112
  sleep: getSleepViewData(userIds),
4884
5113
  domains: listDomains(),
4885
5114
  psyche: canReadPsyche ? getPsycheOverview(userIds) : null,
@@ -5075,6 +5304,10 @@ export async function buildServer(options = {}) {
5075
5304
  });
5076
5305
  app.setErrorHandler((error, request, reply) => {
5077
5306
  const validationIssues = error instanceof ZodError ? formatValidationIssues(error) : undefined;
5307
+ const routeUrl = request.routeOptions.url || request.url;
5308
+ const validationHelp = validationIssues
5309
+ ? buildValidationHelp(request.method, routeUrl, validationIssues)
5310
+ : undefined;
5078
5311
  const statusCode = isHttpError(error)
5079
5312
  ? error.statusCode
5080
5313
  : isManagerError(error)
@@ -5082,7 +5315,6 @@ export async function buildServer(options = {}) {
5082
5315
  : error instanceof ZodError
5083
5316
  ? 400
5084
5317
  : 500;
5085
- const routeUrl = request.routeOptions.url || request.url;
5086
5318
  if (!shouldSkipAutomaticDiagnosticRoute(routeUrl)) {
5087
5319
  try {
5088
5320
  recordDiagnosticLog({
@@ -5123,10 +5355,11 @@ export async function buildServer(options = {}) {
5123
5355
  ? "invalid_request"
5124
5356
  : "internal_error",
5125
5357
  error: validationIssues
5126
- ? "Request validation failed"
5358
+ ? `Request validation failed for ${request.method.toUpperCase()} ${routeUrl}. ${validationHelp?.validationSummary ?? ""}`.trim()
5127
5359
  : getErrorMessage(error),
5128
5360
  statusCode,
5129
5361
  ...(validationIssues ? { details: validationIssues } : {}),
5362
+ ...(validationHelp ?? {}),
5130
5363
  ...(isHttpError(error) && error.details ? error.details : {}),
5131
5364
  ...(isManagerError(error) && error.details ? error.details : {})
5132
5365
  });
@@ -5364,7 +5597,10 @@ export async function buildServer(options = {}) {
5364
5597
  revoked: managers.session.revokeCurrentSession(request.headers, reply)
5365
5598
  }));
5366
5599
  app.get("/api/v1/openapi.json", async () => buildOpenApiDocument());
5367
- app.get("/api/v1/context", async (request) => buildV1Context(resolveScopedUserIds(request.query)));
5600
+ app.get("/api/v1/context", async (request) => {
5601
+ const auth = authenticateRequest(request.headers);
5602
+ return buildV1Context(resolveEffectiveReadScope(request.query, auth));
5603
+ });
5368
5604
  app.get("/api/v1/life-force", async (request) => ({
5369
5605
  lifeForce: buildLifeForcePayload(new Date(), resolveScopedUserIds(request.query)),
5370
5606
  templates: listLifeForceTemplates(resolveLifeForceUser(resolveScopedUserIds(request.query)).id)
@@ -6021,16 +6257,15 @@ export async function buildServer(options = {}) {
6021
6257
  return { sleep };
6022
6258
  });
6023
6259
  app.get("/api/v1/operator/context", async (request) => {
6024
- requireOperatorSession(request.headers, {
6260
+ const auth = requireAuthenticatedActor(request.headers, {
6025
6261
  route: "/api/v1/operator/context"
6026
6262
  });
6027
- const userIds = resolveScopedUserIds(request.query);
6028
6263
  return {
6029
- context: buildOperatorContext(userIds)
6264
+ context: buildOperatorContext(resolveEffectiveReadScope(request.query, auth))
6030
6265
  };
6031
6266
  });
6032
6267
  app.get("/api/v1/operator/overview", async (request) => {
6033
- requireOperatorSession(request.headers, {
6268
+ requireAuthenticatedActor(request.headers, {
6034
6269
  route: "/api/v1/operator/overview"
6035
6270
  });
6036
6271
  return {
@@ -39,6 +39,11 @@ export class AuthenticationManager extends AbstractManager {
39
39
  actor,
40
40
  source,
41
41
  token,
42
+ scope: {
43
+ userIds: token?.scopePolicy.userIds ?? [],
44
+ projectIds: token?.scopePolicy.projectIds ?? [],
45
+ tagIds: token?.scopePolicy.tagIds ?? []
46
+ },
42
47
  session
43
48
  };
44
49
  }
@@ -51,6 +51,11 @@ export class SessionManager extends AbstractAuditedManager {
51
51
  host: null,
52
52
  ip: null,
53
53
  token: null,
54
+ scope: {
55
+ userIds: [],
56
+ projectIds: [],
57
+ tagIds: []
58
+ },
54
59
  session
55
60
  }, {
56
61
  actorLabel
@@ -2780,10 +2780,9 @@ function updateMovementBoxOverrideState(id, input) {
2780
2780
  overridden_user_box_ids_json = ?,
2781
2781
  override_ranges_json = ?,
2782
2782
  is_overridden = ?,
2783
- is_fully_hidden = ?,
2784
- updated_at = ?
2783
+ is_fully_hidden = ?
2785
2784
  WHERE id = ?`)
2786
- .run(input.overrideCount, JSON.stringify(input.overriddenAutomaticBoxIds), input.trueStartedAt, input.trueEndedAt, input.overriddenStartedAt, input.overriddenEndedAt, input.overriddenByBoxId, JSON.stringify(input.overriddenUserBoxIds), JSON.stringify(input.overrideRanges), input.isOverridden ? 1 : 0, input.isFullyHidden ? 1 : 0, nowIso(), id);
2785
+ .run(input.overrideCount, JSON.stringify(input.overriddenAutomaticBoxIds), input.trueStartedAt, input.trueEndedAt, input.overriddenStartedAt, input.overriddenEndedAt, input.overriddenByBoxId, JSON.stringify(input.overriddenUserBoxIds), JSON.stringify(input.overrideRanges), input.isOverridden ? 1 : 0, input.isFullyHidden ? 1 : 0, id);
2787
2786
  }
2788
2787
  function recomputeMovementBoxOverrideState(userId) {
2789
2788
  const rows = listMovementBoxRows({ userIds: [userId] });