@agentproto/runtime 5.10.0 → 5.11.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.mjs CHANGED
@@ -30,7 +30,7 @@ import { APP_UI_DISCOVERY_TOOLS, RUNNER_SELECT_SCRIPT } from '@agentproto/app-cl
30
30
  import { DISPLAY_MODE_SCRIPT } from '@agentproto/app-client/display-mode';
31
31
  import { defineTool, catchErrors, ToolError, pageParamsShape, paginated, toToolResult, paginate, toolText } from '@agentproto/tool';
32
32
  import { toMcpTool, createMcpServer, registerBuiltinTool, registerUiResource, registerEventsMethods } from '@agentproto/mcp-server';
33
- import { SESSION_CHAT_APP_ID, liveSessionApp, makeLiveSessionApp, sessionsPanelApp, agentsOverviewApp, bureauSessionsApp, sessionStoryApp, workBoardApp, reviewPanelApp, makeSessionsPanelApp, makeAgentsOverviewApp, makeBureauSessionsApp, makeSessionStoryPanelApp, makeSessionChatApp, makeWorkBoardApp, makeReviewPanelApp, sessionChatApp } from '@agentproto/apps';
33
+ import { storeApp, SESSION_CHAT_APP_ID, liveSessionApp, makeLiveSessionApp, sessionsPanelApp, agentsOverviewApp, bureauSessionsApp, sessionStoryApp, workBoardApp, reviewPanelApp, makeSessionsPanelApp, makeAgentsOverviewApp, makeBureauSessionsApp, makeSessionStoryPanelApp, makeSessionChatApp, makeWorkBoardApp, makeReviewPanelApp, makeStoreApp, sessionChatApp } from '@agentproto/apps';
34
34
  import { EventEmitter } from 'events';
35
35
  import { inferLegacyModeKind, parseModelSwitchCommand, isModelSwitchAcknowledgement } from '@agentproto/driver-agent-cli';
36
36
  import { lookup } from 'dns/promises';
@@ -27928,6 +27928,184 @@ init_context_continuity();
27928
27928
  // src/session-capabilities.ts
27929
27929
  init_canonical_posture();
27930
27930
  init_conversation_store();
27931
+
27932
+ // src/mcp-session-observer.ts
27933
+ var OBSERVED_METHODS = /* @__PURE__ */ new Set([
27934
+ "initialize",
27935
+ "server/discover",
27936
+ "tools/list"
27937
+ ]);
27938
+ var MAX_DESCRIPTION_CHARS = 200;
27939
+ var DEFAULT_MAX_SESSIONS = 500;
27940
+ function createMcpObservationStore(opts = {}) {
27941
+ const maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
27942
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
27943
+ const bySession = /* @__PURE__ */ new Map();
27944
+ const touch = (sessionId) => {
27945
+ const at = now().toISOString();
27946
+ let obs = bySession.get(sessionId);
27947
+ if (!obs) {
27948
+ obs = { firstSeenAt: at, lastSeenAt: at };
27949
+ bySession.set(sessionId, obs);
27950
+ if (bySession.size > maxSessions) {
27951
+ const oldest = bySession.keys().next().value;
27952
+ if (oldest !== void 0) bySession.delete(oldest);
27953
+ }
27954
+ }
27955
+ obs.lastSeenAt = at;
27956
+ return obs;
27957
+ };
27958
+ const record2 = (sessionId, method, message, clientProtocolVersion) => {
27959
+ const obs = touch(sessionId);
27960
+ const at = obs.lastSeenAt;
27961
+ const result = message.result && typeof message.result === "object" ? message.result : void 0;
27962
+ const error = message.error ? `${String(message.error.code ?? "error")}: ${String(message.error.message ?? "")}` : void 0;
27963
+ const answeredVersion = typeof result?.["protocolVersion"] === "string" ? result["protocolVersion"] : void 0;
27964
+ const protocolVersion = method === "initialize" ? answeredVersion : clientProtocolVersion;
27965
+ const base = {
27966
+ at,
27967
+ ok: error === void 0,
27968
+ ...error !== void 0 ? { error } : {},
27969
+ ...protocolVersion ? { protocolVersion } : {}
27970
+ };
27971
+ if (method === "initialize") obs.initialize = base;
27972
+ else if (method === "server/discover") obs.discover = base;
27973
+ else {
27974
+ const rawTools = Array.isArray(result?.["tools"]) ? result["tools"] : [];
27975
+ const tools = [];
27976
+ for (const t of rawTools) {
27977
+ if (!t || typeof t !== "object") continue;
27978
+ const name = t.name;
27979
+ if (typeof name !== "string") continue;
27980
+ const description = t.description;
27981
+ tools.push({
27982
+ name,
27983
+ ...typeof description === "string" && description.length > 0 ? { description: description.slice(0, MAX_DESCRIPTION_CHARS) } : {}
27984
+ });
27985
+ }
27986
+ obs.toolsList = {
27987
+ ...base,
27988
+ toolCount: tools.length,
27989
+ deferred: tools.some((t) => t.name === "tool_search"),
27990
+ tools
27991
+ };
27992
+ }
27993
+ };
27994
+ return {
27995
+ observe(transport, ctx) {
27996
+ const pending = /* @__PURE__ */ new Map();
27997
+ const innerOnMessage = transport.onmessage;
27998
+ transport.onmessage = (message, extra) => {
27999
+ try {
28000
+ if ("method" in message && "id" in message && message.id !== void 0 && OBSERVED_METHODS.has(message.method)) {
28001
+ pending.set(message.id, message.method);
28002
+ }
28003
+ } catch {
28004
+ }
28005
+ innerOnMessage?.(message, extra);
28006
+ };
28007
+ const innerSend = transport.send.bind(transport);
28008
+ transport.send = async (message, options) => {
28009
+ try {
28010
+ if ("id" in message && message.id !== void 0 && ("result" in message || "error" in message)) {
28011
+ const method = pending.get(message.id);
28012
+ if (method) {
28013
+ pending.delete(message.id);
28014
+ record2(ctx.sessionId, method, message, ctx.clientProtocolVersion);
28015
+ }
28016
+ }
28017
+ } catch {
28018
+ }
28019
+ return innerSend(message, options);
28020
+ };
28021
+ },
28022
+ recordHttpError(sessionId, status) {
28023
+ const obs = touch(sessionId);
28024
+ obs.httpError = { at: obs.lastSeenAt, status };
28025
+ },
28026
+ get: (sessionId) => bySession.get(sessionId),
28027
+ delete: (sessionId) => {
28028
+ bySession.delete(sessionId);
28029
+ }
28030
+ };
28031
+ }
28032
+ function isDaemonMountFor(sessionId, entry) {
28033
+ if (entry.transport !== "http" || typeof entry.ref !== "string") return false;
28034
+ try {
28035
+ const url = new URL(entry.ref);
28036
+ return url.pathname === "/mcp" && url.searchParams.get("callerSessionId") === sessionId;
28037
+ } catch {
28038
+ return false;
28039
+ }
28040
+ }
28041
+ function daemonMountStatus(obs, turnsCompleted) {
28042
+ if (!obs) return (turnsCompleted ?? 0) > 0 ? "never-contacted" : "declared";
28043
+ if (obs.toolsList) return obs.toolsList.ok ? "listed" : "error";
28044
+ if (obs.initialize?.ok === false || obs.discover?.ok === false || obs.httpError) return "error";
28045
+ if (obs.initialize || obs.discover) return "connected";
28046
+ return (turnsCompleted ?? 0) > 0 ? "never-contacted" : "declared";
28047
+ }
28048
+ function assessDaemonMount(obs) {
28049
+ const tl = obs?.toolsList;
28050
+ if (tl) {
28051
+ if (!tl.ok) return { reason: "error", ...tl.error ? { detail: tl.error } : {} };
28052
+ if (tl.toolCount === 0) return { reason: "zero-tools" };
28053
+ return void 0;
28054
+ }
28055
+ if (obs?.initialize?.ok === false) {
28056
+ return { reason: "error", ...obs.initialize.error ? { detail: `initialize ${obs.initialize.error}` } : {} };
28057
+ }
28058
+ if (obs?.discover?.ok === false) {
28059
+ return { reason: "error", ...obs.discover.error ? { detail: `server/discover ${obs.discover.error}` } : {} };
28060
+ }
28061
+ if (obs?.httpError) return { reason: "error", detail: `HTTP ${obs.httpError.status}` };
28062
+ return {
28063
+ reason: "never-listed",
28064
+ ...obs?.initialize || obs?.discover ? { detail: "handshake seen but no tools/list" } : {}
28065
+ };
28066
+ }
28067
+ var ABNORMAL_TURN_REASONS = /* @__PURE__ */ new Set(["error", "cancelled", "watchdog-timeout", "aborted"]);
28068
+ function wireMcpDegradedWarnings(opts) {
28069
+ const lastEmitted = /* @__PURE__ */ new Map();
28070
+ const offTurnEnd = opts.sessionEvents.on("session:turn-end", (ev) => {
28071
+ if (ev.reason !== void 0 && ABNORMAL_TURN_REASONS.has(ev.reason)) return;
28072
+ const desc = opts.getSession(ev.sessionId);
28073
+ if (!desc || desc.kind !== "agent-cli") return;
28074
+ const mount = (desc.mcpServers ?? []).find((s) => isDaemonMountFor(ev.sessionId, s));
28075
+ if (!mount) return;
28076
+ const assessment = assessDaemonMount(opts.store.get(ev.sessionId));
28077
+ if (!assessment) {
28078
+ lastEmitted.delete(ev.sessionId);
28079
+ return;
28080
+ }
28081
+ const key = `${assessment.reason}|${assessment.detail ?? ""}`;
28082
+ if (lastEmitted.get(ev.sessionId) === key) return;
28083
+ lastEmitted.set(ev.sessionId, key);
28084
+ opts.sessionEvents.emit({
28085
+ type: "mcp:degraded",
28086
+ sessionId: ev.sessionId,
28087
+ server: mount.name,
28088
+ reason: assessment.reason,
28089
+ ...assessment.detail ? { detail: assessment.detail } : {},
28090
+ ...desc.label ? { label: desc.label } : {},
28091
+ ts: (/* @__PURE__ */ new Date()).toISOString()
28092
+ });
28093
+ });
28094
+ const offExit = opts.sessionEvents.on("session:exited", (ev) => {
28095
+ lastEmitted.delete(ev.sessionId);
28096
+ });
28097
+ const offResumed = opts.sessionEvents.on("session:resumed", (ev) => {
28098
+ lastEmitted.delete(ev.sessionId);
28099
+ opts.store.delete(ev.sessionId);
28100
+ });
28101
+ return () => {
28102
+ offTurnEnd();
28103
+ offExit();
28104
+ offResumed();
28105
+ };
28106
+ }
28107
+
28108
+ // src/session-capabilities.ts
27931
28109
  var SKILLS_APPLYING_ADAPTERS = /* @__PURE__ */ new Set(["hermes"]);
27932
28110
  function sessionArm(desc) {
27933
28111
  if (desc.kind === "terminal") return "pty";
@@ -27936,7 +28114,7 @@ function sessionArm(desc) {
27936
28114
  }
27937
28115
  return "other";
27938
28116
  }
27939
- function buildSessionCapabilities(desc, pendingPermissions) {
28117
+ function buildSessionCapabilities(desc, pendingPermissions, mcpObservation) {
27940
28118
  return {
27941
28119
  sessionId: desc.id,
27942
28120
  adapter: desc.adapterSlug ?? conversationTerminalSlugFor(desc) ?? "unknown",
@@ -27959,7 +28137,8 @@ function buildSessionCapabilities(desc, pendingPermissions) {
27959
28137
  mcpServers: (desc.mcpServers ?? []).map((s) => ({
27960
28138
  name: s.name,
27961
28139
  transport: s.transport,
27962
- ...s.ref ? { ref: s.ref } : {}
28140
+ ...s.ref ? { ref: s.ref } : {},
28141
+ ...isDaemonMountFor(desc.id, s) ? daemonMountFields(mcpObservation, desc.turnsCompleted) : { status: "declared" }
27963
28142
  })),
27964
28143
  skills: desc.skills ?? [],
27965
28144
  skillsApplied: desc.adapterSlug !== void 0 && SKILLS_APPLYING_ADAPTERS.has(desc.adapterSlug),
@@ -27967,6 +28146,20 @@ function buildSessionCapabilities(desc, pendingPermissions) {
27967
28146
  pendingPermissions
27968
28147
  };
27969
28148
  }
28149
+ function daemonMountFields(obs, turnsCompleted) {
28150
+ const status = daemonMountStatus(obs, turnsCompleted);
28151
+ const tl = obs?.toolsList;
28152
+ const failing = tl && !tl.ok ? tl : [obs?.initialize, obs?.discover].find((c) => c && !c.ok);
28153
+ const error = failing?.error ?? (obs?.httpError ? `HTTP ${obs.httpError.status}` : void 0);
28154
+ const protocolVersion = tl?.protocolVersion ?? obs?.initialize?.protocolVersion ?? obs?.discover?.protocolVersion;
28155
+ return {
28156
+ status,
28157
+ ...tl?.ok ? { toolCount: tl.toolCount, tools: tl.tools, deferred: tl.deferred } : {},
28158
+ ...protocolVersion ? { protocolVersion } : {},
28159
+ ...obs ? { lastSeenAt: obs.lastSeenAt } : {},
28160
+ ...error ? { error } : {}
28161
+ };
28162
+ }
27970
28163
 
27971
28164
  // src/session-tools.ts
27972
28165
  init_context_checkpoint();
@@ -30769,7 +30962,7 @@ function registerSessionTools(rawServer, opts) {
30769
30962
  const pendingPermissions = registry.listPendingPermissions({ sessionId: fresh.id }).length;
30770
30963
  return {
30771
30964
  content: [
30772
- { type: "text", text: JSON.stringify(buildSessionCapabilities(fresh, pendingPermissions)) }
30965
+ { type: "text", text: JSON.stringify(buildSessionCapabilities(fresh, pendingPermissions, opts.mcpObservations?.get(fresh.id))) }
30773
30966
  ]
30774
30967
  };
30775
30968
  }
@@ -36193,6 +36386,20 @@ function registerMcpApps(server, apps) {
36193
36386
  );
36194
36387
  }
36195
36388
  }
36389
+
36390
+ // src/store-panel-allowlist.ts
36391
+ var STORE_UI_TOOLS = [
36392
+ "app_catalog",
36393
+ "app_list",
36394
+ "app_install",
36395
+ "app_resync",
36396
+ "app_updates",
36397
+ "app_uninstall",
36398
+ "app_status"
36399
+ ];
36400
+
36401
+ // src/builtin-apps.ts
36402
+ var STORE_PANEL_APP_ID = storeApp.id ?? "@agentproto/store";
36196
36403
  function makeBuiltinPanelApps(ops) {
36197
36404
  const sessionChatNativeInstalled = ops.isSessionChatInstalled();
36198
36405
  return [
@@ -36236,7 +36443,18 @@ function makeBuiltinPanelApps(ops) {
36236
36443
  // building `makeBuiltinPanelApps` without one — e.g.
36237
36444
  // `builtinPanelCatalogEntries`'s stub ops below — still gets a
36238
36445
  // deterministic, small app list).
36239
- ...ops.listReviews ? [makeReviewPanelApp({ listReviews: ops.listReviews })] : []
36446
+ ...ops.listReviews ? [makeReviewPanelApp({ listReviews: ops.listReviews })] : [],
36447
+ // App Store widget — the browser/launcher over app_catalog + installed
36448
+ // apps (see apps/src/store). Initial snapshot only when the ops are
36449
+ // wired; writes (install/update/uninstall) always go through the SAME
36450
+ // MCP verbs every other caller uses — no second install path.
36451
+ ...ops.listCatalog && ops.listInstalled && ops.listUpdates ? [
36452
+ makeStoreApp({
36453
+ listCatalog: ops.listCatalog,
36454
+ listInstalled: ops.listInstalled,
36455
+ listUpdates: ops.listUpdates
36456
+ })
36457
+ ] : []
36240
36458
  ];
36241
36459
  }
36242
36460
  var PANEL_APP_HANDLES = [
@@ -36247,7 +36465,8 @@ var PANEL_APP_HANDLES = [
36247
36465
  liveSessionApp,
36248
36466
  sessionChatApp,
36249
36467
  workBoardApp,
36250
- reviewPanelApp
36468
+ reviewPanelApp,
36469
+ storeApp
36251
36470
  ];
36252
36471
  var DEEP_LINK_SESSION_ID_RE = /^sess_[A-Za-z0-9_-]+$/;
36253
36472
  function isValidDeepLinkSessionId(id) {
@@ -36260,6 +36479,9 @@ function resolveBuiltinPanelUi(appId, httpBaseUrl, sessionId) {
36260
36479
  const html = typeof app.html === "function" ? app.html({ httpBaseUrl, sessionId: pinnedSessionId }) : app.html;
36261
36480
  return { html, tools: liveSessionApp.ui?.tools ?? [] };
36262
36481
  }
36482
+ if (appId === storeApp.id) {
36483
+ return { html: storeApp.ui?.html ?? "", tools: STORE_UI_TOOLS };
36484
+ }
36263
36485
  const handle = [sessionsPanelApp, agentsOverviewApp, bureauSessionsApp, sessionStoryApp, workBoardApp, reviewPanelApp].find(
36264
36486
  (h) => h.id === appId
36265
36487
  );
@@ -36274,10 +36496,14 @@ function builtinPanelCatalogEntries() {
36274
36496
  listTasks: (boardId) => ({ boardId: boardId ?? "ws:default", tasks: [] }),
36275
36497
  // Always provided (never omitted) here, same reasoning as
36276
36498
  // `isSessionChatInstalled: () => false` above: `PANEL_APP_HANDLES` is
36277
- // zipped against this call's output BY INDEX, so the review panel must
36278
- // always be present in this specific call regardless of whether the
36279
- // real daemon happens to have a review runner wired.
36280
- listReviews: async () => ({ total: 0, attestations: [] })
36499
+ // zipped against this call's output BY INDEX, so the review panel and
36500
+ // the store must always be present in this specific call regardless of
36501
+ // whether the real daemon happens to have a review runner wired or any
36502
+ // store ops at all.
36503
+ listReviews: async () => ({ total: 0, attestations: [] }),
36504
+ listCatalog: async () => [],
36505
+ listInstalled: () => [],
36506
+ listUpdates: () => []
36281
36507
  });
36282
36508
  return apps.map((app, i) => {
36283
36509
  const handle = PANEL_APP_HANDLES[i];
@@ -45778,7 +46004,7 @@ function registerOrchestrationTools(rawServer, opts) {
45778
46004
  "Opaque cursor from a prior call's `nextCursor`. Omit (or pass 0) to start from the current tail \u2014 returns events emitted after this call."
45779
46005
  ),
45780
46006
  sessionIds: z.array(z.string()).optional().describe("Filter to these session ids. Omit \u2192 all sessions."),
45781
- types: z.array(z.enum(["turn-end", "awaiting-input", "permission-request", "permission-resolved", "exited", "session:spawned", "command-done", "policy:passed", "policy:failed", "policy:commit-ready", "policy:committed", "cron:fired", "cron:succeeded", "cron:failed", "cron:unhealthy", "activity:changed", "task:changed"])).optional().describe("Filter to these event types. Omit \u2192 all types."),
46007
+ types: z.array(z.enum(["turn-end", "awaiting-input", "permission-request", "permission-resolved", "exited", "session:spawned", "command-done", "policy:passed", "policy:failed", "policy:commit-ready", "policy:committed", "cron:fired", "cron:succeeded", "cron:failed", "cron:unhealthy", "activity:changed", "task:changed", "mcp:degraded"])).optional().describe("Filter to these event types. Omit \u2192 all types."),
45782
46008
  limit: z.number().int().min(1).max(200).optional().describe("Max events to return. Default 50."),
45783
46009
  full: z.boolean().optional().describe(
45784
46010
  "Request verbose per-event detail. Accepted for forward compatibility \u2014 this build always returns the same compact event shape regardless of the flag. Default false."
@@ -47332,7 +47558,7 @@ var RemoteCatalogSchema = z.object({
47332
47558
  generatedAt: z.string().optional(),
47333
47559
  entries: z.array(z.unknown())
47334
47560
  });
47335
- var DEFAULT_CATALOG_SOURCE_URL = "https://cli.agentproto.sh/catalog/v1/apps.json";
47561
+ var DEFAULT_CATALOG_SOURCE_URL = "https://agentproto.sh/catalog/v1/apps.json";
47336
47562
  var EMPTY_CATALOG = { apps: [] };
47337
47563
  function defaultAppCatalogPath() {
47338
47564
  return join(homedir(), ".agentproto", "app-catalog.json");
@@ -47655,6 +47881,82 @@ function notEnabled(tool) {
47655
47881
  `${tool} is not enabled \u2014 the daemon was started without an adapter resolver. Re-run the daemon with the \`@agentproto/cli\` shim wired (see playground/scripts/gateway.ts).`
47656
47882
  );
47657
47883
  }
47884
+ function confirmTokenOf(args) {
47885
+ const digest = createHash("sha256");
47886
+ digest.update(
47887
+ JSON.stringify([
47888
+ "agentproto-app-install-confirm-v1",
47889
+ args.url,
47890
+ isAgentappUrl(args.url) ? "agentapp" : "git",
47891
+ args.sha256 ?? null,
47892
+ args.sha ?? null,
47893
+ args.ref ?? null,
47894
+ args.subdir ?? null,
47895
+ args.allowBuild === true
47896
+ ])
47897
+ );
47898
+ return digest.digest("hex").slice(0, 32);
47899
+ }
47900
+ var appInstallNonces = /* @__PURE__ */ new Map();
47901
+ var APP_INSTALL_NONCE_MAX = 256;
47902
+ var APP_INSTALL_NONCE_TTL_MS = 5 * 60 * 1e3;
47903
+ function issueInstallNonce(args) {
47904
+ const now = Date.now();
47905
+ for (const [nonce2, entry] of appInstallNonces) {
47906
+ if (entry.expiresAt <= now) appInstallNonces.delete(nonce2);
47907
+ }
47908
+ if (appInstallNonces.size >= APP_INSTALL_NONCE_MAX) {
47909
+ const oldest = appInstallNonces.keys().next().value;
47910
+ if (oldest !== void 0) appInstallNonces.delete(oldest);
47911
+ }
47912
+ const nonce = randomBytes(16).toString("hex");
47913
+ appInstallNonces.set(nonce, {
47914
+ fingerprint: confirmTokenOf({
47915
+ url: typeof args.url === "string" ? args.url : "",
47916
+ sha256: typeof args.sha256 === "string" ? args.sha256 : void 0,
47917
+ sha: typeof args.sha === "string" ? args.sha : void 0,
47918
+ ref: typeof args.ref === "string" ? args.ref : void 0,
47919
+ subdir: typeof args.subdir === "string" ? args.subdir : void 0,
47920
+ allowBuild: args.allowBuild === true
47921
+ }),
47922
+ expiresAt: now + APP_INSTALL_NONCE_TTL_MS
47923
+ });
47924
+ return nonce;
47925
+ }
47926
+ function appInstallConfirmationClass(args) {
47927
+ const url = args.url;
47928
+ if (typeof url !== "string" || url.trim() === "" || args.dir !== void 0 || args.file !== void 0) {
47929
+ return void 0;
47930
+ }
47931
+ if (typeof args.confirm !== "string") return { confirmed: false };
47932
+ const nonce = appInstallNonces.get(args.confirm);
47933
+ appInstallNonces.delete(args.confirm);
47934
+ const fingerprint = confirmTokenOf({
47935
+ url,
47936
+ sha256: typeof args.sha256 === "string" ? args.sha256 : void 0,
47937
+ sha: typeof args.sha === "string" ? args.sha : void 0,
47938
+ ref: typeof args.ref === "string" ? args.ref : void 0,
47939
+ subdir: typeof args.subdir === "string" ? args.subdir : void 0,
47940
+ allowBuild: args.allowBuild === true
47941
+ });
47942
+ const valid = nonce !== void 0 && nonce.fingerprint === fingerprint && Date.now() < nonce.expiresAt;
47943
+ return valid ? { confirmed: true } : { confirmed: false };
47944
+ }
47945
+ function buildAppInstallPreview(args) {
47946
+ const url = typeof args.url === "string" ? args.url : "";
47947
+ const isAgentapp = isAgentappUrl(url);
47948
+ return {
47949
+ needsConfirmation: true,
47950
+ confirm: issueInstallNonce(args),
47951
+ kind: isAgentapp ? "agentapp" : "git",
47952
+ url,
47953
+ ...typeof args.sha256 === "string" ? { sha256: args.sha256 } : {},
47954
+ ...typeof args.sha === "string" ? { sha: args.sha } : {},
47955
+ ...typeof args.ref === "string" ? { ref: args.ref } : {},
47956
+ ...typeof args.subdir === "string" ? { subdir: args.subdir } : {},
47957
+ runsBuildCommand: !isAgentapp && args.allowBuild === true
47958
+ };
47959
+ }
47658
47960
  async function dispatchAllowlistedAppTool(declaredAllowlist, input, deps2) {
47659
47961
  const effectiveAllowlist = [...declaredAllowlist, ...APP_UI_DISCOVERY_TOOLS];
47660
47962
  if (!effectiveAllowlist.includes(input.tool)) {
@@ -47663,6 +47965,9 @@ async function dispatchAllowlistedAppTool(declaredAllowlist, input, deps2) {
47663
47965
  );
47664
47966
  }
47665
47967
  const args = input.args ?? {};
47968
+ if (input.tool === "app_install" && appInstallConfirmationClass(args)?.confirmed === false) {
47969
+ return textResult2(buildAppInstallPreview(args));
47970
+ }
47666
47971
  try {
47667
47972
  if (input.tool.startsWith("imported:")) {
47668
47973
  if (!deps2.callImportedTool) return notEnabled("app_tool_call");
@@ -50447,7 +50752,9 @@ async function startHttpServer(opts) {
50447
50752
  }
50448
50753
  }
50449
50754
  }
50450
- async function serveMcp(req, res, server2) {
50755
+ async function serveMcp(req, res, server2, observedSessionId) {
50756
+ const rawVersionHeader = req.headers["mcp-protocol-version"];
50757
+ const clientProtocolVersion = Array.isArray(rawVersionHeader) ? rawVersionHeader[0] : rawVersionHeader;
50451
50758
  coerceUnsupportedProtocolVersionHeader(req);
50452
50759
  const transport = new StreamableHTTPServerTransport({
50453
50760
  sessionIdGenerator: void 0
@@ -50466,9 +50773,25 @@ async function startHttpServer(opts) {
50466
50773
  void transport.close();
50467
50774
  void server2.close();
50468
50775
  });
50776
+ const observations = observedSessionId ? opts.mcpObservations : void 0;
50777
+ let sawMessage = false;
50469
50778
  try {
50470
50779
  await server2.connect(transport);
50780
+ if (observations && observedSessionId) {
50781
+ observations.observe(transport, {
50782
+ sessionId: observedSessionId,
50783
+ ...clientProtocolVersion ? { clientProtocolVersion } : {}
50784
+ });
50785
+ const innerOnMessage = transport.onmessage;
50786
+ transport.onmessage = (message, extra) => {
50787
+ sawMessage = true;
50788
+ innerOnMessage?.(message, extra);
50789
+ };
50790
+ }
50471
50791
  await transport.handleRequest(req, res);
50792
+ if (observations && observedSessionId && !sawMessage && req.method === "POST" && res.statusCode >= 400) {
50793
+ observations.recordHttpError(observedSessionId, res.statusCode);
50794
+ }
50472
50795
  } catch (err) {
50473
50796
  const line = mcpErrorLogGate.onFailure(
50474
50797
  "mcp:handleRequest",
@@ -50539,7 +50862,7 @@ async function startHttpServer(opts) {
50539
50862
  return;
50540
50863
  }
50541
50864
  const server2 = await opts.mcpServerFactory(denyTools, callerSessionId, origin, deferred, allowTools, surface);
50542
- await serveMcp(req, res, server2);
50865
+ await serveMcp(req, res, server2, callerSessionId);
50543
50866
  }
50544
50867
  async function handleOrchestratorMcp(req, res) {
50545
50868
  if (!opts.orchestratorMcpServerFactory || !opts.verifyOrchestratorScope) {
@@ -51195,7 +51518,8 @@ async function startHttpServer(opts) {
51195
51518
  opts.webhookNotifier,
51196
51519
  opts.ensureLlmEndpointRunning,
51197
51520
  opts.listAgentAdapters,
51198
- opts.deviceMirrorSync
51521
+ opts.deviceMirrorSync,
51522
+ opts.mcpObservations
51199
51523
  );
51200
51524
  if (handled) return;
51201
51525
  }
@@ -52421,6 +52745,12 @@ async function startHttpServer(opts) {
52421
52745
  });
52422
52746
  return;
52423
52747
  }
52748
+ if (path === "/store" && req.method === "GET") {
52749
+ const target = normalizeStoreRedirectUrl(req.url ?? "/store");
52750
+ res.writeHead(302, { location: target });
52751
+ res.end();
52752
+ return;
52753
+ }
52424
52754
  if (opts.appRegistry && path.startsWith("/apps/")) {
52425
52755
  const appUiShellBlocked = (appId) => !iframeEmbedOriginAllowed(req, opts.appRegistry.getApp(appId) ?? {}) && !isValidAppEmbedToken(requestEmbedToken(req)) && guardBrowserOrigin(req, res);
52426
52756
  const assetMatch = path.match(APP_UI_ASSET_RE);
@@ -53281,7 +53611,7 @@ async function handleSandboxes(_req, res, path, resolveSandboxProvider2) {
53281
53611
  json3(200, { alive: true, ...probe.state ? { state: probe.state } : { state: row.state }, checkedAt });
53282
53612
  return true;
53283
53613
  }
53284
- async function handleSessions(req, res, path, registry, resolveAgentAdapter, ptyEnabled = false, resolveBrowserAdapter, listBrowserAdapters, sessionEvents, eventRing, buildOrchestratorMcp, daemonMcpUrl, provisionWorktree, listCatalogModels, resolveSandboxProvider2, webhookNotifier, ensureLlmEndpointRunning, listAgentAdapters, deviceMirrorSync) {
53614
+ async function handleSessions(req, res, path, registry, resolveAgentAdapter, ptyEnabled = false, resolveBrowserAdapter, listBrowserAdapters, sessionEvents, eventRing, buildOrchestratorMcp, daemonMcpUrl, provisionWorktree, listCatalogModels, resolveSandboxProvider2, webhookNotifier, ensureLlmEndpointRunning, listAgentAdapters, deviceMirrorSync, mcpObservations) {
53285
53615
  const json3 = (status, body) => {
53286
53616
  res.writeHead(status, { "content-type": "application/json" });
53287
53617
  res.end(JSON.stringify(body));
@@ -54329,7 +54659,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
54329
54659
  }
54330
54660
  const fresh = registry.get(desc.id) ?? desc;
54331
54661
  const pendingPermissions = registry.listPendingPermissions({ sessionId: fresh.id }).length;
54332
- json3(200, buildSessionCapabilities(fresh, pendingPermissions));
54662
+ json3(200, buildSessionCapabilities(fresh, pendingPermissions, mcpObservations?.get(fresh.id)));
54333
54663
  return true;
54334
54664
  }
54335
54665
  const renameMatch = path.match(/^\/sessions\/([^/]+)$/);
@@ -56690,6 +57020,11 @@ async function handleAppUiPage(req, res, appId, appRegistry, frameAncestors) {
56690
57020
  sendRepresentation(req, res, rep, headers);
56691
57021
  }
56692
57022
  var APP_UI_PAGE_RE = /^\/apps\/(.+)\/ui\/?$/;
57023
+ function normalizeStoreRedirectUrl(rawUrl) {
57024
+ const queryIndex = rawUrl.indexOf("?");
57025
+ const query = queryIndex >= 0 ? rawUrl.slice(queryIndex) : "";
57026
+ return `/apps/${STORE_PANEL_APP_ID.split("/")[0]}/${STORE_PANEL_APP_ID.split("/")[1]}/ui${query}`;
57027
+ }
56693
57028
  var APP_UI_ASSET_RE = /^\/apps\/(.+?)\/ui\/assets\/([^/]*)$/;
56694
57029
  function isAppUiShellRequest(method, path) {
56695
57030
  if (method !== "GET") return false;
@@ -66904,6 +67239,7 @@ async function createGateway(opts) {
66904
67239
  });
66905
67240
  const sessionEvents = createSessionEventBus();
66906
67241
  const eventRing = createEventRing();
67242
+ const mcpObservations = createMcpObservationStore();
66907
67243
  eventRing.wire(sessionEvents);
66908
67244
  const webhookNotifier = createWebhookNotifier();
66909
67245
  sessionEvents.onAny((ev) => webhookNotifier.onSessionEvent(ev));
@@ -67235,6 +67571,11 @@ async function createGateway(opts) {
67235
67571
  webOrigins: daemonConfig.approvals?.webOrigins ?? []
67236
67572
  });
67237
67573
  wireSupervisorNotify({ registry: sessions, sessionEvents });
67574
+ wireMcpDegradedWarnings({
67575
+ sessionEvents,
67576
+ getSession: (id) => sessions.get(id),
67577
+ store: mcpObservations
67578
+ });
67238
67579
  const activityProjector = createActivityProjector({
67239
67580
  registry: sessions,
67240
67581
  sessionEvents,
@@ -67435,6 +67776,7 @@ async function createGateway(opts) {
67435
67776
  }
67436
67777
  registerSessionTools(server, {
67437
67778
  registry: sessions,
67779
+ mcpObservations,
67438
67780
  workspace,
67439
67781
  mcpProxy,
67440
67782
  ptyEnabled: opts.spawnPty != null,
@@ -67539,6 +67881,21 @@ async function createGateway(opts) {
67539
67881
  registerAppDataTools(server, { appRegistry });
67540
67882
  registerAppExternalTools(server, { appRegistry });
67541
67883
  registerTelegramBotTools(server, { telegramCreds: telegramBotCreds });
67884
+ const listCatalogRows = async () => {
67885
+ const rows = await dispatchTool("app_catalog", {});
67886
+ if (typeof rows !== "string") return [];
67887
+ try {
67888
+ const parsed = JSON.parse(rows);
67889
+ return Array.isArray(parsed) ? parsed : [];
67890
+ } catch {
67891
+ return [];
67892
+ }
67893
+ };
67894
+ const listInstalledRows = () => {
67895
+ const apps = appRegistry.listApps();
67896
+ return apps;
67897
+ };
67898
+ const listUpdateRows = () => appRegistry.listApps().filter((a) => a.source !== void 0 && a.source.kind !== "local" && a.source.catalogId !== void 0).map((a) => ({ appId: a.appId, updateAvailable: true, ...a.version !== void 0 ? { version: a.version } : {} }));
67542
67899
  const listSessionsFiltered = (filter) => {
67543
67900
  let rows = sessions.list().filter((s) => s.kind !== "command");
67544
67901
  if (filter === "running") {
@@ -67579,7 +67936,14 @@ async function createGateway(opts) {
67579
67936
  reviewRunner,
67580
67937
  { ...input, includeRunning: true },
67581
67938
  { resolveSubtree: (sessionId) => [...collectSubtree(sessionId, sessions.list({ includeArchived: true }))] }
67582
- )
67939
+ ),
67940
+ // Store widget's read paths — app_catalog's rows (via the SAME
67941
+ // `dispatchTool` the panel's own apply verbs will route through, so
67942
+ // its snapshot can never disagree with what the tool call returns),
67943
+ // and the registry's installed apps.
67944
+ listCatalog: listCatalogRows,
67945
+ listInstalled: listInstalledRows,
67946
+ listUpdates: listUpdateRows
67583
67947
  }),
67584
67948
  // Same ptyEnabled gate as terminal_start/terminal_input/… in
67585
67949
  // session-tools.ts — the panel would be able to open the WS but
@@ -67745,6 +68109,7 @@ async function createGateway(opts) {
67745
68109
  const http = await startHttpServer({
67746
68110
  port,
67747
68111
  bind: opts.bind,
68112
+ mcpObservations,
67748
68113
  // Auth is read on every request via this getter. `opts.auth`
67749
68114
  // (when provided) is the startup default and represents the
67750
68115
  // operator's intent — bearer-only deployments stay bearer-only