@objectstack/runtime 14.8.0 → 15.1.0

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.cjs CHANGED
@@ -913,11 +913,12 @@ function collectBundleFunctions(bundle) {
913
913
  merge(bundle?.manifest?.functions);
914
914
  return out;
915
915
  }
916
- var import_core2, import_types, AppPlugin;
916
+ var import_core2, import_metadata_core, import_types, AppPlugin;
917
917
  var init_app_plugin = __esm({
918
918
  "src/app-plugin.ts"() {
919
919
  "use strict";
920
920
  import_core2 = require("@objectstack/core");
921
+ import_metadata_core = require("@objectstack/metadata-core");
921
922
  import_types = require("@objectstack/types");
922
923
  init_seed_loader();
923
924
  init_package_state_store();
@@ -940,6 +941,7 @@ var init_app_plugin = __esm({
940
941
  }
941
942
  const sys = this.bundle.manifest || this.bundle;
942
943
  const appId = sys.id || sys.name;
944
+ (0, import_metadata_core.assertProtocolCompat)(sys, void 0, (m) => ctx.logger.warn(`[AppPlugin] ${m}`));
943
945
  ctx.logger.info("Registering App Service", {
944
946
  appId,
945
947
  pluginName: this.name,
@@ -1056,6 +1058,10 @@ var init_app_plugin = __esm({
1056
1058
  const SECURITY_FIELDS = [
1057
1059
  ["positions", "position"],
1058
1060
  ["permissions", "permission"],
1061
+ // [ADR-0066 D1] Package-declared authorization capabilities —
1062
+ // read back by bootstrapDeclaredCapabilities to seed
1063
+ // sys_capability with package provenance.
1064
+ ["capabilities", "capability"],
1059
1065
  ["sharingRules", "sharing_rule"],
1060
1066
  ["policies", "policy"]
1061
1067
  ];
@@ -1383,6 +1389,18 @@ var init_app_plugin = __esm({
1383
1389
  const budget = new Promise((resolve) => {
1384
1390
  timer = setTimeout(() => resolve("budget"), seedBudgetMs);
1385
1391
  });
1392
+ const emitSeedSettled = (overBudget) => {
1393
+ const trigger = ctx.trigger;
1394
+ if (typeof trigger !== "function") return;
1395
+ try {
1396
+ const p = trigger.call(ctx, "app:seeded", { appId, overBudget });
1397
+ if (p && typeof p.catch === "function") {
1398
+ p.catch((err) => ctx.logger.debug("[Seeder] app:seeded trigger failed", { appId, error: err?.message ?? String(err) }));
1399
+ }
1400
+ } catch (err) {
1401
+ ctx.logger.debug("[Seeder] app:seeded trigger failed", { appId, error: err?.message ?? String(err) });
1402
+ }
1403
+ };
1386
1404
  const winner = await Promise.race([seedPromise.then(() => "done"), budget]);
1387
1405
  if (timer) clearTimeout(timer);
1388
1406
  if (winner === "budget") {
@@ -1391,7 +1409,9 @@ var init_app_plugin = __esm({
1391
1409
  );
1392
1410
  seedPromise.catch((err) => {
1393
1411
  ctx.logger.warn("[Seeder] Background seed failed after budget", { appId, error: err?.message ?? String(err) });
1394
- });
1412
+ }).then(() => emitSeedSettled(true));
1413
+ } else {
1414
+ emitSeedSettled(false);
1395
1415
  }
1396
1416
  }
1397
1417
  }
@@ -2161,6 +2181,7 @@ function safeGet(ctx, name) {
2161
2181
  var import_core5 = require("@objectstack/core");
2162
2182
  var import_types3 = require("@objectstack/types");
2163
2183
  var import_system = require("@objectstack/spec/system");
2184
+ var import_api = require("@objectstack/spec/api");
2164
2185
  var import_ai2 = require("@objectstack/spec/ai");
2165
2186
  var import_shared2 = require("@objectstack/spec/shared");
2166
2187
  init_package_state_store();
@@ -2173,6 +2194,9 @@ var ACTION_TO_API_METHOD = {
2173
2194
  delete: "delete",
2174
2195
  query: "list",
2175
2196
  find: "list",
2197
+ // Aggregation is a list-class read: an object whose whitelist excludes
2198
+ // `list` must not leak row statistics through GROUP BY either.
2199
+ aggregate: "list",
2176
2200
  batch: "bulk"
2177
2201
  };
2178
2202
  function checkApiExposure(def, action) {
@@ -2274,6 +2298,7 @@ async function resolveExecutionContext(opts) {
2274
2298
  if (authz.email) ctx.email = authz.email;
2275
2299
  if (authz.accessToken) ctx.accessToken = authz.accessToken;
2276
2300
  if (authz.tabPermissions) ctx.tabPermissions = authz.tabPermissions;
2301
+ if (authz.posture) ctx.posture = authz.posture;
2277
2302
  ctx.org_user_ids = authz.org_user_ids;
2278
2303
  if (oauthPrincipal && ctx.userId === oauthPrincipal.userId) {
2279
2304
  ctx.oauthScopes = oauthPrincipal.scopes;
@@ -2513,6 +2538,26 @@ var _HttpDispatcher = class _HttpDispatcher {
2513
2538
  }
2514
2539
  throw { statusCode: 503, message: "Data service not available" };
2515
2540
  }
2541
+ if (action === "aggregate") {
2542
+ if (!Array.isArray(params.aggregations) || params.aggregations.length === 0) {
2543
+ throw { statusCode: 400, message: "aggregate requires at least one aggregation" };
2544
+ }
2545
+ const engine = await this.getObjectQLService(scopeId) ?? await this.resolveService("objectql", scopeId).catch(() => null);
2546
+ if (engine && typeof engine.aggregate === "function") {
2547
+ const rows = await engine.aggregate(
2548
+ params.object,
2549
+ {
2550
+ ...params.where ? { where: params.where } : {},
2551
+ ...params.groupBy ? { groupBy: params.groupBy } : {},
2552
+ ...params.aggregations ? { aggregations: params.aggregations } : {},
2553
+ ...params.timezone ? { timezone: params.timezone } : {},
2554
+ ...executionContext ? { context: executionContext } : {}
2555
+ }
2556
+ );
2557
+ return { object: params.object, rows: rows ?? [] };
2558
+ }
2559
+ throw { statusCode: 503, message: "Data service not available" };
2560
+ }
2516
2561
  if (action === "batch") {
2517
2562
  return { object: params.object, results: [] };
2518
2563
  }
@@ -2796,31 +2841,45 @@ var _HttpDispatcher = class _HttpDispatcher {
2796
2841
  const res = await callData("get", { object, id }, driver, envId, ec);
2797
2842
  return res?.record ?? res ?? null;
2798
2843
  },
2844
+ aggregate: async (object, o) => {
2845
+ const res = await callData(
2846
+ "aggregate",
2847
+ {
2848
+ object,
2849
+ where: o?.where,
2850
+ groupBy: o?.groupBy,
2851
+ aggregations: o?.aggregations,
2852
+ timezone: o?.timezone
2853
+ },
2854
+ void 0,
2855
+ envId,
2856
+ ec
2857
+ );
2858
+ return res?.rows ?? [];
2859
+ },
2799
2860
  create: async (object, data) => await callData("create", { object, data }, driver, envId, ec),
2800
2861
  update: async (object, id, data) => await callData("update", { object, id, data }, driver, envId, ec),
2801
2862
  remove: async (object, id) => await callData("delete", { object, id }, driver, envId, ec),
2802
2863
  // ── Business-action surface (McpActionBridge) ──────────────
2803
2864
  // Resolution + dispatch flow through the framework's own action
2804
- // mechanism (engine.executeAction / automation flow runner) bound to
2805
- // THIS request's ExecutionContextthe same permission + RLS path
2806
- // the REST `/actions/...` route uses. No `@objectstack/service-ai`.
2865
+ // mechanism (engine.executeAction / automation flow runner). All
2866
+ // gating is at INVOKE time `ai.exposed` (author opt-in, #2849) +
2867
+ // the ADR-0066 D4 capability gate + record load under the caller's
2868
+ // RLS. Script/body handlers then run TRUSTED (see
2869
+ // buildActionEngineFacade); flows honour `runAs` with the caller's
2870
+ // identity forwarded. No `@objectstack/service-ai`.
2807
2871
  listActions: async () => {
2808
2872
  const meta = await getMeta();
2809
- const objs = await meta?.listObjects?.() ?? [];
2810
2873
  const hasAutomation = Boolean(
2811
2874
  await this.resolveService("automation", envId).catch(() => null)
2812
2875
  );
2813
2876
  const out = [];
2814
- for (const obj of objs) {
2815
- const objectName = obj?.name;
2877
+ for (const { action, objectName, obj } of await this.collectActionDeclarations(meta)) {
2816
2878
  if (!objectName || isSystemObjectName(objectName)) continue;
2817
- const actions = Array.isArray(obj?.actions) ? obj.actions : [];
2818
- for (const action of actions) {
2819
- if (!action || typeof action.name !== "string") continue;
2820
- if (!this.isHeadlessInvokableAction(action, hasAutomation)) continue;
2821
- if (this.actionPermissionError(action, ec)) continue;
2822
- out.push(this.summarizeAction(action, obj, objectName));
2823
- }
2879
+ if (!this.isHeadlessInvokableAction(action, hasAutomation)) continue;
2880
+ if (this.actionAiExposureError(action)) continue;
2881
+ if (this.actionPermissionError(action, ec)) continue;
2882
+ out.push(this.summarizeAction(action, obj, objectName));
2824
2883
  }
2825
2884
  return out;
2826
2885
  },
@@ -2846,6 +2905,23 @@ var _HttpDispatcher = class _HttpDispatcher {
2846
2905
  const on = objectName ? ` on '${objectName}'` : "";
2847
2906
  return `Action '${actionDef?.name ?? "unknown"}'${on} requires capability [${required.join(", ")}] \u2014 caller is missing [${missing.join(", ")}]`;
2848
2907
  }
2908
+ /**
2909
+ * [#2849 / ADR-0011] AI-exposure gate for the MCP action surface. Returns a
2910
+ * human-readable error string unless the action's author explicitly opted it
2911
+ * into the AI surface with `ai.exposed: true`, or `null` when exposed.
2912
+ *
2913
+ * This gate is the REAL agent-facing boundary for actions: script/body
2914
+ * handlers execute as TRUSTED application code (the engine facade carries no
2915
+ * ExecutionContext — see {@link buildActionEngineFacade}), so once invoked, a
2916
+ * body's reads/writes are NOT bounded by the caller's RLS/FLS or an agent's
2917
+ * data ceiling (ADR-0090 D10). The author's explicit opt-in — not a data-layer
2918
+ * backstop — therefore decides what AI may trigger. Fail-closed by default.
2919
+ */
2920
+ actionAiExposureError(actionDef, objectName) {
2921
+ if (actionDef?.ai?.exposed === true) return null;
2922
+ const on = objectName ? ` on '${objectName}'` : "";
2923
+ return `Action '${actionDef?.name ?? "unknown"}'${on} is not exposed to AI \u2014 the app author must opt it in with \`ai: { exposed: true, description: \u2026 }\``;
2924
+ }
2849
2925
  /**
2850
2926
  * Whether an action has a headless invocation path (so MCP can run it).
2851
2927
  * Mirrors the supported-type set of the (now cloud-side) action-tools
@@ -2928,7 +3004,18 @@ var _HttpDispatcher = class _HttpDispatcher {
2928
3004
  }
2929
3005
  return out;
2930
3006
  }
2931
- /** Slim engine facade matching the ActionContext.engine shape handlers expect. */
3007
+ /**
3008
+ * Slim engine facade matching the ActionContext.engine shape handlers expect.
3009
+ *
3010
+ * ⚠️ TRUSTED (SECURITY-DEFINER-like) BY DESIGN (#2849): these calls carry NO
3011
+ * ExecutionContext, so the data engine's security middleware skips RLS / FLS /
3012
+ * CRUD / tenant scoping entirely. Action bodies are the app author's own code
3013
+ * and legitimately perform cross-object writes the invoking user could not
3014
+ * (convert-lead, cascade-close). The boundary is therefore enforced at INVOKE
3015
+ * time (`ai.exposed` + ADR-0066 D4 capability gate), and every dispatch is
3016
+ * audit-logged. Longer-term direction: an action-level `runAs: 'user'|'system'`
3017
+ * mirroring flows (ADR-0049) — tracked in #2849.
3018
+ */
2932
3019
  buildActionEngineFacade(ql) {
2933
3020
  return {
2934
3021
  async insert(object, data) {
@@ -2956,11 +3043,19 @@ var _HttpDispatcher = class _HttpDispatcher {
2956
3043
  }
2957
3044
  /**
2958
3045
  * Resolve + invoke a business action by its declarative name for the MCP
2959
- * `run_action` tool. Enforces the ADR-0066 D4 capability gate and RLS as the
2960
- * caller, loads the subject record under RLS for row-context actions, and
2961
- * dispatches through the framework's `engine.executeAction` (script/body) or
2962
- * automation flow runner (flow). Throws on denial / not-found / handler
2963
- * failure so the tool surfaces a clean tool-error. No service-ai dependency.
3046
+ * `run_action` tool. Enforces the AI-exposure gate (`ai.exposed`, #2849), the
3047
+ * ADR-0066 D4 capability gate, loads the subject record under the caller's
3048
+ * RLS for row-context actions, and dispatches through the framework's
3049
+ * `engine.executeAction` (script/body) or automation flow runner (flow).
3050
+ * Throws on denial / not-found / handler failure so the tool surfaces a
3051
+ * clean tool-error. No service-ai dependency.
3052
+ *
3053
+ * SECURITY MODEL (#2849): all gating happens at INVOKE time. A script/body
3054
+ * handler then runs as trusted code — its engine facade performs
3055
+ * context-less reads/writes that bypass RLS/FLS (SECURITY-DEFINER-like), so
3056
+ * the caller's permissions and an agent's ADR-0090 D10 data ceiling do NOT
3057
+ * bound what the body does internally. Flow actions differ: the flow engine
3058
+ * receives the caller's identity below and honours `runAs` (ADR-0049).
2964
3059
  */
2965
3060
  async invokeBusinessAction(name, input, wiring) {
2966
3061
  const { driver, envId, ec, getMeta, callData } = wiring;
@@ -2983,6 +3078,8 @@ var _HttpDispatcher = class _HttpDispatcher {
2983
3078
  `Action '${name}' (type='${action?.type ?? "script"}') cannot be invoked via MCP`
2984
3079
  );
2985
3080
  }
3081
+ const exposureError = this.actionAiExposureError(action, objectName);
3082
+ if (exposureError) throw new Error(exposureError);
2986
3083
  const gateError = this.actionPermissionError(action, ec, objectName);
2987
3084
  if (gateError) throw new Error(gateError);
2988
3085
  let record = {};
@@ -3001,7 +3098,15 @@ var _HttpDispatcher = class _HttpDispatcher {
3001
3098
  throw new Error(`Action '${name}' is a flow but no automation service is available`);
3002
3099
  }
3003
3100
  const result = await automation.execute(action.target, {
3004
- triggerData: { record, params, user, action: action.name }
3101
+ record,
3102
+ ...objectName !== "global" ? { object: objectName } : {},
3103
+ userId: ec?.userId,
3104
+ ...Array.isArray(ec?.positions) && ec.positions.length ? { positions: ec.positions } : {},
3105
+ ...Array.isArray(ec?.permissions) && ec.permissions.length ? { permissions: ec.permissions } : {},
3106
+ ...ec?.tenantId ? { tenantId: ec.tenantId } : {},
3107
+ // Record fields seed flows' named `isInput` variables (like the
3108
+ // record-change trigger); explicit action params win on clash.
3109
+ params: { ...record, ...params }
3005
3110
  });
3006
3111
  if (result && typeof result === "object" && "success" in result && result.success === false) {
3007
3112
  throw new Error(`Flow '${action.target}' failed: ${result.error ?? "unknown error"}`);
@@ -3012,6 +3117,9 @@ var _HttpDispatcher = class _HttpDispatcher {
3012
3117
  if (!ql || typeof ql.executeAction !== "function") {
3013
3118
  throw new Error("Data engine not available for action dispatch");
3014
3119
  }
3120
+ console.info(
3121
+ `[action-audit] MCP run_action '${action.name}' on '${objectName}' \u2014 body executes TRUSTED (context-less engine, RLS/FLS-bypassing) for user '${ec?.userId ?? "anonymous"}'` + (ec?.principalKind === "agent" ? ` (AGENT on behalf of '${ec?.onBehalfOf?.userId ?? "unknown"}')` : "")
3122
+ );
3015
3123
  const actionContext = {
3016
3124
  record,
3017
3125
  user,
@@ -3042,24 +3150,82 @@ var _HttpDispatcher = class _HttpDispatcher {
3042
3150
  * and no `objectName` was supplied (so `run_action` can ask for one).
3043
3151
  */
3044
3152
  async resolveActionByName(meta, name, objectName) {
3153
+ const decls = await this.collectActionDeclarations(meta);
3045
3154
  if (objectName) {
3046
- const def = await meta?.getObject?.(objectName);
3047
- const action = Array.isArray(def?.actions) ? def.actions.find((a) => a?.name === name) : void 0;
3048
- return action ? { action, objectName } : null;
3049
- }
3050
- const objs = await meta?.listObjects?.() ?? [];
3051
- const matches = [];
3052
- for (const obj of objs) {
3053
- if (!obj?.name) continue;
3054
- const action = Array.isArray(obj?.actions) ? obj.actions.find((a) => a?.name === name) : void 0;
3055
- if (action) matches.push({ action, objectName: obj.name });
3155
+ const hit = decls.find((d) => d.objectName === objectName && d.action?.name === name);
3156
+ return hit ? { action: hit.action, objectName } : null;
3056
3157
  }
3158
+ const matches = decls.filter((d) => d.action?.name === name);
3057
3159
  if (matches.length === 0) return null;
3058
3160
  if (matches.length > 1) {
3059
3161
  const where = matches.map((m) => m.objectName).join(", ");
3060
3162
  throw new Error(`Action '${name}' exists on multiple objects (${where}); pass objectName to disambiguate`);
3061
3163
  }
3062
- return matches[0];
3164
+ return { action: matches[0].action, objectName: matches[0].objectName };
3165
+ }
3166
+ /**
3167
+ * The MCP surface's single declaration source: every action declaration the
3168
+ * bridge may list or invoke, as `{ action, objectName, obj }` rows.
3169
+ *
3170
+ * Two shapes feed it (#3010):
3171
+ * 1. `object.actions` — bundle/artifact objects and authored object rows.
3172
+ * 2. Standalone `action` metadata items — Studio-authored rows that the
3173
+ * engine executes since #2608 (`resyncAuthoredActions`) but that never
3174
+ * appear inside any object definition. Their owning object follows the
3175
+ * same convention as the engine registration key (`objectName` field,
3176
+ * legacy `object` field, else the `'global'` wildcard).
3177
+ *
3178
+ * On a key clash (`objectName:name`) the object-embedded declaration wins,
3179
+ * mirroring the execution layer's artifact-wins rule — `resyncAuthoredActions`
3180
+ * refuses to clobber an artifact-registered handler, so the embedded
3181
+ * declaration is the one that matches what actually runs. All MCP gating
3182
+ * (`ai.exposed`, ADR-0066 D4, headless-invokability) applies downstream of
3183
+ * this collection, unchanged.
3184
+ */
3185
+ async collectActionDeclarations(meta) {
3186
+ const objs = await meta?.listObjects?.() ?? [];
3187
+ const objByName = /* @__PURE__ */ new Map();
3188
+ for (const obj of objs) {
3189
+ if (typeof obj?.name === "string") objByName.set(obj.name, obj);
3190
+ }
3191
+ const out = [];
3192
+ const seen = /* @__PURE__ */ new Set();
3193
+ for (const obj of objs) {
3194
+ const objectName = obj?.name;
3195
+ if (!objectName) continue;
3196
+ for (const action of Array.isArray(obj?.actions) ? obj.actions : []) {
3197
+ if (!action || typeof action.name !== "string") continue;
3198
+ seen.add(`${objectName}:${action.name}`);
3199
+ out.push({ action, objectName, obj });
3200
+ }
3201
+ }
3202
+ let standalone = [];
3203
+ try {
3204
+ standalone = await meta?.loadMany?.("action") ?? [];
3205
+ } catch {
3206
+ standalone = [];
3207
+ }
3208
+ for (const action of standalone) {
3209
+ if (!action || typeof action.name !== "string") continue;
3210
+ const objectName = this.standaloneActionObjectName(action);
3211
+ const key = `${objectName}:${action.name}`;
3212
+ if (seen.has(key)) continue;
3213
+ seen.add(key);
3214
+ out.push({ action, objectName, obj: objByName.get(objectName) });
3215
+ }
3216
+ return out;
3217
+ }
3218
+ /**
3219
+ * Owning object of a standalone `action` item — must stay in lockstep with
3220
+ * the ObjectQL plugin's `actionObjectKey` (the engine registration key), so
3221
+ * the declaration the MCP surface resolves is the one whose handler
3222
+ * `executeAction` will find: spec `objectName`, bundle-collector `object`,
3223
+ * else the `'global'` wildcard.
3224
+ */
3225
+ standaloneActionObjectName(action) {
3226
+ if (typeof action?.objectName === "string" && action.objectName.length > 0) return action.objectName;
3227
+ if (typeof action?.object === "string" && action.object.length > 0) return action.object;
3228
+ return "global";
3063
3229
  }
3064
3230
  /**
3065
3231
  * Generate a `sys_api_key` and return the raw secret EXACTLY ONCE
@@ -3328,7 +3494,6 @@ var _HttpDispatcher = class _HttpDispatcher {
3328
3494
  const hasAuth = !!authSvc;
3329
3495
  const hasGraphQL = !!(graphqlSvc || this.kernel.graphql);
3330
3496
  const hasSearch = !!searchSvc;
3331
- const hasWebSockets = !!realtimeSvc;
3332
3497
  const hasFiles = !!filesSvc;
3333
3498
  const hasAnalytics = !!analyticsSvc;
3334
3499
  const hasWorkflow = !!workflowSvc;
@@ -3351,7 +3516,12 @@ var _HttpDispatcher = class _HttpDispatcher {
3351
3516
  analytics: hasAnalytics ? `${prefix}/analytics` : void 0,
3352
3517
  automation: hasAutomation ? `${prefix}/automation` : void 0,
3353
3518
  workflow: hasWorkflow ? `${prefix}/workflow` : void 0,
3354
- realtime: hasWebSockets ? `${prefix}/realtime` : void 0,
3519
+ // Never advertised (ADR-0076 D12, #2462): service-realtime is an
3520
+ // in-process pub/sub bus — the dispatcher has no /realtime branch
3521
+ // and no plugin mounts one, so an advertised route would 404.
3522
+ // Re-add only when a real HTTP/WS surface exists (and then it must
3523
+ // pass through the shouldDenyAnonymous gate, #2567).
3524
+ realtime: void 0,
3355
3525
  notifications: hasNotification ? `${prefix}/notifications` : void 0,
3356
3526
  ai: hasAi ? `${prefix}/ai` : void 0,
3357
3527
  i18n: hasI18n ? `${prefix}/i18n` : void 0,
@@ -3360,19 +3530,27 @@ var _HttpDispatcher = class _HttpDispatcher {
3360
3530
  // out. The objectui Integrations page reads this.
3361
3531
  mcp: _HttpDispatcher.isMcpEnabled() ? `${prefix}/mcp` : void 0
3362
3532
  };
3363
- const svcAvailable = (route, provider) => ({
3364
- enabled: true,
3365
- status: "available",
3366
- handlerReady: true,
3367
- route,
3368
- provider
3369
- });
3533
+ const svcAvailable = (route, provider, svc) => {
3534
+ const self = svc ? (0, import_api.readServiceSelfInfo)(svc) : void 0;
3535
+ if (self) {
3536
+ return {
3537
+ enabled: true,
3538
+ status: self.status,
3539
+ handlerReady: self.handlerReady ?? false,
3540
+ route,
3541
+ provider,
3542
+ message: self.message
3543
+ };
3544
+ }
3545
+ return { enabled: true, status: "available", handlerReady: true, route, provider };
3546
+ };
3370
3547
  const svcUnavailable = (name) => ({
3371
3548
  enabled: false,
3372
3549
  status: "unavailable",
3373
3550
  handlerReady: false,
3374
3551
  message: `Install a ${name} plugin to enable`
3375
3552
  });
3553
+ const realtimeSelf = realtimeSvc ? (0, import_api.readServiceSelfInfo)(realtimeSvc) : void 0;
3376
3554
  let locale = { default: "en", supported: ["en"], timezone: "UTC" };
3377
3555
  if (hasI18n && i18nSvc) {
3378
3556
  const defaultLocale = typeof i18nSvc.getDefaultLocale === "function" ? i18nSvc.getDefaultLocale() : "en";
@@ -3393,7 +3571,10 @@ var _HttpDispatcher = class _HttpDispatcher {
3393
3571
  features: {
3394
3572
  graphql: hasGraphQL,
3395
3573
  search: hasSearch,
3396
- websockets: hasWebSockets,
3574
+ // No WS/HTTP realtime surface is mounted anywhere — a mere
3575
+ // in-process realtime service must not advertise websockets
3576
+ // (ADR-0076 D12, #2462).
3577
+ websockets: false,
3397
3578
  files: hasFiles,
3398
3579
  analytics: hasAnalytics,
3399
3580
  ai: hasAi,
@@ -3406,21 +3587,31 @@ var _HttpDispatcher = class _HttpDispatcher {
3406
3587
  metadata: { enabled: true, status: "degraded", handlerReady: true, route: routes.metadata, provider: "kernel", message: "In-memory registry; DB persistence pending" },
3407
3588
  data: svcAvailable(routes.data, "kernel"),
3408
3589
  // Plugin-provided — only available when a plugin registers the service
3409
- auth: hasAuth ? svcAvailable(routes.auth) : svcUnavailable("auth"),
3410
- automation: hasAutomation ? svcAvailable(routes.automation) : svcUnavailable("automation"),
3411
- analytics: hasAnalytics ? svcAvailable(routes.analytics) : svcUnavailable("analytics"),
3412
- cache: hasCache ? svcAvailable() : svcUnavailable("cache"),
3413
- queue: hasQueue ? svcAvailable() : svcUnavailable("queue"),
3414
- job: hasJob ? svcAvailable() : svcUnavailable("job"),
3415
- ui: hasUi ? svcAvailable(routes.ui) : svcUnavailable("ui"),
3416
- workflow: hasWorkflow ? svcAvailable(routes.workflow) : svcUnavailable("workflow"),
3417
- realtime: hasWebSockets ? svcAvailable(routes.realtime) : svcUnavailable("realtime"),
3418
- notification: hasNotification ? svcAvailable(routes.notifications) : svcUnavailable("notification"),
3419
- ai: hasAi ? svcAvailable(routes.ai) : svcUnavailable("ai"),
3420
- i18n: hasI18n ? svcAvailable(routes.i18n) : svcUnavailable("i18n"),
3421
- graphql: hasGraphQL ? svcAvailable(routes.graphql) : svcUnavailable("graphql"),
3422
- "file-storage": hasFiles ? svcAvailable(routes.storage) : svcUnavailable("file-storage"),
3423
- search: hasSearch ? svcAvailable() : svcUnavailable("search")
3590
+ auth: hasAuth ? svcAvailable(routes.auth, void 0, authSvc) : svcUnavailable("auth"),
3591
+ automation: hasAutomation ? svcAvailable(routes.automation, void 0, automationSvc) : svcUnavailable("automation"),
3592
+ analytics: hasAnalytics ? svcAvailable(routes.analytics, void 0, analyticsSvc) : svcUnavailable("analytics"),
3593
+ cache: hasCache ? svcAvailable(void 0, void 0, cacheSvc) : svcUnavailable("cache"),
3594
+ queue: hasQueue ? svcAvailable(void 0, void 0, queueSvc) : svcUnavailable("queue"),
3595
+ job: hasJob ? svcAvailable(void 0, void 0, jobSvc) : svcUnavailable("job"),
3596
+ ui: hasUi ? svcAvailable(routes.ui, void 0, uiSvc) : svcUnavailable("ui"),
3597
+ workflow: hasWorkflow ? svcAvailable(routes.workflow, void 0, workflowSvc) : svcUnavailable("workflow"),
3598
+ // Honest entry (ADR-0076 D12, #2462): the registered realtime
3599
+ // service is an in-process event bus with NO mounted HTTP/WS
3600
+ // surface report it degraded with handlerReady:false (or as
3601
+ // the stub it declares itself to be), never as an available
3602
+ // HTTP capability with a route that would 404.
3603
+ realtime: realtimeSvc ? {
3604
+ enabled: true,
3605
+ status: realtimeSelf?.status ?? "degraded",
3606
+ handlerReady: false,
3607
+ message: realtimeSelf?.message ?? "In-process event bus only \u2014 no HTTP/WS realtime surface is mounted"
3608
+ } : svcUnavailable("realtime"),
3609
+ notification: hasNotification ? svcAvailable(routes.notifications, void 0, notificationSvc) : svcUnavailable("notification"),
3610
+ ai: hasAi ? svcAvailable(routes.ai, void 0, aiSvc) : svcUnavailable("ai"),
3611
+ i18n: hasI18n ? svcAvailable(routes.i18n, void 0, i18nSvc) : svcUnavailable("i18n"),
3612
+ graphql: hasGraphQL ? svcAvailable(routes.graphql, void 0, graphqlSvc) : svcUnavailable("graphql"),
3613
+ "file-storage": hasFiles ? svcAvailable(routes.storage, void 0, filesSvc) : svcUnavailable("file-storage"),
3614
+ search: hasSearch ? svcAvailable(void 0, void 0, searchSvc) : svcUnavailable("search")
3424
3615
  },
3425
3616
  locale
3426
3617
  };
@@ -3432,13 +3623,58 @@ var _HttpDispatcher = class _HttpDispatcher {
3432
3623
  if (!body || !body.query) {
3433
3624
  throw { statusCode: 400, message: "Missing query in request body" };
3434
3625
  }
3626
+ let ec = context.executionContext;
3627
+ if (!ec) {
3628
+ ec = await this.resolveRequestExecutionContext(context);
3629
+ if (ec) context.executionContext = ec;
3630
+ }
3631
+ if ((0, import_core5.shouldDenyAnonymous)({ requireAuth: this.requireAuth, userId: ec?.userId, isSystem: ec?.isSystem })) {
3632
+ throw {
3633
+ statusCode: import_core5.ANONYMOUS_DENY_STATUS,
3634
+ message: import_core5.ANONYMOUS_DENY_MESSAGE,
3635
+ code: import_core5.ANONYMOUS_DENY_CODE
3636
+ };
3637
+ }
3435
3638
  if (typeof this.kernel.graphql !== "function") {
3436
3639
  throw { statusCode: 501, message: "GraphQL service not available" };
3437
3640
  }
3438
3641
  return this.kernel.graphql(body.query, body.variables, {
3439
- request: context.request
3642
+ request: context.request,
3643
+ context: ec
3440
3644
  });
3441
3645
  }
3646
+ /**
3647
+ * Resolve the RBAC/RLS execution context for a request that did NOT flow
3648
+ * through {@link dispatch} (which resolves and caches it on
3649
+ * `context.executionContext`). The dispatcher-plugin's direct `/graphql`
3650
+ * route is the current caller. Mirrors the identity resolution `dispatch()`
3651
+ * performs so an anonymous-deny gate can tell an authenticated caller from
3652
+ * an anonymous one, and so the caller's identity can be THREADED to the
3653
+ * engine (#2992 / ADR-0096 D1) instead of dropped. Best-effort: returns
3654
+ * `undefined` on failure (treated as anonymous, i.e. denied under
3655
+ * `requireAuth`).
3656
+ */
3657
+ async resolveRequestExecutionContext(context) {
3658
+ try {
3659
+ return await resolveExecutionContext({
3660
+ getService: (n) => this.resolveService(n, context.environmentId),
3661
+ getQl: async () => {
3662
+ const k = this.kernel;
3663
+ if (k && typeof k.getServiceAsync === "function") {
3664
+ const ql = await k.getServiceAsync("objectql").catch(() => void 0);
3665
+ if (ql && (ql.registry || typeof ql.find === "function")) return ql;
3666
+ }
3667
+ return this.getObjectQLService(context.environmentId);
3668
+ },
3669
+ request: context.request,
3670
+ // OAuth access tokens are honoured only on the MCP surface
3671
+ // (#2698); GraphQL enforces no per-scope tool gating.
3672
+ acceptOAuthAccessToken: false
3673
+ });
3674
+ } catch {
3675
+ return void 0;
3676
+ }
3677
+ }
3442
3678
  /**
3443
3679
  * Handles Auth requests
3444
3680
  * path: sub-path after /auth/
@@ -3506,12 +3742,12 @@ var _HttpDispatcher = class _HttpDispatcher {
3506
3742
  * Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
3507
3743
  */
3508
3744
  async handleMetadata(path, _context, method, body, query) {
3509
- if (this.requireAuth) {
3745
+ {
3510
3746
  const ec = _context.executionContext;
3511
- if (!ec?.userId && !ec?.isSystem) {
3747
+ if ((0, import_core5.shouldDenyAnonymous)({ requireAuth: this.requireAuth, userId: ec?.userId, isSystem: ec?.isSystem })) {
3512
3748
  return {
3513
3749
  handled: true,
3514
- response: this.error("Authentication is required to access this endpoint.", 401, { code: "unauthenticated" })
3750
+ response: this.error(import_core5.ANONYMOUS_DENY_MESSAGE, import_core5.ANONYMOUS_DENY_STATUS, { code: import_core5.ANONYMOUS_DENY_CODE })
3515
3751
  };
3516
3752
  }
3517
3753
  }
@@ -3893,8 +4129,11 @@ var _HttpDispatcher = class _HttpDispatcher {
3893
4129
  return { handled: true, response: this.error("Security service not available", 503) };
3894
4130
  }
3895
4131
  const ec = context.executionContext;
3896
- if (!ec?.userId && !ec?.isSystem) {
3897
- return { handled: true, response: this.error("Authentication required", 401) };
4132
+ if ((0, import_core5.shouldDenyAnonymous)({ requireAuth: true, userId: ec?.userId, isSystem: ec?.isSystem })) {
4133
+ return {
4134
+ handled: true,
4135
+ response: this.error(import_core5.ANONYMOUS_DENY_MESSAGE, import_core5.ANONYMOUS_DENY_STATUS, { code: import_core5.ANONYMOUS_DENY_CODE })
4136
+ };
3898
4137
  }
3899
4138
  const m = method.toUpperCase();
3900
4139
  const parts = path.split("/").filter(Boolean);
@@ -4017,6 +4256,17 @@ var _HttpDispatcher = class _HttpDispatcher {
4017
4256
  }
4018
4257
  if (parts.length === 0 && m === "POST") {
4019
4258
  const manifest = body.manifest || body;
4259
+ const pkgId = typeof manifest?.id === "string" ? manifest.id.trim() : "";
4260
+ if (!pkgId) {
4261
+ return { handled: true, response: this.error("Package id is required", 400) };
4262
+ }
4263
+ const overwrite = body?.overwrite === true || query?.overwrite === "true" || query?.overwrite === true;
4264
+ if (!overwrite && registry.getPackage(pkgId)) {
4265
+ return {
4266
+ handled: true,
4267
+ response: this.error(`Package '${pkgId}' already exists`, 409)
4268
+ };
4269
+ }
4020
4270
  let pkg;
4021
4271
  const protocolSvc = await this.resolveService("protocol").catch(() => null);
4022
4272
  if (protocolSvc && typeof protocolSvc.installPackage === "function") {
@@ -4944,6 +5194,9 @@ var _HttpDispatcher = class _HttpDispatcher {
4944
5194
  engine: engineFacade,
4945
5195
  params: { ...reqParams, recordId, objectName }
4946
5196
  };
5197
+ console.info(
5198
+ `[action-audit] REST action '${objectName}/${actionName}' \u2014 body executes TRUSTED (context-less engine, RLS/FLS-bypassing) for user '${userFromAuth.id}'`
5199
+ );
4947
5200
  try {
4948
5201
  let result;
4949
5202
  try {
@@ -5016,12 +5269,12 @@ var _HttpDispatcher = class _HttpDispatcher {
5016
5269
  if (route.method !== method) continue;
5017
5270
  const params = matchRoute(route.path, fullPath);
5018
5271
  if (params === null) continue;
5019
- if (this.requireAuth && route.auth !== false) {
5272
+ if (route.auth !== false) {
5020
5273
  const gec = context.executionContext;
5021
- if (!gec?.userId && !gec?.isSystem) {
5274
+ if ((0, import_core5.shouldDenyAnonymous)({ requireAuth: this.requireAuth, userId: gec?.userId, isSystem: gec?.isSystem })) {
5022
5275
  return {
5023
5276
  handled: true,
5024
- response: this.error("Authentication is required to access this endpoint.", 401, { code: "unauthenticated" })
5277
+ response: this.error(import_core5.ANONYMOUS_DENY_MESSAGE, import_core5.ANONYMOUS_DENY_STATUS, { code: import_core5.ANONYMOUS_DENY_CODE })
5025
5278
  };
5026
5279
  }
5027
5280
  }
@@ -5951,7 +6204,12 @@ function createDispatcherPlugin(config = {}) {
5951
6204
  if (!server) return;
5952
6205
  const kernel = ctx.getKernel();
5953
6206
  const enforceMembership = config.enforceProjectMembership ?? (config.scoping?.enableProjectScoping ?? false);
5954
- const requireAuth = config.requireAuth ?? config.scoping?.requireAuth ?? false;
6207
+ const requireAuth = config.requireAuth ?? config.scoping?.requireAuth ?? true;
6208
+ if (!requireAuth) {
6209
+ ctx.logger?.warn?.(
6210
+ "[dispatcher] requireAuth is OFF \u2014 /graphql, /ai and the /meta catch-all serve anonymous callers. This is a deliberate opt-out; set api.requireAuth=true to deny anonymous access (ADR-0056 D2, #2567)."
6211
+ );
6212
+ }
5955
6213
  const dispatcher = new HttpDispatcher(kernel, void 0, {
5956
6214
  enforceProjectMembership: enforceMembership,
5957
6215
  requireAuth
@@ -6138,6 +6396,14 @@ function createDispatcherPlugin(config = {}) {
6138
6396
  errorResponse(err, res);
6139
6397
  }
6140
6398
  });
6399
+ server.patch(`${prefix}/packages/:id`, async (req, res) => {
6400
+ try {
6401
+ const result = await dispatcher.handlePackages(`/${req.params.id}`, "PATCH", req.body, req.query, { request: req });
6402
+ sendResult(result, res);
6403
+ } catch (err) {
6404
+ errorResponse(err, res);
6405
+ }
6406
+ });
6141
6407
  server.patch(`${prefix}/packages/:id/enable`, async (req, res) => {
6142
6408
  try {
6143
6409
  const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, "PATCH", {}, {}, { request: req });