@agentproto/runtime 5.10.0 → 5.11.1

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,185 @@ 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 isExpectedDiscoverMiss = method === "server/discover" && message.error?.code === -32601;
27963
+ const error = message.error && !isExpectedDiscoverMiss ? `${String(message.error.code ?? "error")}: ${String(message.error.message ?? "")}` : void 0;
27964
+ const answeredVersion = typeof result?.["protocolVersion"] === "string" ? result["protocolVersion"] : void 0;
27965
+ const protocolVersion = method === "initialize" ? answeredVersion : clientProtocolVersion;
27966
+ const base = {
27967
+ at,
27968
+ ok: error === void 0,
27969
+ ...error !== void 0 ? { error } : {},
27970
+ ...protocolVersion ? { protocolVersion } : {}
27971
+ };
27972
+ if (method === "initialize") obs.initialize = base;
27973
+ else if (method === "server/discover") obs.discover = base;
27974
+ else {
27975
+ const rawTools = Array.isArray(result?.["tools"]) ? result["tools"] : [];
27976
+ const tools = [];
27977
+ for (const t of rawTools) {
27978
+ if (!t || typeof t !== "object") continue;
27979
+ const name = t.name;
27980
+ if (typeof name !== "string") continue;
27981
+ const description = t.description;
27982
+ tools.push({
27983
+ name,
27984
+ ...typeof description === "string" && description.length > 0 ? { description: description.slice(0, MAX_DESCRIPTION_CHARS) } : {}
27985
+ });
27986
+ }
27987
+ obs.toolsList = {
27988
+ ...base,
27989
+ toolCount: tools.length,
27990
+ deferred: tools.some((t) => t.name === "tool_search"),
27991
+ tools
27992
+ };
27993
+ }
27994
+ };
27995
+ return {
27996
+ observe(transport, ctx) {
27997
+ const pending = /* @__PURE__ */ new Map();
27998
+ const innerOnMessage = transport.onmessage;
27999
+ transport.onmessage = (message, extra) => {
28000
+ try {
28001
+ if ("method" in message && "id" in message && message.id !== void 0 && OBSERVED_METHODS.has(message.method)) {
28002
+ pending.set(message.id, message.method);
28003
+ }
28004
+ } catch {
28005
+ }
28006
+ innerOnMessage?.(message, extra);
28007
+ };
28008
+ const innerSend = transport.send.bind(transport);
28009
+ transport.send = async (message, options) => {
28010
+ try {
28011
+ if ("id" in message && message.id !== void 0 && ("result" in message || "error" in message)) {
28012
+ const method = pending.get(message.id);
28013
+ if (method) {
28014
+ pending.delete(message.id);
28015
+ record2(ctx.sessionId, method, message, ctx.clientProtocolVersion);
28016
+ }
28017
+ }
28018
+ } catch {
28019
+ }
28020
+ return innerSend(message, options);
28021
+ };
28022
+ },
28023
+ recordHttpError(sessionId, status) {
28024
+ const obs = touch(sessionId);
28025
+ obs.httpError = { at: obs.lastSeenAt, status };
28026
+ },
28027
+ get: (sessionId) => bySession.get(sessionId),
28028
+ delete: (sessionId) => {
28029
+ bySession.delete(sessionId);
28030
+ }
28031
+ };
28032
+ }
28033
+ function isDaemonMountFor(sessionId, entry) {
28034
+ if (entry.transport !== "http" || typeof entry.ref !== "string") return false;
28035
+ try {
28036
+ const url = new URL(entry.ref);
28037
+ return url.pathname === "/mcp" && url.searchParams.get("callerSessionId") === sessionId;
28038
+ } catch {
28039
+ return false;
28040
+ }
28041
+ }
28042
+ function daemonMountStatus(obs, turnsCompleted) {
28043
+ if (!obs) return (turnsCompleted ?? 0) > 0 ? "never-contacted" : "declared";
28044
+ if (obs.toolsList) return obs.toolsList.ok ? "listed" : "error";
28045
+ if (obs.initialize?.ok === false || obs.discover?.ok === false || obs.httpError) return "error";
28046
+ if (obs.initialize || obs.discover) return "connected";
28047
+ return (turnsCompleted ?? 0) > 0 ? "never-contacted" : "declared";
28048
+ }
28049
+ function assessDaemonMount(obs) {
28050
+ const tl = obs?.toolsList;
28051
+ if (tl) {
28052
+ if (!tl.ok) return { reason: "error", ...tl.error ? { detail: tl.error } : {} };
28053
+ if (tl.toolCount === 0) return { reason: "zero-tools" };
28054
+ return void 0;
28055
+ }
28056
+ if (obs?.initialize?.ok === false) {
28057
+ return { reason: "error", ...obs.initialize.error ? { detail: `initialize ${obs.initialize.error}` } : {} };
28058
+ }
28059
+ if (obs?.discover?.ok === false) {
28060
+ return { reason: "error", ...obs.discover.error ? { detail: `server/discover ${obs.discover.error}` } : {} };
28061
+ }
28062
+ if (obs?.httpError) return { reason: "error", detail: `HTTP ${obs.httpError.status}` };
28063
+ return {
28064
+ reason: "never-listed",
28065
+ ...obs?.initialize || obs?.discover ? { detail: "handshake seen but no tools/list" } : {}
28066
+ };
28067
+ }
28068
+ var ABNORMAL_TURN_REASONS = /* @__PURE__ */ new Set(["error", "cancelled", "watchdog-timeout", "aborted"]);
28069
+ function wireMcpDegradedWarnings(opts) {
28070
+ const lastEmitted = /* @__PURE__ */ new Map();
28071
+ const offTurnEnd = opts.sessionEvents.on("session:turn-end", (ev) => {
28072
+ if (ev.reason !== void 0 && ABNORMAL_TURN_REASONS.has(ev.reason)) return;
28073
+ const desc = opts.getSession(ev.sessionId);
28074
+ if (!desc || desc.kind !== "agent-cli") return;
28075
+ const mount = (desc.mcpServers ?? []).find((s) => isDaemonMountFor(ev.sessionId, s));
28076
+ if (!mount) return;
28077
+ const assessment = assessDaemonMount(opts.store.get(ev.sessionId));
28078
+ if (!assessment) {
28079
+ lastEmitted.delete(ev.sessionId);
28080
+ return;
28081
+ }
28082
+ const key = `${assessment.reason}|${assessment.detail ?? ""}`;
28083
+ if (lastEmitted.get(ev.sessionId) === key) return;
28084
+ lastEmitted.set(ev.sessionId, key);
28085
+ opts.sessionEvents.emit({
28086
+ type: "mcp:degraded",
28087
+ sessionId: ev.sessionId,
28088
+ server: mount.name,
28089
+ reason: assessment.reason,
28090
+ ...assessment.detail ? { detail: assessment.detail } : {},
28091
+ ...desc.label ? { label: desc.label } : {},
28092
+ ts: (/* @__PURE__ */ new Date()).toISOString()
28093
+ });
28094
+ });
28095
+ const offExit = opts.sessionEvents.on("session:exited", (ev) => {
28096
+ lastEmitted.delete(ev.sessionId);
28097
+ });
28098
+ const offResumed = opts.sessionEvents.on("session:resumed", (ev) => {
28099
+ lastEmitted.delete(ev.sessionId);
28100
+ opts.store.delete(ev.sessionId);
28101
+ });
28102
+ return () => {
28103
+ offTurnEnd();
28104
+ offExit();
28105
+ offResumed();
28106
+ };
28107
+ }
28108
+
28109
+ // src/session-capabilities.ts
27931
28110
  var SKILLS_APPLYING_ADAPTERS = /* @__PURE__ */ new Set(["hermes"]);
27932
28111
  function sessionArm(desc) {
27933
28112
  if (desc.kind === "terminal") return "pty";
@@ -27936,7 +28115,7 @@ function sessionArm(desc) {
27936
28115
  }
27937
28116
  return "other";
27938
28117
  }
27939
- function buildSessionCapabilities(desc, pendingPermissions) {
28118
+ function buildSessionCapabilities(desc, pendingPermissions, mcpObservation) {
27940
28119
  return {
27941
28120
  sessionId: desc.id,
27942
28121
  adapter: desc.adapterSlug ?? conversationTerminalSlugFor(desc) ?? "unknown",
@@ -27959,7 +28138,8 @@ function buildSessionCapabilities(desc, pendingPermissions) {
27959
28138
  mcpServers: (desc.mcpServers ?? []).map((s) => ({
27960
28139
  name: s.name,
27961
28140
  transport: s.transport,
27962
- ...s.ref ? { ref: s.ref } : {}
28141
+ ...s.ref ? { ref: s.ref } : {},
28142
+ ...isDaemonMountFor(desc.id, s) ? daemonMountFields(mcpObservation, desc.turnsCompleted) : { status: "declared" }
27963
28143
  })),
27964
28144
  skills: desc.skills ?? [],
27965
28145
  skillsApplied: desc.adapterSlug !== void 0 && SKILLS_APPLYING_ADAPTERS.has(desc.adapterSlug),
@@ -27967,6 +28147,20 @@ function buildSessionCapabilities(desc, pendingPermissions) {
27967
28147
  pendingPermissions
27968
28148
  };
27969
28149
  }
28150
+ function daemonMountFields(obs, turnsCompleted) {
28151
+ const status = daemonMountStatus(obs, turnsCompleted);
28152
+ const tl = obs?.toolsList;
28153
+ const failing = tl ? tl.ok ? void 0 : tl : [obs?.initialize, obs?.discover].find((c) => c && !c.ok);
28154
+ const error = failing?.error ?? (obs?.httpError ? `HTTP ${obs.httpError.status}` : void 0);
28155
+ const protocolVersion = tl?.protocolVersion ?? obs?.initialize?.protocolVersion ?? obs?.discover?.protocolVersion;
28156
+ return {
28157
+ status,
28158
+ ...tl?.ok ? { toolCount: tl.toolCount, tools: tl.tools, deferred: tl.deferred } : {},
28159
+ ...protocolVersion ? { protocolVersion } : {},
28160
+ ...obs ? { lastSeenAt: obs.lastSeenAt } : {},
28161
+ ...error ? { error } : {}
28162
+ };
28163
+ }
27970
28164
 
27971
28165
  // src/session-tools.ts
27972
28166
  init_context_checkpoint();
@@ -30769,7 +30963,7 @@ function registerSessionTools(rawServer, opts) {
30769
30963
  const pendingPermissions = registry.listPendingPermissions({ sessionId: fresh.id }).length;
30770
30964
  return {
30771
30965
  content: [
30772
- { type: "text", text: JSON.stringify(buildSessionCapabilities(fresh, pendingPermissions)) }
30966
+ { type: "text", text: JSON.stringify(buildSessionCapabilities(fresh, pendingPermissions, opts.mcpObservations?.get(fresh.id))) }
30773
30967
  ]
30774
30968
  };
30775
30969
  }
@@ -36193,6 +36387,20 @@ function registerMcpApps(server, apps) {
36193
36387
  );
36194
36388
  }
36195
36389
  }
36390
+
36391
+ // src/store-panel-allowlist.ts
36392
+ var STORE_UI_TOOLS = [
36393
+ "app_catalog",
36394
+ "app_list",
36395
+ "app_install",
36396
+ "app_resync",
36397
+ "app_updates",
36398
+ "app_uninstall",
36399
+ "app_status"
36400
+ ];
36401
+
36402
+ // src/builtin-apps.ts
36403
+ var STORE_PANEL_APP_ID = storeApp.id ?? "@agentproto/store";
36196
36404
  function makeBuiltinPanelApps(ops) {
36197
36405
  const sessionChatNativeInstalled = ops.isSessionChatInstalled();
36198
36406
  return [
@@ -36236,7 +36444,18 @@ function makeBuiltinPanelApps(ops) {
36236
36444
  // building `makeBuiltinPanelApps` without one — e.g.
36237
36445
  // `builtinPanelCatalogEntries`'s stub ops below — still gets a
36238
36446
  // deterministic, small app list).
36239
- ...ops.listReviews ? [makeReviewPanelApp({ listReviews: ops.listReviews })] : []
36447
+ ...ops.listReviews ? [makeReviewPanelApp({ listReviews: ops.listReviews })] : [],
36448
+ // App Store widget — the browser/launcher over app_catalog + installed
36449
+ // apps (see apps/src/store). Initial snapshot only when the ops are
36450
+ // wired; writes (install/update/uninstall) always go through the SAME
36451
+ // MCP verbs every other caller uses — no second install path.
36452
+ ...ops.listCatalog && ops.listInstalled && ops.listUpdates ? [
36453
+ makeStoreApp({
36454
+ listCatalog: ops.listCatalog,
36455
+ listInstalled: ops.listInstalled,
36456
+ listUpdates: ops.listUpdates
36457
+ })
36458
+ ] : []
36240
36459
  ];
36241
36460
  }
36242
36461
  var PANEL_APP_HANDLES = [
@@ -36247,7 +36466,8 @@ var PANEL_APP_HANDLES = [
36247
36466
  liveSessionApp,
36248
36467
  sessionChatApp,
36249
36468
  workBoardApp,
36250
- reviewPanelApp
36469
+ reviewPanelApp,
36470
+ storeApp
36251
36471
  ];
36252
36472
  var DEEP_LINK_SESSION_ID_RE = /^sess_[A-Za-z0-9_-]+$/;
36253
36473
  function isValidDeepLinkSessionId(id) {
@@ -36260,6 +36480,9 @@ function resolveBuiltinPanelUi(appId, httpBaseUrl, sessionId) {
36260
36480
  const html = typeof app.html === "function" ? app.html({ httpBaseUrl, sessionId: pinnedSessionId }) : app.html;
36261
36481
  return { html, tools: liveSessionApp.ui?.tools ?? [] };
36262
36482
  }
36483
+ if (appId === storeApp.id) {
36484
+ return { html: storeApp.ui?.html ?? "", tools: STORE_UI_TOOLS };
36485
+ }
36263
36486
  const handle = [sessionsPanelApp, agentsOverviewApp, bureauSessionsApp, sessionStoryApp, workBoardApp, reviewPanelApp].find(
36264
36487
  (h) => h.id === appId
36265
36488
  );
@@ -36274,10 +36497,14 @@ function builtinPanelCatalogEntries() {
36274
36497
  listTasks: (boardId) => ({ boardId: boardId ?? "ws:default", tasks: [] }),
36275
36498
  // Always provided (never omitted) here, same reasoning as
36276
36499
  // `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: [] })
36500
+ // zipped against this call's output BY INDEX, so the review panel and
36501
+ // the store must always be present in this specific call regardless of
36502
+ // whether the real daemon happens to have a review runner wired or any
36503
+ // store ops at all.
36504
+ listReviews: async () => ({ total: 0, attestations: [] }),
36505
+ listCatalog: async () => [],
36506
+ listInstalled: () => [],
36507
+ listUpdates: () => []
36281
36508
  });
36282
36509
  return apps.map((app, i) => {
36283
36510
  const handle = PANEL_APP_HANDLES[i];
@@ -45778,7 +46005,7 @@ function registerOrchestrationTools(rawServer, opts) {
45778
46005
  "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
46006
  ),
45780
46007
  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."),
46008
+ 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
46009
  limit: z.number().int().min(1).max(200).optional().describe("Max events to return. Default 50."),
45783
46010
  full: z.boolean().optional().describe(
45784
46011
  "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 +47559,7 @@ var RemoteCatalogSchema = z.object({
47332
47559
  generatedAt: z.string().optional(),
47333
47560
  entries: z.array(z.unknown())
47334
47561
  });
47335
- var DEFAULT_CATALOG_SOURCE_URL = "https://cli.agentproto.sh/catalog/v1/apps.json";
47562
+ var DEFAULT_CATALOG_SOURCE_URL = "https://agentproto.sh/catalog/v1/apps.json";
47336
47563
  var EMPTY_CATALOG = { apps: [] };
47337
47564
  function defaultAppCatalogPath() {
47338
47565
  return join(homedir(), ".agentproto", "app-catalog.json");
@@ -47655,6 +47882,82 @@ function notEnabled(tool) {
47655
47882
  `${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
47883
  );
47657
47884
  }
47885
+ function confirmTokenOf(args) {
47886
+ const digest = createHash("sha256");
47887
+ digest.update(
47888
+ JSON.stringify([
47889
+ "agentproto-app-install-confirm-v1",
47890
+ args.url,
47891
+ isAgentappUrl(args.url) ? "agentapp" : "git",
47892
+ args.sha256 ?? null,
47893
+ args.sha ?? null,
47894
+ args.ref ?? null,
47895
+ args.subdir ?? null,
47896
+ args.allowBuild === true
47897
+ ])
47898
+ );
47899
+ return digest.digest("hex").slice(0, 32);
47900
+ }
47901
+ var appInstallNonces = /* @__PURE__ */ new Map();
47902
+ var APP_INSTALL_NONCE_MAX = 256;
47903
+ var APP_INSTALL_NONCE_TTL_MS = 5 * 60 * 1e3;
47904
+ function issueInstallNonce(args) {
47905
+ const now = Date.now();
47906
+ for (const [nonce2, entry] of appInstallNonces) {
47907
+ if (entry.expiresAt <= now) appInstallNonces.delete(nonce2);
47908
+ }
47909
+ if (appInstallNonces.size >= APP_INSTALL_NONCE_MAX) {
47910
+ const oldest = appInstallNonces.keys().next().value;
47911
+ if (oldest !== void 0) appInstallNonces.delete(oldest);
47912
+ }
47913
+ const nonce = randomBytes(16).toString("hex");
47914
+ appInstallNonces.set(nonce, {
47915
+ fingerprint: confirmTokenOf({
47916
+ url: typeof args.url === "string" ? args.url : "",
47917
+ sha256: typeof args.sha256 === "string" ? args.sha256 : void 0,
47918
+ sha: typeof args.sha === "string" ? args.sha : void 0,
47919
+ ref: typeof args.ref === "string" ? args.ref : void 0,
47920
+ subdir: typeof args.subdir === "string" ? args.subdir : void 0,
47921
+ allowBuild: args.allowBuild === true
47922
+ }),
47923
+ expiresAt: now + APP_INSTALL_NONCE_TTL_MS
47924
+ });
47925
+ return nonce;
47926
+ }
47927
+ function appInstallConfirmationClass(args) {
47928
+ const url = args.url;
47929
+ if (typeof url !== "string" || url.trim() === "" || args.dir !== void 0 || args.file !== void 0) {
47930
+ return void 0;
47931
+ }
47932
+ if (typeof args.confirm !== "string") return { confirmed: false };
47933
+ const nonce = appInstallNonces.get(args.confirm);
47934
+ appInstallNonces.delete(args.confirm);
47935
+ const fingerprint = confirmTokenOf({
47936
+ url,
47937
+ sha256: typeof args.sha256 === "string" ? args.sha256 : void 0,
47938
+ sha: typeof args.sha === "string" ? args.sha : void 0,
47939
+ ref: typeof args.ref === "string" ? args.ref : void 0,
47940
+ subdir: typeof args.subdir === "string" ? args.subdir : void 0,
47941
+ allowBuild: args.allowBuild === true
47942
+ });
47943
+ const valid = nonce !== void 0 && nonce.fingerprint === fingerprint && Date.now() < nonce.expiresAt;
47944
+ return valid ? { confirmed: true } : { confirmed: false };
47945
+ }
47946
+ function buildAppInstallPreview(args) {
47947
+ const url = typeof args.url === "string" ? args.url : "";
47948
+ const isAgentapp = isAgentappUrl(url);
47949
+ return {
47950
+ needsConfirmation: true,
47951
+ confirm: issueInstallNonce(args),
47952
+ kind: isAgentapp ? "agentapp" : "git",
47953
+ url,
47954
+ ...typeof args.sha256 === "string" ? { sha256: args.sha256 } : {},
47955
+ ...typeof args.sha === "string" ? { sha: args.sha } : {},
47956
+ ...typeof args.ref === "string" ? { ref: args.ref } : {},
47957
+ ...typeof args.subdir === "string" ? { subdir: args.subdir } : {},
47958
+ runsBuildCommand: !isAgentapp && args.allowBuild === true
47959
+ };
47960
+ }
47658
47961
  async function dispatchAllowlistedAppTool(declaredAllowlist, input, deps2) {
47659
47962
  const effectiveAllowlist = [...declaredAllowlist, ...APP_UI_DISCOVERY_TOOLS];
47660
47963
  if (!effectiveAllowlist.includes(input.tool)) {
@@ -47663,6 +47966,9 @@ async function dispatchAllowlistedAppTool(declaredAllowlist, input, deps2) {
47663
47966
  );
47664
47967
  }
47665
47968
  const args = input.args ?? {};
47969
+ if (input.tool === "app_install" && appInstallConfirmationClass(args)?.confirmed === false) {
47970
+ return textResult2(buildAppInstallPreview(args));
47971
+ }
47666
47972
  try {
47667
47973
  if (input.tool.startsWith("imported:")) {
47668
47974
  if (!deps2.callImportedTool) return notEnabled("app_tool_call");
@@ -50263,16 +50569,7 @@ function projectSessionForHttp(session, fields) {
50263
50569
  var DEFAULT_ALLOWED_ORIGINS = [
50264
50570
  "http://localhost:*",
50265
50571
  "http://127.0.0.1:*",
50266
- "https://localhost:*",
50267
- // The canonical hosted agentproto panel (github.com/agentproto/cli-site,
50268
- // deployed at cli.agentproto.sh). It drives the user's OWN local daemon
50269
- // from the browser: read-only GETs (session list, SSE stream) are ungated
50270
- // and already work, but the /sessions/:id/pty WebSocket upgrade IS gated —
50271
- // so a PTY terminal in the panel 401s unless this first-party origin is
50272
- // trusted like localhost. A malicious page can't forge this Origin (the
50273
- // browser sets it), so the trust is scoped to agentproto's own panel,
50274
- // matching how guilde.work is trusted. Drop it via `strictOrigins`.
50275
- "https://cli.agentproto.sh"
50572
+ "https://localhost:*"
50276
50573
  ];
50277
50574
  var DEFAULT_FRAME_ANCESTORS = ["vscode-webview:", "vscode-file:"];
50278
50575
  var EVENTS_KEEPALIVE_MS = 25e3;
@@ -50447,7 +50744,9 @@ async function startHttpServer(opts) {
50447
50744
  }
50448
50745
  }
50449
50746
  }
50450
- async function serveMcp(req, res, server2) {
50747
+ async function serveMcp(req, res, server2, observedSessionId) {
50748
+ const rawVersionHeader = req.headers["mcp-protocol-version"];
50749
+ const clientProtocolVersion = Array.isArray(rawVersionHeader) ? rawVersionHeader[0] : rawVersionHeader;
50451
50750
  coerceUnsupportedProtocolVersionHeader(req);
50452
50751
  const transport = new StreamableHTTPServerTransport({
50453
50752
  sessionIdGenerator: void 0
@@ -50466,9 +50765,25 @@ async function startHttpServer(opts) {
50466
50765
  void transport.close();
50467
50766
  void server2.close();
50468
50767
  });
50768
+ const observations = observedSessionId ? opts.mcpObservations : void 0;
50769
+ let sawMessage = false;
50469
50770
  try {
50470
50771
  await server2.connect(transport);
50772
+ if (observations && observedSessionId) {
50773
+ observations.observe(transport, {
50774
+ sessionId: observedSessionId,
50775
+ ...clientProtocolVersion ? { clientProtocolVersion } : {}
50776
+ });
50777
+ const innerOnMessage = transport.onmessage;
50778
+ transport.onmessage = (message, extra) => {
50779
+ sawMessage = true;
50780
+ innerOnMessage?.(message, extra);
50781
+ };
50782
+ }
50471
50783
  await transport.handleRequest(req, res);
50784
+ if (observations && observedSessionId && !sawMessage && req.method === "POST" && res.statusCode >= 400) {
50785
+ observations.recordHttpError(observedSessionId, res.statusCode);
50786
+ }
50472
50787
  } catch (err) {
50473
50788
  const line = mcpErrorLogGate.onFailure(
50474
50789
  "mcp:handleRequest",
@@ -50539,7 +50854,7 @@ async function startHttpServer(opts) {
50539
50854
  return;
50540
50855
  }
50541
50856
  const server2 = await opts.mcpServerFactory(denyTools, callerSessionId, origin, deferred, allowTools, surface);
50542
- await serveMcp(req, res, server2);
50857
+ await serveMcp(req, res, server2, callerSessionId);
50543
50858
  }
50544
50859
  async function handleOrchestratorMcp(req, res) {
50545
50860
  if (!opts.orchestratorMcpServerFactory || !opts.verifyOrchestratorScope) {
@@ -51195,7 +51510,8 @@ async function startHttpServer(opts) {
51195
51510
  opts.webhookNotifier,
51196
51511
  opts.ensureLlmEndpointRunning,
51197
51512
  opts.listAgentAdapters,
51198
- opts.deviceMirrorSync
51513
+ opts.deviceMirrorSync,
51514
+ opts.mcpObservations
51199
51515
  );
51200
51516
  if (handled) return;
51201
51517
  }
@@ -52421,6 +52737,12 @@ async function startHttpServer(opts) {
52421
52737
  });
52422
52738
  return;
52423
52739
  }
52740
+ if (path === "/store" && req.method === "GET") {
52741
+ const target = normalizeStoreRedirectUrl(req.url ?? "/store");
52742
+ res.writeHead(302, { location: target });
52743
+ res.end();
52744
+ return;
52745
+ }
52424
52746
  if (opts.appRegistry && path.startsWith("/apps/")) {
52425
52747
  const appUiShellBlocked = (appId) => !iframeEmbedOriginAllowed(req, opts.appRegistry.getApp(appId) ?? {}) && !isValidAppEmbedToken(requestEmbedToken(req)) && guardBrowserOrigin(req, res);
52426
52748
  const assetMatch = path.match(APP_UI_ASSET_RE);
@@ -53281,7 +53603,7 @@ async function handleSandboxes(_req, res, path, resolveSandboxProvider2) {
53281
53603
  json3(200, { alive: true, ...probe.state ? { state: probe.state } : { state: row.state }, checkedAt });
53282
53604
  return true;
53283
53605
  }
53284
- async function handleSessions(req, res, path, registry, resolveAgentAdapter, ptyEnabled = false, resolveBrowserAdapter, listBrowserAdapters, sessionEvents, eventRing, buildOrchestratorMcp, daemonMcpUrl, provisionWorktree, listCatalogModels, resolveSandboxProvider2, webhookNotifier, ensureLlmEndpointRunning, listAgentAdapters, deviceMirrorSync) {
53606
+ 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
53607
  const json3 = (status, body) => {
53286
53608
  res.writeHead(status, { "content-type": "application/json" });
53287
53609
  res.end(JSON.stringify(body));
@@ -54329,7 +54651,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
54329
54651
  }
54330
54652
  const fresh = registry.get(desc.id) ?? desc;
54331
54653
  const pendingPermissions = registry.listPendingPermissions({ sessionId: fresh.id }).length;
54332
- json3(200, buildSessionCapabilities(fresh, pendingPermissions));
54654
+ json3(200, buildSessionCapabilities(fresh, pendingPermissions, mcpObservations?.get(fresh.id)));
54333
54655
  return true;
54334
54656
  }
54335
54657
  const renameMatch = path.match(/^\/sessions\/([^/]+)$/);
@@ -56690,6 +57012,11 @@ async function handleAppUiPage(req, res, appId, appRegistry, frameAncestors) {
56690
57012
  sendRepresentation(req, res, rep, headers);
56691
57013
  }
56692
57014
  var APP_UI_PAGE_RE = /^\/apps\/(.+)\/ui\/?$/;
57015
+ function normalizeStoreRedirectUrl(rawUrl) {
57016
+ const queryIndex = rawUrl.indexOf("?");
57017
+ const query = queryIndex >= 0 ? rawUrl.slice(queryIndex) : "";
57018
+ return `/apps/${STORE_PANEL_APP_ID.split("/")[0]}/${STORE_PANEL_APP_ID.split("/")[1]}/ui${query}`;
57019
+ }
56693
57020
  var APP_UI_ASSET_RE = /^\/apps\/(.+?)\/ui\/assets\/([^/]*)$/;
56694
57021
  function isAppUiShellRequest(method, path) {
56695
57022
  if (method !== "GET") return false;
@@ -64021,7 +64348,9 @@ var RemoteController = class {
64021
64348
  result.bearerToken = bearerToken;
64022
64349
  result.mcpEndpoint = mcpEndpoint;
64023
64350
  result.mcpConfigSnippet = buildMcpConfigSnippet2(mcpEndpoint, bearerToken);
64024
- result.phoneUrl = this.opts.isSessionChatInstalled?.() ? `${started.publicUrl}/apps/${SESSION_CHAT_APP_ID}/ui#token=${bearerToken}` : `https://cli.agentproto.sh/panel#daemon=${encodeURIComponent(started.publicUrl)}&token=${bearerToken}`;
64351
+ if (this.opts.isSessionChatInstalled?.()) {
64352
+ result.phoneUrl = `${started.publicUrl}/apps/${SESSION_CHAT_APP_ID}/ui#token=${bearerToken}`;
64353
+ }
64025
64354
  } else {
64026
64355
  const warning = `\u26A0 ${started.publicUrl} forwards to ${target.host}:${target.port} with NO authentication from the daemon \u2014 anyone with the URL can reach it. Ensure the upstream service enforces its own auth, and run remote_disable when done.`;
64027
64356
  result.warning = warning;
@@ -64115,7 +64444,7 @@ function registerRemoteTools(server, opts) {
64115
64444
  const { controller } = opts;
64116
64445
  server.tool(
64117
64446
  "remote_enable",
64118
- "Publish a local port to the internet via Cloudflare. By default exposes this gateway and gates it with a bearer token (returned ONCE in the response \u2014 store it; only its hash is persisted). Pass `targetPort` to tunnel a different local service (Supabase, dev server, \u2026); in that mode the daemon does not gate the proxied traffic \u2014 the upstream service handles its own auth. Returns the public URL plus a paste-ready `.mcp.json` snippet and a `phoneUrl` \u2014 a single link to open on a phone, with the token in a URL fragment (gateway mode only). Re-running while a tunnel is already up errors; call `remote_disable` first to rotate.",
64447
+ "Publish a local port to the internet via Cloudflare. By default exposes this gateway and gates it with a bearer token (returned ONCE in the response \u2014 store it; only its hash is persisted). Pass `targetPort` to tunnel a different local service (Supabase, dev server, \u2026); in that mode the daemon does not gate the proxied traffic \u2014 the upstream service handles its own auth. Returns the public URL plus a paste-ready `.mcp.json` snippet and a `phoneUrl` \u2014 a single link to open on a phone, with the token in a URL fragment (gateway mode, and only when the @agentik/session-chat app is installed). Re-running while a tunnel is already up errors; call `remote_disable` first to rotate.",
64119
64448
  {
64120
64449
  provider: z.enum(["quick"]).optional().describe(
64121
64450
  "Tunnel backend. `quick` = Cloudflare Quick Tunnel (no API key, ephemeral *.trycloudflare.com URL). Defaults to `quick`. Named-CF / Tailscale providers TBD."
@@ -66904,6 +67233,7 @@ async function createGateway(opts) {
66904
67233
  });
66905
67234
  const sessionEvents = createSessionEventBus();
66906
67235
  const eventRing = createEventRing();
67236
+ const mcpObservations = createMcpObservationStore();
66907
67237
  eventRing.wire(sessionEvents);
66908
67238
  const webhookNotifier = createWebhookNotifier();
66909
67239
  sessionEvents.onAny((ev) => webhookNotifier.onSessionEvent(ev));
@@ -67235,6 +67565,11 @@ async function createGateway(opts) {
67235
67565
  webOrigins: daemonConfig.approvals?.webOrigins ?? []
67236
67566
  });
67237
67567
  wireSupervisorNotify({ registry: sessions, sessionEvents });
67568
+ wireMcpDegradedWarnings({
67569
+ sessionEvents,
67570
+ getSession: (id) => sessions.get(id),
67571
+ store: mcpObservations
67572
+ });
67238
67573
  const activityProjector = createActivityProjector({
67239
67574
  registry: sessions,
67240
67575
  sessionEvents,
@@ -67435,6 +67770,7 @@ async function createGateway(opts) {
67435
67770
  }
67436
67771
  registerSessionTools(server, {
67437
67772
  registry: sessions,
67773
+ mcpObservations,
67438
67774
  workspace,
67439
67775
  mcpProxy,
67440
67776
  ptyEnabled: opts.spawnPty != null,
@@ -67539,6 +67875,21 @@ async function createGateway(opts) {
67539
67875
  registerAppDataTools(server, { appRegistry });
67540
67876
  registerAppExternalTools(server, { appRegistry });
67541
67877
  registerTelegramBotTools(server, { telegramCreds: telegramBotCreds });
67878
+ const listCatalogRows = async () => {
67879
+ const rows = await dispatchTool("app_catalog", {});
67880
+ if (typeof rows !== "string") return [];
67881
+ try {
67882
+ const parsed = JSON.parse(rows);
67883
+ return Array.isArray(parsed) ? parsed : [];
67884
+ } catch {
67885
+ return [];
67886
+ }
67887
+ };
67888
+ const listInstalledRows = () => {
67889
+ const apps = appRegistry.listApps();
67890
+ return apps;
67891
+ };
67892
+ 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
67893
  const listSessionsFiltered = (filter) => {
67543
67894
  let rows = sessions.list().filter((s) => s.kind !== "command");
67544
67895
  if (filter === "running") {
@@ -67579,7 +67930,14 @@ async function createGateway(opts) {
67579
67930
  reviewRunner,
67580
67931
  { ...input, includeRunning: true },
67581
67932
  { resolveSubtree: (sessionId) => [...collectSubtree(sessionId, sessions.list({ includeArchived: true }))] }
67582
- )
67933
+ ),
67934
+ // Store widget's read paths — app_catalog's rows (via the SAME
67935
+ // `dispatchTool` the panel's own apply verbs will route through, so
67936
+ // its snapshot can never disagree with what the tool call returns),
67937
+ // and the registry's installed apps.
67938
+ listCatalog: listCatalogRows,
67939
+ listInstalled: listInstalledRows,
67940
+ listUpdates: listUpdateRows
67583
67941
  }),
67584
67942
  // Same ptyEnabled gate as terminal_start/terminal_input/… in
67585
67943
  // session-tools.ts — the panel would be able to open the WS but
@@ -67745,6 +68103,7 @@ async function createGateway(opts) {
67745
68103
  const http = await startHttpServer({
67746
68104
  port,
67747
68105
  bind: opts.bind,
68106
+ mcpObservations,
67748
68107
  // Auth is read on every request via this getter. `opts.auth`
67749
68108
  // (when provided) is the startup default and represents the
67750
68109
  // operator's intent — bearer-only deployments stay bearer-only