@mono-agent/agent-app 0.19.0 → 0.20.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/README.md +304 -32
- package/dist/acp-bridge.d.ts.map +1 -1
- package/dist/acp-bridge.js +113 -54
- package/dist/acp-bridge.js.map +1 -1
- package/dist/acp-session-store.d.ts +12 -0
- package/dist/acp-session-store.d.ts.map +1 -0
- package/dist/acp-session-store.js +74 -0
- package/dist/acp-session-store.js.map +1 -0
- package/dist/agent-artifact-paths.d.ts +8 -0
- package/dist/agent-artifact-paths.d.ts.map +1 -0
- package/dist/agent-artifact-paths.js +11 -0
- package/dist/agent-artifact-paths.js.map +1 -0
- package/dist/agent-root-coordinator.d.ts +101 -0
- package/dist/agent-root-coordinator.d.ts.map +1 -0
- package/dist/agent-root-coordinator.js +470 -0
- package/dist/agent-root-coordinator.js.map +1 -0
- package/dist/app-controller-channels.d.ts +11 -1
- package/dist/app-controller-channels.d.ts.map +1 -1
- package/dist/app-controller-channels.js +145 -20
- package/dist/app-controller-channels.js.map +1 -1
- package/dist/app-controller-continuation.d.ts +2 -0
- package/dist/app-controller-continuation.d.ts.map +1 -1
- package/dist/app-controller-continuation.js.map +1 -1
- package/dist/app-controller-lifecycle.d.ts +5 -0
- package/dist/app-controller-lifecycle.d.ts.map +1 -1
- package/dist/app-controller-lifecycle.js +24 -5
- package/dist/app-controller-lifecycle.js.map +1 -1
- package/dist/app-controller-maintenance.d.ts +2 -0
- package/dist/app-controller-maintenance.d.ts.map +1 -1
- package/dist/app-controller-maintenance.js +32 -2
- package/dist/app-controller-maintenance.js.map +1 -1
- package/dist/app-controller-process-jobs.d.ts +46 -0
- package/dist/app-controller-process-jobs.d.ts.map +1 -0
- package/dist/app-controller-process-jobs.js +281 -0
- package/dist/app-controller-process-jobs.js.map +1 -0
- package/dist/app-controller-responder.d.ts +15 -0
- package/dist/app-controller-responder.d.ts.map +1 -1
- package/dist/app-controller-responder.js +146 -12
- package/dist/app-controller-responder.js.map +1 -1
- package/dist/app-controller-traceability.d.ts +6 -0
- package/dist/app-controller-traceability.d.ts.map +1 -1
- package/dist/app-controller-traceability.js +10 -0
- package/dist/app-controller-traceability.js.map +1 -1
- package/dist/app-controller-utils.d.ts +8 -0
- package/dist/app-controller-utils.d.ts.map +1 -1
- package/dist/app-controller-utils.js +24 -1
- package/dist/app-controller-utils.js.map +1 -1
- package/dist/app-controller.d.ts +40 -1
- package/dist/app-controller.d.ts.map +1 -1
- package/dist/app-controller.js +124 -39
- package/dist/app-controller.js.map +1 -1
- package/dist/background-log-maintenance.d.ts +85 -2
- package/dist/background-log-maintenance.d.ts.map +1 -1
- package/dist/background-log-maintenance.js +407 -136
- package/dist/background-log-maintenance.js.map +1 -1
- package/dist/background-runtime.d.ts.map +1 -1
- package/dist/background-runtime.js +22 -41
- package/dist/background-runtime.js.map +1 -1
- package/dist/background.d.ts +18 -26
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +83 -138
- package/dist/background.js.map +1 -1
- package/dist/channel-drivers/cron.d.ts +12 -1
- package/dist/channel-drivers/cron.d.ts.map +1 -1
- package/dist/channel-drivers/cron.js +121 -18
- package/dist/channel-drivers/cron.js.map +1 -1
- package/dist/channel-drivers/native-notify.d.ts +3 -0
- package/dist/channel-drivers/native-notify.d.ts.map +1 -1
- package/dist/channel-drivers/native-notify.js +2 -1
- package/dist/channel-drivers/native-notify.js.map +1 -1
- package/dist/channel-drivers/slack.d.ts.map +1 -1
- package/dist/channel-drivers/slack.js +41 -4
- package/dist/channel-drivers/slack.js.map +1 -1
- package/dist/channel-drivers/telegram.d.ts.map +1 -1
- package/dist/channel-drivers/telegram.js +46 -3
- package/dist/channel-drivers/telegram.js.map +1 -1
- package/dist/channel-drivers/tui.d.ts +13 -2
- package/dist/channel-drivers/tui.d.ts.map +1 -1
- package/dist/channel-drivers/tui.js +112 -3
- package/dist/channel-drivers/tui.js.map +1 -1
- package/dist/channels.d.ts +1 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +4 -2
- package/dist/channels.js.map +1 -1
- package/dist/cli-args.d.ts +15 -1
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +82 -12
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-background-command.d.ts +9 -2
- package/dist/cli-background-command.d.ts.map +1 -1
- package/dist/cli-background-command.js +174 -66
- package/dist/cli-background-command.js.map +1 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +50 -12
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +21 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +99 -2
- package/dist/config-reference.js.map +1 -1
- package/dist/configured-agent.d.ts +44 -3
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +805 -156
- package/dist/configured-agent.js.map +1 -1
- package/dist/conversation-state-roots.d.ts +59 -0
- package/dist/conversation-state-roots.d.ts.map +1 -0
- package/dist/conversation-state-roots.js +248 -0
- package/dist/conversation-state-roots.js.map +1 -0
- package/dist/cron-control-store.d.ts +91 -0
- package/dist/cron-control-store.d.ts.map +1 -0
- package/dist/cron-control-store.js +1251 -0
- package/dist/cron-control-store.js.map +1 -0
- package/dist/cron-operator-service.d.ts +54 -0
- package/dist/cron-operator-service.d.ts.map +1 -0
- package/dist/cron-operator-service.js +352 -0
- package/dist/cron-operator-service.js.map +1 -0
- package/dist/doctor-session-history.d.ts +9 -0
- package/dist/doctor-session-history.d.ts.map +1 -0
- package/dist/doctor-session-history.js +313 -0
- package/dist/doctor-session-history.js.map +1 -0
- package/dist/doctor.d.ts +2 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +320 -9
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interaction-bridge.d.ts +1 -0
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +54 -6
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/jobs-command.d.ts +17 -0
- package/dist/jobs-command.d.ts.map +1 -0
- package/dist/jobs-command.js +207 -0
- package/dist/jobs-command.js.map +1 -0
- package/dist/launchd-lifecycle-lock.d.ts +36 -0
- package/dist/launchd-lifecycle-lock.d.ts.map +1 -0
- package/dist/launchd-lifecycle-lock.js +124 -0
- package/dist/launchd-lifecycle-lock.js.map +1 -0
- package/dist/launchd-log-monitor-status.d.ts +7 -0
- package/dist/launchd-log-monitor-status.d.ts.map +1 -0
- package/dist/launchd-log-monitor-status.js +224 -0
- package/dist/launchd-log-monitor-status.js.map +1 -0
- package/dist/launchd-logs.d.ts +9 -1
- package/dist/launchd-logs.d.ts.map +1 -1
- package/dist/launchd-logs.js +69 -16
- package/dist/launchd-logs.js.map +1 -1
- package/dist/launchd-maintenance-command.d.ts +10 -0
- package/dist/launchd-maintenance-command.d.ts.map +1 -0
- package/dist/launchd-maintenance-command.js +2 -0
- package/dist/launchd-maintenance-command.js.map +1 -0
- package/dist/launchd-maintenance-entry.d.ts +30 -0
- package/dist/launchd-maintenance-entry.d.ts.map +1 -0
- package/dist/launchd-maintenance-entry.js +206 -0
- package/dist/launchd-maintenance-entry.js.map +1 -0
- package/dist/launchd-maintenance-gate.d.ts +28 -0
- package/dist/launchd-maintenance-gate.d.ts.map +1 -0
- package/dist/launchd-maintenance-gate.js +80 -0
- package/dist/launchd-maintenance-gate.js.map +1 -0
- package/dist/launchd-private-files.d.ts +3 -0
- package/dist/launchd-private-files.d.ts.map +1 -0
- package/dist/launchd-private-files.js +58 -0
- package/dist/launchd-private-files.js.map +1 -0
- package/dist/launchd.d.ts +56 -2
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +210 -5
- package/dist/launchd.js.map +1 -1
- package/dist/local-configuration.d.ts.map +1 -1
- package/dist/local-configuration.js +33 -7
- package/dist/local-configuration.js.map +1 -1
- package/dist/managed-launchd-maintenance-environment.d.ts +3 -0
- package/dist/managed-launchd-maintenance-environment.d.ts.map +1 -0
- package/dist/managed-launchd-maintenance-environment.js +12 -0
- package/dist/managed-launchd-maintenance-environment.js.map +1 -0
- package/dist/managed-runtime-launch-proof.d.ts +11 -0
- package/dist/managed-runtime-launch-proof.d.ts.map +1 -0
- package/dist/managed-runtime-launch-proof.js +59 -0
- package/dist/managed-runtime-launch-proof.js.map +1 -0
- package/dist/managed-runtime-maintenance-entry.d.ts +10 -0
- package/dist/managed-runtime-maintenance-entry.d.ts.map +1 -0
- package/dist/managed-runtime-maintenance-entry.js +244 -0
- package/dist/managed-runtime-maintenance-entry.js.map +1 -0
- package/dist/managed-web-logs.d.ts +29 -11
- package/dist/managed-web-logs.d.ts.map +1 -1
- package/dist/managed-web-logs.js +244 -121
- package/dist/managed-web-logs.js.map +1 -1
- package/dist/managed-web-maintenance-environment.d.ts +6 -0
- package/dist/managed-web-maintenance-environment.d.ts.map +1 -0
- package/dist/managed-web-maintenance-environment.js +19 -0
- package/dist/managed-web-maintenance-environment.js.map +1 -0
- package/dist/mcp-apps.d.ts +37 -0
- package/dist/mcp-apps.d.ts.map +1 -0
- package/dist/mcp-apps.js +1753 -0
- package/dist/mcp-apps.js.map +1 -0
- package/dist/memory-command.d.ts +12 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +510 -1
- package/dist/memory-command.js.map +1 -1
- package/dist/modules/known-tools.d.ts +1 -1
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +2 -0
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/posted-reply-history.d.ts.map +1 -1
- package/dist/posted-reply-history.js +12 -0
- package/dist/posted-reply-history.js.map +1 -1
- package/dist/proactive-notify.d.ts +4 -1
- package/dist/proactive-notify.d.ts.map +1 -1
- package/dist/proactive-notify.js +43 -5
- package/dist/proactive-notify.js.map +1 -1
- package/dist/process-jobs-config.d.ts +82 -0
- package/dist/process-jobs-config.d.ts.map +1 -0
- package/dist/process-jobs-config.js +423 -0
- package/dist/process-jobs-config.js.map +1 -0
- package/dist/process-jobs-context.d.ts +24 -0
- package/dist/process-jobs-context.d.ts.map +1 -0
- package/dist/process-jobs-context.js +135 -0
- package/dist/process-jobs-context.js.map +1 -0
- package/dist/process-jobs-root-registry.d.ts +115 -0
- package/dist/process-jobs-root-registry.d.ts.map +1 -0
- package/dist/process-jobs-root-registry.js +978 -0
- package/dist/process-jobs-root-registry.js.map +1 -0
- package/dist/process-jobs-runtime.d.ts +35 -0
- package/dist/process-jobs-runtime.d.ts.map +1 -0
- package/dist/process-jobs-runtime.js +177 -0
- package/dist/process-jobs-runtime.js.map +1 -0
- package/dist/process-jobs-service.d.ts +70 -0
- package/dist/process-jobs-service.d.ts.map +1 -0
- package/dist/process-jobs-service.js +1989 -0
- package/dist/process-jobs-service.js.map +1 -0
- package/dist/process-jobs-store.d.ts +151 -0
- package/dist/process-jobs-store.d.ts.map +1 -0
- package/dist/process-jobs-store.js +1290 -0
- package/dist/process-jobs-store.js.map +1 -0
- package/dist/reply-artifacts.d.ts +65 -0
- package/dist/reply-artifacts.d.ts.map +1 -0
- package/dist/reply-artifacts.js +1147 -0
- package/dist/reply-artifacts.js.map +1 -0
- package/dist/reply-part-budget.d.ts +16 -0
- package/dist/reply-part-budget.d.ts.map +1 -0
- package/dist/reply-part-budget.js +57 -0
- package/dist/reply-part-budget.js.map +1 -0
- package/dist/request-model-override.d.ts +19 -0
- package/dist/request-model-override.d.ts.map +1 -1
- package/dist/request-model-override.js +54 -1
- package/dist/request-model-override.js.map +1 -1
- package/dist/request-scoped-mcp.d.ts +31 -0
- package/dist/request-scoped-mcp.d.ts.map +1 -0
- package/dist/request-scoped-mcp.js +197 -0
- package/dist/request-scoped-mcp.js.map +1 -0
- package/dist/run-history.d.ts.map +1 -1
- package/dist/run-history.js +50 -254
- package/dist/run-history.js.map +1 -1
- package/dist/runtime-option-extensions.d.ts +21 -0
- package/dist/runtime-option-extensions.d.ts.map +1 -1
- package/dist/runtime-option-extensions.js +53 -0
- package/dist/runtime-option-extensions.js.map +1 -1
- package/dist/secure-file-replace.d.ts +3 -0
- package/dist/secure-file-replace.d.ts.map +1 -1
- package/dist/secure-file-replace.js +31 -7
- package/dist/secure-file-replace.js.map +1 -1
- package/dist/session-history.d.ts +146 -0
- package/dist/session-history.d.ts.map +1 -0
- package/dist/session-history.js +347 -0
- package/dist/session-history.js.map +1 -0
- package/dist/sessions.d.ts +45 -2
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +849 -30
- package/dist/sessions.js.map +1 -1
- package/dist/web-command.d.ts +12 -11
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +554 -80
- package/dist/web-command.js.map +1 -1
- package/dist/web-log-maintenance-command.d.ts +7 -0
- package/dist/web-log-maintenance-command.d.ts.map +1 -0
- package/dist/web-log-maintenance-command.js +21 -0
- package/dist/web-log-maintenance-command.js.map +1 -0
- package/dist/web-log-maintenance-status.d.ts +26 -0
- package/dist/web-log-maintenance-status.d.ts.map +1 -0
- package/dist/web-log-maintenance-status.js +171 -0
- package/dist/web-log-maintenance-status.js.map +1 -0
- package/dist/web-log-maintenance.d.ts +32 -0
- package/dist/web-log-maintenance.d.ts.map +1 -0
- package/dist/web-log-maintenance.js +163 -0
- package/dist/web-log-maintenance.js.map +1 -0
- package/dist/web-maintenance-paths.d.ts +12 -0
- package/dist/web-maintenance-paths.d.ts.map +1 -0
- package/dist/web-maintenance-paths.js +24 -0
- package/dist/web-maintenance-paths.js.map +1 -0
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +1 -0
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.js +1 -1
- package/dist/wizard/run.js.map +1 -1
- package/package.json +15 -15
- package/schema/mono-agent.config.schema.json +102 -1
- package/skills/mono-agent-composer/references/config-blueprint.md +26 -2
- package/skills/mono-agent-composer/references/discovery-questions.md +2 -2
- package/skills/mono-agent-composer/references/feature-coverage.md +6 -4
- package/skills/mono-agent-composer/references/package-map.md +19 -3
- package/skills/mono-agent-composer/references/playbooks.md +3 -3
- package/skills/mono-agent-composer/references/validation.md +4 -2
package/dist/mcp-apps.js
ADDED
|
@@ -0,0 +1,1753 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, realpathSync, renameSync, rmSync, writeSync, } from "node:fs";
|
|
4
|
+
import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { CodedError, MCP_APP_RESOURCE_MIME_TYPE, MCP_APP_SUPPORTED_VERSIONS, } from "@mono-agent/agent-contracts";
|
|
7
|
+
import { createReplyPartBudget, mergeReplyParts, } from "./reply-part-budget.js";
|
|
8
|
+
import { DEFAULT_REPLY_ARTIFACT_STORAGE_MAX_BYTES, ReplyArtifactStorageFullError, replyArtifactStorageBudgetFor, } from "./reply-artifacts.js";
|
|
9
|
+
const APP_ID = /^[0-9a-f]{8}-[0-9a-f-]{27,35}$/u;
|
|
10
|
+
const MAX_HTML_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
const MAX_STATE_BYTES = 1024 * 1024;
|
|
12
|
+
const MAX_BRIDGE_REQUEST_BYTES = 64 * 1024;
|
|
13
|
+
const MAX_BRIDGE_RESULT_BYTES = 1024 * 1024;
|
|
14
|
+
const MAX_MANIFEST_BYTES = 64 * 1024;
|
|
15
|
+
const STATE_JSON_OVERHEAD_BYTES = Buffer.byteLength('{"toolInput":,"toolResult":}\n', "utf8");
|
|
16
|
+
const MAX_STATE_VALUE_BYTES = Math.floor((MAX_STATE_BYTES - STATE_JSON_OVERHEAD_BYTES) / 2);
|
|
17
|
+
const DEFAULT_RETENTION_DAYS = 30;
|
|
18
|
+
const STAGING_NAMESPACE = ".staging";
|
|
19
|
+
const DEFAULT_STAGING_GRACE_MS = 10 * 60 * 1000;
|
|
20
|
+
const DEFAULT_MAX_RETAINED_CONNECTIONS = 8;
|
|
21
|
+
const DEFAULT_CONNECTION_IDLE_MS = 10 * 60 * 1000;
|
|
22
|
+
const DEFAULT_BRIDGE_RATE_LIMIT = 60;
|
|
23
|
+
const DEFAULT_BRIDGE_RATE_WINDOW_MS = 60 * 1000;
|
|
24
|
+
const DEFAULT_AUDIT_MAX_BYTES = 256 * 1024;
|
|
25
|
+
const DEFAULT_AUDIT_RETAINED_FILES = 2;
|
|
26
|
+
export const DEFAULT_MCP_APP_AUDIT_STORAGE_MAX_BYTES = 1024 * 1024;
|
|
27
|
+
// Darwin's O_NOFOLLOW_ANY rejects symbolic links in every path component.
|
|
28
|
+
// Node does not expose it, but openSync accepts the stable fcntl.h value.
|
|
29
|
+
const DARWIN_O_NOFOLLOW_ANY = 0x20000000;
|
|
30
|
+
const BIDI_CONTROL = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu;
|
|
31
|
+
// Intentionally process-global: services sharing one root must share the same
|
|
32
|
+
// append gate and exactly one initialization inventory. Disposing/re-creating a
|
|
33
|
+
// service unregisters only its live/protected-owner predicate; clearing this
|
|
34
|
+
// state would split serialization and repeat the bounded root inventory.
|
|
35
|
+
const mcpAppAuditStorageStates = new Map();
|
|
36
|
+
function mcpAppAuditStorageFor(root, options, beforeOperation, isOwnerProtected) {
|
|
37
|
+
const canonicalRoot = resolve(root);
|
|
38
|
+
const existing = mcpAppAuditStorageStates.get(canonicalRoot);
|
|
39
|
+
if (existing !== undefined
|
|
40
|
+
&& (existing.maxBytes !== options.maxBytes
|
|
41
|
+
|| existing.fileMaxBytes !== options.fileMaxBytes
|
|
42
|
+
|| existing.retainedFiles !== options.retainedFiles
|
|
43
|
+
|| existing.beforeOperation !== beforeOperation)) {
|
|
44
|
+
throw new RangeError("MCP App audit storage options must agree for one artifact root.");
|
|
45
|
+
}
|
|
46
|
+
const state = existing ?? {
|
|
47
|
+
root: canonicalRoot,
|
|
48
|
+
...options,
|
|
49
|
+
beforeOperation,
|
|
50
|
+
owners: new Map(),
|
|
51
|
+
reservations: new Map(),
|
|
52
|
+
protectionPredicates: new Set(),
|
|
53
|
+
initialized: false,
|
|
54
|
+
gate: Promise.resolve(),
|
|
55
|
+
};
|
|
56
|
+
mcpAppAuditStorageStates.set(canonicalRoot, state);
|
|
57
|
+
state.protectionPredicates.add(isOwnerProtected);
|
|
58
|
+
let disposed = false;
|
|
59
|
+
return {
|
|
60
|
+
append: async (directory, line) => await runMcpAppAuditExclusive(state, async () => await appendMcpAppAudit(state, resolve(directory), line)),
|
|
61
|
+
reserveAppend: async (directory, line, reservedBytes) => {
|
|
62
|
+
if (!Number.isSafeInteger(reservedBytes) || reservedBytes < 0 || reservedBytes > state.fileMaxBytes) {
|
|
63
|
+
throw new RangeError("MCP App audit completion reservation is invalid.");
|
|
64
|
+
}
|
|
65
|
+
const canonicalDirectory = resolve(directory);
|
|
66
|
+
const token = Symbol("mcp-app-audit-completion");
|
|
67
|
+
await runMcpAppAuditExclusive(state, async () => {
|
|
68
|
+
await appendMcpAppAudit(state, canonicalDirectory, line, reservedBytes);
|
|
69
|
+
state.reservations.set(token, { directory: canonicalDirectory, bytes: reservedBytes });
|
|
70
|
+
});
|
|
71
|
+
let settled = false;
|
|
72
|
+
return {
|
|
73
|
+
append: async (completionLine) => {
|
|
74
|
+
if (settled)
|
|
75
|
+
throw new Error("MCP App audit completion reservation was already released.");
|
|
76
|
+
settled = true;
|
|
77
|
+
await runMcpAppAuditExclusive(state, async () => {
|
|
78
|
+
const reservation = state.reservations.get(token);
|
|
79
|
+
if (reservation === undefined) {
|
|
80
|
+
throw new Error("MCP App audit completion reservation is unavailable.");
|
|
81
|
+
}
|
|
82
|
+
const incomingBytes = Buffer.byteLength(completionLine, "utf8");
|
|
83
|
+
if (incomingBytes > reservation.bytes) {
|
|
84
|
+
state.reservations.delete(token);
|
|
85
|
+
throw new Error("MCP App audit completion exceeded its reserved bytes.");
|
|
86
|
+
}
|
|
87
|
+
state.reservations.delete(token);
|
|
88
|
+
await appendMcpAppAudit(state, canonicalDirectory, completionLine);
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
release: async () => {
|
|
92
|
+
if (settled)
|
|
93
|
+
return;
|
|
94
|
+
settled = true;
|
|
95
|
+
await runMcpAppAuditExclusive(state, async () => {
|
|
96
|
+
state.reservations.delete(token);
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
remove: async (directory) => await runMcpAppAuditExclusive(state, async () => {
|
|
102
|
+
const canonicalDirectory = resolve(directory);
|
|
103
|
+
assertMcpAppAuditDirectory(state, canonicalDirectory);
|
|
104
|
+
const tracked = state.owners.get(canonicalDirectory);
|
|
105
|
+
if (tracked?.poisoned === true) {
|
|
106
|
+
throw new Error("MCP App audit cleanup refused a poisoned owner.");
|
|
107
|
+
}
|
|
108
|
+
const initial = await lstat(canonicalDirectory);
|
|
109
|
+
if (!initial.isDirectory() || initial.isSymbolicLink()) {
|
|
110
|
+
throw new Error("MCP App audit cleanup refused an unsafe owner directory.");
|
|
111
|
+
}
|
|
112
|
+
const expectedIdentity = tracked?.identity ?? mcpAppAuditIdentity(initial);
|
|
113
|
+
if (!sameMcpAppAuditIdentity(mcpAppAuditIdentity(initial), expectedIdentity)) {
|
|
114
|
+
throw new Error("MCP App audit cleanup refused a replaced owner directory.");
|
|
115
|
+
}
|
|
116
|
+
await beforeMcpAppAuditOperation(state, "remove", canonicalDirectory);
|
|
117
|
+
const current = await lstat(canonicalDirectory);
|
|
118
|
+
if (!current.isDirectory()
|
|
119
|
+
|| current.isSymbolicLink()
|
|
120
|
+
|| !sameMcpAppAuditIdentity(mcpAppAuditIdentity(current), expectedIdentity)) {
|
|
121
|
+
throw new Error("MCP App audit cleanup refused an unsafe owner directory.");
|
|
122
|
+
}
|
|
123
|
+
const opened = await openMcpAppAuditOwner(state, canonicalDirectory, expectedIdentity);
|
|
124
|
+
try {
|
|
125
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
126
|
+
rmSync(mcpAppAuditChildPath(opened.root, basename(canonicalDirectory)), {
|
|
127
|
+
recursive: true,
|
|
128
|
+
force: true,
|
|
129
|
+
});
|
|
130
|
+
assertOpenedMcpAppAuditDirectorySync(state, opened.root);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
await closeOpenedMcpAppAuditOwner(opened);
|
|
134
|
+
}
|
|
135
|
+
if (!state.initialized)
|
|
136
|
+
return;
|
|
137
|
+
replaceMcpAppAuditOwner(state, canonicalDirectory, undefined);
|
|
138
|
+
}),
|
|
139
|
+
dispose: () => {
|
|
140
|
+
if (disposed)
|
|
141
|
+
return;
|
|
142
|
+
disposed = true;
|
|
143
|
+
state.protectionPredicates.delete(isOwnerProtected);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Durable MCP Apps registry with live handles scoped to exact MCP connections. */
|
|
148
|
+
export function createMcpAppService(options) {
|
|
149
|
+
const root = resolve(options.artifactDir, "mcp-apps");
|
|
150
|
+
const stagingRoot = join(root, STAGING_NAMESPACE);
|
|
151
|
+
const retentionDays = options.retentionDays ?? DEFAULT_RETENTION_DAYS;
|
|
152
|
+
const stagingGraceMs = options.stagingGraceMs ?? DEFAULT_STAGING_GRACE_MS;
|
|
153
|
+
const maxRetainedConnections = options.maxRetainedConnections ?? DEFAULT_MAX_RETAINED_CONNECTIONS;
|
|
154
|
+
const connectionIdleMs = options.connectionIdleMs ?? DEFAULT_CONNECTION_IDLE_MS;
|
|
155
|
+
const bridgeRateLimit = options.bridgeRateLimit ?? DEFAULT_BRIDGE_RATE_LIMIT;
|
|
156
|
+
const bridgeRateWindowMs = options.bridgeRateWindowMs ?? DEFAULT_BRIDGE_RATE_WINDOW_MS;
|
|
157
|
+
const auditMaxBytes = options.auditMaxBytes ?? DEFAULT_AUDIT_MAX_BYTES;
|
|
158
|
+
const auditRetainedFiles = options.auditRetainedFiles ?? DEFAULT_AUDIT_RETAINED_FILES;
|
|
159
|
+
const auditStorageMaxBytes = options.auditStorageMaxBytes ?? DEFAULT_MCP_APP_AUDIT_STORAGE_MAX_BYTES;
|
|
160
|
+
const now = options.now ?? (() => new Date());
|
|
161
|
+
const budget = options.replyPartBudget ?? createReplyPartBudget();
|
|
162
|
+
const partsByRun = new Map();
|
|
163
|
+
const partsByIdentityByRun = new Map();
|
|
164
|
+
const invocationIdsByRun = new Map();
|
|
165
|
+
const invocationProtectionsByRun = new Map();
|
|
166
|
+
const liveByInvocation = new Map();
|
|
167
|
+
const connections = new Map();
|
|
168
|
+
const auditProtectionCounts = new Map();
|
|
169
|
+
const responseContext = new AsyncLocalStorage();
|
|
170
|
+
const runOwners = new Map();
|
|
171
|
+
const bridgeRates = new Map();
|
|
172
|
+
const auditQueues = new Map();
|
|
173
|
+
const pendingAuditCompletions = new Set();
|
|
174
|
+
let idleTimer;
|
|
175
|
+
if (!Number.isFinite(retentionDays) || retentionDays <= 0) {
|
|
176
|
+
throw new RangeError("MCP App retentionDays must be positive.");
|
|
177
|
+
}
|
|
178
|
+
for (const [name, value] of [
|
|
179
|
+
["stagingGraceMs", stagingGraceMs],
|
|
180
|
+
["maxRetainedConnections", maxRetainedConnections],
|
|
181
|
+
["connectionIdleMs", connectionIdleMs],
|
|
182
|
+
["bridgeRateLimit", bridgeRateLimit],
|
|
183
|
+
["bridgeRateWindowMs", bridgeRateWindowMs],
|
|
184
|
+
["auditRetainedFiles", auditRetainedFiles],
|
|
185
|
+
["auditStorageMaxBytes", auditStorageMaxBytes],
|
|
186
|
+
]) {
|
|
187
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
188
|
+
throw new RangeError(`MCP App ${name} must be a positive safe integer.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!Number.isSafeInteger(auditMaxBytes) || auditMaxBytes < 1_024) {
|
|
192
|
+
throw new RangeError("MCP App auditMaxBytes must be a safe integer of at least 1024.");
|
|
193
|
+
}
|
|
194
|
+
if (auditMaxBytes > Math.floor(auditStorageMaxBytes / (auditRetainedFiles + 1))) {
|
|
195
|
+
throw new RangeError("MCP App auditStorageMaxBytes must hold the active audit file and every retained rotation.");
|
|
196
|
+
}
|
|
197
|
+
if (auditStorageMaxBytes >= DEFAULT_REPLY_ARTIFACT_STORAGE_MAX_BYTES) {
|
|
198
|
+
throw new RangeError("MCP App auditStorageMaxBytes must be smaller than the aggregate storage ceiling.");
|
|
199
|
+
}
|
|
200
|
+
const contentStorageMaxBytes = DEFAULT_REPLY_ARTIFACT_STORAGE_MAX_BYTES - auditStorageMaxBytes;
|
|
201
|
+
const storage = options.storageBudget
|
|
202
|
+
?? replyArtifactStorageBudgetFor(options.artifactDir, contentStorageMaxBytes);
|
|
203
|
+
if (storage.maxBytes > contentStorageMaxBytes) {
|
|
204
|
+
throw new RangeError("MCP App content and audit storage budgets must fit the aggregate storage ceiling.");
|
|
205
|
+
}
|
|
206
|
+
const auditStorage = mcpAppAuditStorageFor(root, {
|
|
207
|
+
maxBytes: auditStorageMaxBytes,
|
|
208
|
+
fileMaxBytes: auditMaxBytes,
|
|
209
|
+
retainedFiles: auditRetainedFiles,
|
|
210
|
+
}, options.beforeAuditStorageOperation, (directory) => {
|
|
211
|
+
const invocationId = basename(directory);
|
|
212
|
+
if (liveByInvocation.has(invocationId) || auditProtectionCounts.has(invocationId))
|
|
213
|
+
return true;
|
|
214
|
+
for (const protections of invocationProtectionsByRun.values()) {
|
|
215
|
+
if (protections.has(invocationId))
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
});
|
|
220
|
+
const createExtension = async ({ request, runId }) => {
|
|
221
|
+
await mkdir(stagingRoot, { recursive: true, mode: 0o700 });
|
|
222
|
+
const owner = responseContext.getStore();
|
|
223
|
+
owner?.runIds.add(runId);
|
|
224
|
+
runOwners.set(runId, owner);
|
|
225
|
+
await cleanupExpired().catch(() => undefined);
|
|
226
|
+
const host = {
|
|
227
|
+
protocolVersions: MCP_APP_SUPPORTED_VERSIONS,
|
|
228
|
+
mimeTypes: [MCP_APP_RESOURCE_MIME_TYPE],
|
|
229
|
+
register: async (registration) => await register(runId, request.conversationId, registration),
|
|
230
|
+
recordFailure: async (failure) => recordFailure(runId, failure),
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
runtimeOptions: { mcpApps: host },
|
|
234
|
+
cleanup: async () => { },
|
|
235
|
+
settleCleanup: async () => {
|
|
236
|
+
if (runOwners.get(runId) === undefined)
|
|
237
|
+
await discardRun(runId);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
async function register(runId, conversationId, registration) {
|
|
242
|
+
const fail = (message) => {
|
|
243
|
+
const part = recordFailure(runId, {
|
|
244
|
+
serverName: registration.serverName,
|
|
245
|
+
toolName: registration.toolName,
|
|
246
|
+
toolCallId: registration.toolCallId,
|
|
247
|
+
code: "app_resource_invalid",
|
|
248
|
+
message,
|
|
249
|
+
});
|
|
250
|
+
return { part, retainConnection: false };
|
|
251
|
+
};
|
|
252
|
+
if (!isSupportedProtocolVersion(registration.protocolVersion)
|
|
253
|
+
|| registration.connection.connectionId.length === 0
|
|
254
|
+
|| !registration.resourceUri.startsWith("ui://")) {
|
|
255
|
+
return fail("The MCP App registration is incompatible with this host.");
|
|
256
|
+
}
|
|
257
|
+
const resource = appResource(registration.resource, registration.resourceUri);
|
|
258
|
+
if (resource === undefined)
|
|
259
|
+
return fail("The MCP App resource is invalid or oversized.");
|
|
260
|
+
const identity = stableAppIdentity(registration);
|
|
261
|
+
const claim = budget.claim(runId, identity);
|
|
262
|
+
if (claim === "limit")
|
|
263
|
+
return { part: capacityFailure(runId), retainConnection: false };
|
|
264
|
+
if (claim === "duplicate") {
|
|
265
|
+
const existing = partsByIdentityByRun.get(runId)?.get(identity);
|
|
266
|
+
return {
|
|
267
|
+
part: existing?.type === "mcp_app" ? existing : capacityFailure(runId),
|
|
268
|
+
retainConnection: false,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const invocationId = randomUUID();
|
|
272
|
+
const createdAt = now();
|
|
273
|
+
const manifest = {
|
|
274
|
+
schema: 1,
|
|
275
|
+
invocationId,
|
|
276
|
+
connectionId: registration.connection.connectionId,
|
|
277
|
+
runId,
|
|
278
|
+
conversationId,
|
|
279
|
+
serverName: registration.serverName,
|
|
280
|
+
toolName: registration.toolName,
|
|
281
|
+
toolCallId: registration.toolCallId,
|
|
282
|
+
resourceUri: registration.resourceUri,
|
|
283
|
+
protocolVersion: registration.protocolVersion,
|
|
284
|
+
mediaType: MCP_APP_RESOURCE_MIME_TYPE,
|
|
285
|
+
...(registration.title === undefined ? {} : { title: boundedText(registration.title, 240) }),
|
|
286
|
+
...(registration.description === undefined ? {} : { description: boundedText(registration.description, 1_000) }),
|
|
287
|
+
appVisibleTools: [...new Set(registration.appVisibleTools.filter(safeToolName))].slice(0, 256),
|
|
288
|
+
appVisibleResources: [registration.resourceUri],
|
|
289
|
+
...(resource.metadata === undefined ? {} : { resourceMetadata: resource.metadata }),
|
|
290
|
+
createdAt: createdAt.toISOString(),
|
|
291
|
+
expiresAt: new Date(createdAt.getTime() + retentionDays * 86_400_000).toISOString(),
|
|
292
|
+
};
|
|
293
|
+
const state = {
|
|
294
|
+
toolInput: boundedJsonValue(registration.toolInput, MAX_STATE_VALUE_BYTES),
|
|
295
|
+
toolResult: boundedJsonValue(registration.toolResult, MAX_STATE_VALUE_BYTES),
|
|
296
|
+
};
|
|
297
|
+
const storageProtection = await storage.protect("mcp-apps", invocationId);
|
|
298
|
+
let persisted = false;
|
|
299
|
+
try {
|
|
300
|
+
await writeAppAtomically(root, stagingRoot, manifest, resource.html, state, storage, options.beforePublicationCommit);
|
|
301
|
+
persisted = true;
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
budget.unclaim(runId, identity);
|
|
305
|
+
return fail(error instanceof ReplyArtifactStorageFullError
|
|
306
|
+
? "Reply artifact storage is full; this MCP App was not retained."
|
|
307
|
+
: "The MCP App resource could not be persisted safely.");
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
if (!persisted)
|
|
311
|
+
await storageProtection.release();
|
|
312
|
+
}
|
|
313
|
+
const part = partFromManifest(manifest);
|
|
314
|
+
acceptClaimedPart(runId, identity, part);
|
|
315
|
+
const invocationIds = invocationIdsByRun.get(runId) ?? new Set();
|
|
316
|
+
invocationIds.add(invocationId);
|
|
317
|
+
invocationIdsByRun.set(runId, invocationIds);
|
|
318
|
+
const protections = invocationProtectionsByRun.get(runId)
|
|
319
|
+
?? new Map();
|
|
320
|
+
protections.set(invocationId, storageProtection);
|
|
321
|
+
invocationProtectionsByRun.set(runId, protections);
|
|
322
|
+
liveByInvocation.set(invocationId, {
|
|
323
|
+
connectionId: registration.connection.connectionId,
|
|
324
|
+
appVisibleTools: new Set(manifest.appVisibleTools),
|
|
325
|
+
appVisibleResources: new Set(manifest.appVisibleResources),
|
|
326
|
+
});
|
|
327
|
+
await retainConnection(invocationId, registration.connection);
|
|
328
|
+
return { part, retainConnection: true };
|
|
329
|
+
}
|
|
330
|
+
function capacityFailure(runId) {
|
|
331
|
+
return {
|
|
332
|
+
type: "failure",
|
|
333
|
+
id: stablePartId("mcp-app-limit", runId),
|
|
334
|
+
code: "app_capability_mismatch",
|
|
335
|
+
message: "This reply already reached the maximum number of rich parts.",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function recordFailure(runId, input) {
|
|
339
|
+
const identity = `mcp-app-failure:${stableDigest(input)}`;
|
|
340
|
+
const part = {
|
|
341
|
+
type: "failure",
|
|
342
|
+
id: stablePartId(`mcp-app-${safeIdSegment(input.toolCallId)}`, identity),
|
|
343
|
+
code: input.code,
|
|
344
|
+
message: boundedText(input.message, 1_000),
|
|
345
|
+
};
|
|
346
|
+
const claim = budget.claim(runId, identity);
|
|
347
|
+
if (claim === "limit")
|
|
348
|
+
return capacityFailure(runId);
|
|
349
|
+
if (claim === "duplicate") {
|
|
350
|
+
const existing = partsByIdentityByRun.get(runId)?.get(identity);
|
|
351
|
+
return existing?.type === "failure" ? existing : capacityFailure(runId);
|
|
352
|
+
}
|
|
353
|
+
acceptClaimedPart(runId, identity, part);
|
|
354
|
+
return part;
|
|
355
|
+
}
|
|
356
|
+
function acceptClaimedPart(runId, identity, part) {
|
|
357
|
+
const current = partsByRun.get(runId) ?? [];
|
|
358
|
+
const byIdentity = partsByIdentityByRun.get(runId) ?? new Map();
|
|
359
|
+
current.push(part);
|
|
360
|
+
byIdentity.set(identity, part);
|
|
361
|
+
partsByRun.set(runId, current);
|
|
362
|
+
partsByIdentityByRun.set(runId, byIdentity);
|
|
363
|
+
}
|
|
364
|
+
function protectAuditInvocation(invocationId) {
|
|
365
|
+
auditProtectionCounts.set(invocationId, (auditProtectionCounts.get(invocationId) ?? 0) + 1);
|
|
366
|
+
let released = false;
|
|
367
|
+
return () => {
|
|
368
|
+
if (released)
|
|
369
|
+
return;
|
|
370
|
+
released = true;
|
|
371
|
+
const count = auditProtectionCounts.get(invocationId) ?? 0;
|
|
372
|
+
if (count <= 1)
|
|
373
|
+
auditProtectionCounts.delete(invocationId);
|
|
374
|
+
else
|
|
375
|
+
auditProtectionCounts.set(invocationId, count - 1);
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
async function load(request) {
|
|
379
|
+
const releaseAuditProtection = protectAuditInvocation(request.invocationId);
|
|
380
|
+
let protection;
|
|
381
|
+
try {
|
|
382
|
+
protection = await storage.protect("mcp-apps", request.invocationId);
|
|
383
|
+
await evictIdleConnections();
|
|
384
|
+
const manifest = await readManifest(root, request.invocationId);
|
|
385
|
+
authorizeApp(manifest, request);
|
|
386
|
+
if (Date.parse(manifest.expiresAt) <= now().getTime()) {
|
|
387
|
+
throw new CodedError("app_expired", "The MCP App has expired.");
|
|
388
|
+
}
|
|
389
|
+
const directory = join(root, manifest.invocationId);
|
|
390
|
+
const [html, state] = await Promise.all([
|
|
391
|
+
readBoundedText(join(directory, "resource.html"), MAX_HTML_BYTES),
|
|
392
|
+
readBoundedJson(join(directory, "state.json"), MAX_STATE_BYTES),
|
|
393
|
+
]);
|
|
394
|
+
const live = liveByInvocation.get(manifest.invocationId);
|
|
395
|
+
const retained = live === undefined ? undefined : connections.get(live.connectionId);
|
|
396
|
+
const connected = retained?.connection.connectionId === manifest.connectionId;
|
|
397
|
+
if (connected)
|
|
398
|
+
touchConnection(manifest.connectionId);
|
|
399
|
+
return {
|
|
400
|
+
app: partFromManifest(manifest),
|
|
401
|
+
html,
|
|
402
|
+
toolInput: state.toolInput,
|
|
403
|
+
toolResult: state.toolResult,
|
|
404
|
+
...(manifest.resourceMetadata === undefined ? {} : { resourceMetadata: manifest.resourceMetadata }),
|
|
405
|
+
connected,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
await protection?.release();
|
|
410
|
+
releaseAuditProtection();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function request(input) {
|
|
414
|
+
const releaseAuditProtection = protectAuditInvocation(input.invocationId);
|
|
415
|
+
let protection;
|
|
416
|
+
try {
|
|
417
|
+
protection = await storage.protect("mcp-apps", input.invocationId);
|
|
418
|
+
await evictIdleConnections();
|
|
419
|
+
const manifest = await readManifest(root, input.invocationId);
|
|
420
|
+
authorizeApp(manifest, input);
|
|
421
|
+
if (Date.parse(manifest.expiresAt) <= now().getTime()) {
|
|
422
|
+
throw new CodedError("app_expired", "The MCP App has expired.");
|
|
423
|
+
}
|
|
424
|
+
if (serializedBytes(input.params) > MAX_BRIDGE_REQUEST_BYTES) {
|
|
425
|
+
throw new CodedError("app_request_too_large", "The MCP App bridge request is too large.");
|
|
426
|
+
}
|
|
427
|
+
const live = liveByInvocation.get(manifest.invocationId);
|
|
428
|
+
const retained = live === undefined ? undefined : connections.get(live.connectionId);
|
|
429
|
+
if (live === undefined
|
|
430
|
+
|| retained === undefined
|
|
431
|
+
|| retained.connection.connectionId !== manifest.connectionId) {
|
|
432
|
+
throw new CodedError("app_connection_closed", "The originating MCP connection is no longer available.");
|
|
433
|
+
}
|
|
434
|
+
consumeBridgeRate(manifest.connectionId);
|
|
435
|
+
touchConnection(manifest.connectionId);
|
|
436
|
+
if (input.method === "tools/call") {
|
|
437
|
+
requireConfirmation(input);
|
|
438
|
+
const params = record(input.params);
|
|
439
|
+
const name = typeof params?.name === "string" ? params.name : undefined;
|
|
440
|
+
if (name === undefined || !live.appVisibleTools.has(name)) {
|
|
441
|
+
throw new CodedError("app_tool_forbidden", "The MCP App requested a tool that is not visible to apps.");
|
|
442
|
+
}
|
|
443
|
+
const completionAudit = await reserveAuditCompletion(manifest, input.method);
|
|
444
|
+
let result;
|
|
445
|
+
try {
|
|
446
|
+
result = await retained.connection.callTool(name, params?.arguments ?? {});
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
await completionAudit.release().catch(() => undefined);
|
|
450
|
+
await closeConnection(manifest.connectionId);
|
|
451
|
+
throw new CodedError("app_connection_closed", "The originating MCP connection closed during the tool call.");
|
|
452
|
+
}
|
|
453
|
+
touchConnection(manifest.connectionId);
|
|
454
|
+
try {
|
|
455
|
+
await completionAudit.complete();
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
throw new CodedError("app_audit_incomplete", "The MCP App tool ran, but completion could not be recorded safely; do not retry automatically.");
|
|
459
|
+
}
|
|
460
|
+
return boundedJsonValue(result, MAX_BRIDGE_RESULT_BYTES);
|
|
461
|
+
}
|
|
462
|
+
if (input.method === "resources/read") {
|
|
463
|
+
const params = record(input.params);
|
|
464
|
+
const uri = typeof params?.uri === "string" ? params.uri : undefined;
|
|
465
|
+
if (uri === undefined || Buffer.byteLength(uri, "utf8") > 4_096) {
|
|
466
|
+
throw new CodedError("app_resource_invalid", "The MCP App requested an invalid resource URI.");
|
|
467
|
+
}
|
|
468
|
+
if (!live.appVisibleResources.has(uri)) {
|
|
469
|
+
throw new CodedError("app_resource_forbidden", "The MCP App requested a resource that was not declared for this app invocation.");
|
|
470
|
+
}
|
|
471
|
+
await appendAudit(manifest, input.method, "requested");
|
|
472
|
+
try {
|
|
473
|
+
const result = await retained.connection.readResource(uri);
|
|
474
|
+
touchConnection(manifest.connectionId);
|
|
475
|
+
return boundedJsonValue(result, MAX_BRIDGE_RESULT_BYTES);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
await closeConnection(manifest.connectionId);
|
|
479
|
+
throw new CodedError("app_connection_closed", "The originating MCP connection closed during resource access.");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (input.method === "ui/open-link") {
|
|
483
|
+
requireConfirmation(input);
|
|
484
|
+
const params = record(input.params);
|
|
485
|
+
const url = typeof params?.url === "string" ? safeExternalUrl(params.url) : undefined;
|
|
486
|
+
if (url === undefined)
|
|
487
|
+
throw new CodedError("app_open_link_forbidden", "Only HTTP(S) links can be opened.");
|
|
488
|
+
await appendAudit(manifest, input.method, "confirmed");
|
|
489
|
+
return { allowed: true, url };
|
|
490
|
+
}
|
|
491
|
+
requireConfirmation(input);
|
|
492
|
+
await appendAudit(manifest, input.method, "rejected");
|
|
493
|
+
return {
|
|
494
|
+
accepted: false,
|
|
495
|
+
reason: "Model-context updates require a new user-authorized turn and are not applied to a completed run.",
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
await protection?.release();
|
|
500
|
+
releaseAuditProtection();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function cleanupExpired() {
|
|
504
|
+
await storage.runExclusive(async (isProtected) => {
|
|
505
|
+
let entries;
|
|
506
|
+
try {
|
|
507
|
+
entries = await readdir(root);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
if (error.code === "ENOENT")
|
|
511
|
+
return;
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
const cutoff = now().getTime();
|
|
515
|
+
for (const entry of entries.filter((value) => APP_ID.test(value))) {
|
|
516
|
+
if (isProtected("mcp-apps", entry))
|
|
517
|
+
continue;
|
|
518
|
+
try {
|
|
519
|
+
const manifest = await readManifest(root, entry);
|
|
520
|
+
if (Date.parse(manifest.expiresAt) > cutoff)
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
catch { }
|
|
524
|
+
await dropInvocation(entry);
|
|
525
|
+
await auditStorage.remove(join(root, entry)).catch(() => undefined);
|
|
526
|
+
}
|
|
527
|
+
await cleanupStaging(stagingRoot, cutoff - stagingGraceMs, (id) => isProtected("mcp-apps", id));
|
|
528
|
+
});
|
|
529
|
+
await evictIdleConnections();
|
|
530
|
+
}
|
|
531
|
+
async function closeConnection(connectionId) {
|
|
532
|
+
const retained = connections.get(connectionId);
|
|
533
|
+
connections.delete(connectionId);
|
|
534
|
+
bridgeRates.delete(connectionId);
|
|
535
|
+
for (const [invocationId, live] of liveByInvocation) {
|
|
536
|
+
if (live.connectionId === connectionId)
|
|
537
|
+
liveByInvocation.delete(invocationId);
|
|
538
|
+
}
|
|
539
|
+
await retained?.connection.close().catch(() => undefined);
|
|
540
|
+
scheduleIdleEviction();
|
|
541
|
+
}
|
|
542
|
+
async function dropInvocation(invocationId) {
|
|
543
|
+
const live = liveByInvocation.get(invocationId);
|
|
544
|
+
liveByInvocation.delete(invocationId);
|
|
545
|
+
auditQueues.delete(invocationId);
|
|
546
|
+
if (live === undefined)
|
|
547
|
+
return;
|
|
548
|
+
const retained = connections.get(live.connectionId);
|
|
549
|
+
retained?.invocationIds.delete(invocationId);
|
|
550
|
+
if (retained?.invocationIds.size === 0)
|
|
551
|
+
await closeConnection(live.connectionId);
|
|
552
|
+
}
|
|
553
|
+
async function retainConnection(invocationId, connection) {
|
|
554
|
+
await evictIdleConnections();
|
|
555
|
+
const retained = connections.get(connection.connectionId) ?? {
|
|
556
|
+
connection,
|
|
557
|
+
invocationIds: new Set(),
|
|
558
|
+
lastUsedAtMs: now().getTime(),
|
|
559
|
+
};
|
|
560
|
+
retained.invocationIds.add(invocationId);
|
|
561
|
+
retained.lastUsedAtMs = now().getTime();
|
|
562
|
+
connections.delete(connection.connectionId);
|
|
563
|
+
connections.set(connection.connectionId, retained);
|
|
564
|
+
while (connections.size > maxRetainedConnections) {
|
|
565
|
+
const oldest = connections.keys().next().value;
|
|
566
|
+
if (oldest === undefined)
|
|
567
|
+
break;
|
|
568
|
+
await closeConnection(oldest);
|
|
569
|
+
}
|
|
570
|
+
scheduleIdleEviction();
|
|
571
|
+
}
|
|
572
|
+
function touchConnection(connectionId) {
|
|
573
|
+
const retained = connections.get(connectionId);
|
|
574
|
+
if (retained === undefined)
|
|
575
|
+
return;
|
|
576
|
+
retained.lastUsedAtMs = now().getTime();
|
|
577
|
+
connections.delete(connectionId);
|
|
578
|
+
connections.set(connectionId, retained);
|
|
579
|
+
scheduleIdleEviction();
|
|
580
|
+
}
|
|
581
|
+
async function evictIdleConnections() {
|
|
582
|
+
const cutoff = now().getTime() - connectionIdleMs;
|
|
583
|
+
const idle = [...connections]
|
|
584
|
+
.filter(([, retained]) => retained.lastUsedAtMs <= cutoff)
|
|
585
|
+
.map(([connectionId]) => connectionId);
|
|
586
|
+
await Promise.allSettled(idle.map(async (connectionId) => await closeConnection(connectionId)));
|
|
587
|
+
scheduleIdleEviction();
|
|
588
|
+
}
|
|
589
|
+
function scheduleIdleEviction() {
|
|
590
|
+
if (idleTimer !== undefined)
|
|
591
|
+
clearTimeout(idleTimer);
|
|
592
|
+
idleTimer = undefined;
|
|
593
|
+
if (connections.size === 0)
|
|
594
|
+
return;
|
|
595
|
+
const nextExpiry = Math.min(...[...connections.values()]
|
|
596
|
+
.map((retained) => retained.lastUsedAtMs + connectionIdleMs));
|
|
597
|
+
const delay = Math.max(1, Math.min(2_147_483_647, nextExpiry - now().getTime()));
|
|
598
|
+
idleTimer = setTimeout(() => {
|
|
599
|
+
idleTimer = undefined;
|
|
600
|
+
void evictIdleConnections();
|
|
601
|
+
}, delay);
|
|
602
|
+
idleTimer.unref?.();
|
|
603
|
+
}
|
|
604
|
+
function consumeBridgeRate(connectionId) {
|
|
605
|
+
const at = now().getTime();
|
|
606
|
+
const current = bridgeRates.get(connectionId);
|
|
607
|
+
const bucket = current === undefined || at - current.startedAtMs >= bridgeRateWindowMs
|
|
608
|
+
? { startedAtMs: at, count: 0 }
|
|
609
|
+
: current;
|
|
610
|
+
if (bucket.count >= bridgeRateLimit) {
|
|
611
|
+
throw new CodedError("app_rate_limited", "The MCP App bridge request rate limit was exceeded.");
|
|
612
|
+
}
|
|
613
|
+
bucket.count += 1;
|
|
614
|
+
bridgeRates.set(connectionId, bucket);
|
|
615
|
+
}
|
|
616
|
+
async function appendAudit(manifest, method, phase) {
|
|
617
|
+
const line = mcpAppAuditLine(manifest, method, phase);
|
|
618
|
+
await enqueueAudit(manifest.invocationId, async () => {
|
|
619
|
+
await auditStorage.append(join(root, manifest.invocationId), line);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
async function reserveAuditCompletion(manifest, method) {
|
|
623
|
+
const confirmedLine = mcpAppAuditLine(manifest, method, "confirmed");
|
|
624
|
+
const completionBytes = Buffer.byteLength(mcpAppAuditLine(manifest, method, "completed"), "utf8");
|
|
625
|
+
const storageReservation = await enqueueAudit(manifest.invocationId, async () => (await auditStorage.reserveAppend(join(root, manifest.invocationId), confirmedLine, completionBytes)));
|
|
626
|
+
let settled = false;
|
|
627
|
+
let reservation;
|
|
628
|
+
reservation = {
|
|
629
|
+
complete: async () => {
|
|
630
|
+
if (settled)
|
|
631
|
+
throw new Error("MCP App audit completion was already settled.");
|
|
632
|
+
settled = true;
|
|
633
|
+
try {
|
|
634
|
+
const line = mcpAppAuditLine(manifest, method, "completed");
|
|
635
|
+
await enqueueAudit(manifest.invocationId, async () => await storageReservation.append(line));
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
await storageReservation.release().catch(() => undefined);
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
pendingAuditCompletions.delete(reservation);
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
release: async () => {
|
|
646
|
+
if (settled)
|
|
647
|
+
return;
|
|
648
|
+
settled = true;
|
|
649
|
+
try {
|
|
650
|
+
await storageReservation.release();
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
pendingAuditCompletions.delete(reservation);
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
pendingAuditCompletions.add(reservation);
|
|
658
|
+
return reservation;
|
|
659
|
+
}
|
|
660
|
+
function mcpAppAuditLine(manifest, method, phase) {
|
|
661
|
+
const line = `${JSON.stringify({
|
|
662
|
+
at: now().toISOString(),
|
|
663
|
+
invocationId: manifest.invocationId,
|
|
664
|
+
connectionId: manifest.connectionId,
|
|
665
|
+
method,
|
|
666
|
+
details: { phase },
|
|
667
|
+
})}\n`;
|
|
668
|
+
if (Buffer.byteLength(line, "utf8") > auditMaxBytes) {
|
|
669
|
+
throw new Error("MCP App audit entry exceeded its configured file ceiling.");
|
|
670
|
+
}
|
|
671
|
+
return line;
|
|
672
|
+
}
|
|
673
|
+
async function enqueueAudit(invocationId, operation) {
|
|
674
|
+
const previous = auditQueues.get(invocationId) ?? Promise.resolve();
|
|
675
|
+
let result;
|
|
676
|
+
const next = previous.catch(() => undefined).then(async () => {
|
|
677
|
+
result = await operation();
|
|
678
|
+
});
|
|
679
|
+
auditQueues.set(invocationId, next);
|
|
680
|
+
try {
|
|
681
|
+
await next;
|
|
682
|
+
return result;
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
throw new CodedError("app_audit_failed", "The MCP App action could not be recorded safely.");
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
if (auditQueues.get(invocationId) === next)
|
|
689
|
+
auditQueues.delete(invocationId);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async function dispose() {
|
|
693
|
+
if (idleTimer !== undefined)
|
|
694
|
+
clearTimeout(idleTimer);
|
|
695
|
+
idleTimer = undefined;
|
|
696
|
+
await Promise.allSettled([...pendingAuditCompletions].map(async (reservation) => {
|
|
697
|
+
await reservation.release();
|
|
698
|
+
}));
|
|
699
|
+
await Promise.allSettled([...connections.keys()].map(async (id) => await closeConnection(id)));
|
|
700
|
+
await Promise.allSettled([...invocationProtectionsByRun.values()].flatMap((protections) => ([...protections.values()].map(async (protection) => await protection.release()))));
|
|
701
|
+
liveByInvocation.clear();
|
|
702
|
+
partsByRun.clear();
|
|
703
|
+
partsByIdentityByRun.clear();
|
|
704
|
+
invocationIdsByRun.clear();
|
|
705
|
+
invocationProtectionsByRun.clear();
|
|
706
|
+
auditProtectionCounts.clear();
|
|
707
|
+
runOwners.clear();
|
|
708
|
+
bridgeRates.clear();
|
|
709
|
+
auditQueues.clear();
|
|
710
|
+
pendingAuditCompletions.clear();
|
|
711
|
+
auditStorage.dispose();
|
|
712
|
+
}
|
|
713
|
+
function wrapResponder(responder) {
|
|
714
|
+
const originalDispose = responder.dispose;
|
|
715
|
+
return {
|
|
716
|
+
...responder,
|
|
717
|
+
async respond(request, stream) {
|
|
718
|
+
const context = { runIds: new Set() };
|
|
719
|
+
let retainedRunId;
|
|
720
|
+
try {
|
|
721
|
+
const response = await responseContext.run(context, async () => await responder.respond(request, stream));
|
|
722
|
+
const runId = typeof response.metadata?.runId === "string" ? response.metadata.runId : undefined;
|
|
723
|
+
if (runId === undefined || (context.runIds.size > 0 && !context.runIds.has(runId)))
|
|
724
|
+
return response;
|
|
725
|
+
const parts = partsByRun.get(runId) ?? [];
|
|
726
|
+
if (parts.length === 0) {
|
|
727
|
+
await retainRun(runId);
|
|
728
|
+
retainedRunId = runId;
|
|
729
|
+
return response;
|
|
730
|
+
}
|
|
731
|
+
const finalized = await finalizeDeliveryParts(runId, parts, request.conversationId);
|
|
732
|
+
const merged = mergeReplyParts(response.parts, finalized);
|
|
733
|
+
await retainRun(runId);
|
|
734
|
+
retainedRunId = runId;
|
|
735
|
+
return { ...response, parts: merged };
|
|
736
|
+
}
|
|
737
|
+
finally {
|
|
738
|
+
await Promise.all([...context.runIds]
|
|
739
|
+
.filter((runId) => runId !== retainedRunId)
|
|
740
|
+
.map(async (runId) => await discardRun(runId)));
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
loadMcpApp: load,
|
|
744
|
+
requestMcpApp: request,
|
|
745
|
+
...(responder.openReplyArtifact === undefined
|
|
746
|
+
? {}
|
|
747
|
+
: { openReplyArtifact: responder.openReplyArtifact.bind(responder) }),
|
|
748
|
+
async dispose() {
|
|
749
|
+
await Promise.allSettled([
|
|
750
|
+
...(originalDispose === undefined ? [] : [originalDispose.call(responder)]),
|
|
751
|
+
dispose(),
|
|
752
|
+
]);
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
async function retainRun(runId) {
|
|
757
|
+
const protections = [...(invocationProtectionsByRun.get(runId)?.values() ?? [])];
|
|
758
|
+
partsByRun.delete(runId);
|
|
759
|
+
partsByIdentityByRun.delete(runId);
|
|
760
|
+
invocationIdsByRun.delete(runId);
|
|
761
|
+
invocationProtectionsByRun.delete(runId);
|
|
762
|
+
runOwners.delete(runId);
|
|
763
|
+
budget.release(runId);
|
|
764
|
+
await Promise.all(protections.map(async (protection) => await protection.release()));
|
|
765
|
+
}
|
|
766
|
+
async function finalizeDeliveryParts(runId, parts, conversationId) {
|
|
767
|
+
const finalized = [];
|
|
768
|
+
for (const part of parts) {
|
|
769
|
+
if (part.type !== "mcp_app") {
|
|
770
|
+
finalized.push(part);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const protection = await storage.protect("mcp-apps", part.invocationId);
|
|
774
|
+
try {
|
|
775
|
+
await bindDeliveryConversation(root, part, conversationId, storage);
|
|
776
|
+
finalized.push(part);
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
const failure = replaceAppWithDeliveryFailure(runId, part);
|
|
780
|
+
invocationIdsByRun.get(runId)?.delete(part.invocationId);
|
|
781
|
+
await dropInvocation(part.invocationId);
|
|
782
|
+
await auditStorage.remove(join(root, part.invocationId)).catch(() => undefined);
|
|
783
|
+
finalized.push(failure);
|
|
784
|
+
}
|
|
785
|
+
finally {
|
|
786
|
+
await protection.release();
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return finalized;
|
|
790
|
+
}
|
|
791
|
+
function replaceAppWithDeliveryFailure(runId, part) {
|
|
792
|
+
const failureIdentity = `mcp-app-delivery-failure:${part.invocationId}`;
|
|
793
|
+
const failure = {
|
|
794
|
+
type: "failure",
|
|
795
|
+
id: stablePartId("mcp-app-delivery-failure", failureIdentity),
|
|
796
|
+
code: "app_resource_invalid",
|
|
797
|
+
message: "The MCP App could not be finalized for delivery.",
|
|
798
|
+
relatedPartId: part.id,
|
|
799
|
+
};
|
|
800
|
+
const byIdentity = partsByIdentityByRun.get(runId) ?? new Map();
|
|
801
|
+
const oldIdentity = [...byIdentity].find(([, current]) => current === part)?.[0];
|
|
802
|
+
if (oldIdentity !== undefined) {
|
|
803
|
+
byIdentity.delete(oldIdentity);
|
|
804
|
+
budget.unclaim(runId, oldIdentity);
|
|
805
|
+
}
|
|
806
|
+
if (budget.claim(runId, failureIdentity) === "accepted")
|
|
807
|
+
byIdentity.set(failureIdentity, failure);
|
|
808
|
+
const current = partsByRun.get(runId) ?? [];
|
|
809
|
+
const index = current.indexOf(part);
|
|
810
|
+
if (index >= 0)
|
|
811
|
+
current[index] = failure;
|
|
812
|
+
partsByRun.set(runId, current);
|
|
813
|
+
partsByIdentityByRun.set(runId, byIdentity);
|
|
814
|
+
return failure;
|
|
815
|
+
}
|
|
816
|
+
async function discardRun(runId) {
|
|
817
|
+
const invocationIds = [...(invocationIdsByRun.get(runId) ?? [])];
|
|
818
|
+
await retainRun(runId);
|
|
819
|
+
await Promise.all(invocationIds.map(async (invocationId) => {
|
|
820
|
+
await dropInvocation(invocationId);
|
|
821
|
+
await auditStorage.remove(join(root, invocationId)).catch(() => undefined);
|
|
822
|
+
}));
|
|
823
|
+
}
|
|
824
|
+
return { createExtension, wrapResponder, load, request, cleanupExpired, dispose };
|
|
825
|
+
}
|
|
826
|
+
function appResource(value, expectedUri) {
|
|
827
|
+
const resource = record(value);
|
|
828
|
+
if (resource?.uri !== expectedUri
|
|
829
|
+
|| resource.mimeType !== MCP_APP_RESOURCE_MIME_TYPE
|
|
830
|
+
|| typeof resource.text !== "string"
|
|
831
|
+
|| Buffer.byteLength(resource.text, "utf8") > MAX_HTML_BYTES)
|
|
832
|
+
return undefined;
|
|
833
|
+
const metadata = record(resource._meta);
|
|
834
|
+
return {
|
|
835
|
+
html: resource.text,
|
|
836
|
+
...(metadata === undefined ? {} : { metadata: boundedJsonValue(metadata, 32 * 1024) }),
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
async function writeAppAtomically(root, stagingRoot, manifest, html, state, storage, beforeCommit) {
|
|
840
|
+
const directory = join(root, manifest.invocationId);
|
|
841
|
+
const stagingDirectory = join(stagingRoot, manifest.invocationId);
|
|
842
|
+
const stateJson = serializeBoundedJsonLine(state, MAX_STATE_BYTES);
|
|
843
|
+
const manifestJson = serializeBoundedJsonLine(manifest, MAX_MANIFEST_BYTES);
|
|
844
|
+
const reservation = await storage.reserve(Buffer.byteLength(html, "utf8")
|
|
845
|
+
+ Buffer.byteLength(stateJson, "utf8")
|
|
846
|
+
+ Buffer.byteLength(manifestJson, "utf8"));
|
|
847
|
+
if (reservation === undefined)
|
|
848
|
+
throw new ReplyArtifactStorageFullError();
|
|
849
|
+
try {
|
|
850
|
+
await mkdir(stagingRoot, { recursive: true, mode: 0o700 });
|
|
851
|
+
await mkdir(stagingDirectory, { mode: 0o700 });
|
|
852
|
+
let complete = false;
|
|
853
|
+
try {
|
|
854
|
+
await writeFile(join(stagingDirectory, "resource.html"), html, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
855
|
+
await writeFile(join(stagingDirectory, "state.json"), stateJson, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
856
|
+
await writeFile(join(stagingDirectory, "manifest.json"), manifestJson, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
857
|
+
await beforeCommit?.();
|
|
858
|
+
await rename(stagingDirectory, directory);
|
|
859
|
+
complete = true;
|
|
860
|
+
}
|
|
861
|
+
finally {
|
|
862
|
+
if (!complete)
|
|
863
|
+
await rm(stagingDirectory, { recursive: true, force: true }).catch(() => undefined);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
finally {
|
|
867
|
+
await reservation.release();
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async function readManifest(root, invocationId) {
|
|
871
|
+
if (!APP_ID.test(invocationId))
|
|
872
|
+
throw new CodedError("app_missing", "The MCP App reference is invalid.");
|
|
873
|
+
let value;
|
|
874
|
+
try {
|
|
875
|
+
value = await readBoundedJson(join(root, invocationId, "manifest.json"), MAX_MANIFEST_BYTES);
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
throw new CodedError("app_missing", "The MCP App is unavailable.");
|
|
879
|
+
}
|
|
880
|
+
if (!isManifest(value) || value.invocationId !== invocationId) {
|
|
881
|
+
throw new CodedError("app_missing", "The MCP App metadata is invalid.");
|
|
882
|
+
}
|
|
883
|
+
return value;
|
|
884
|
+
}
|
|
885
|
+
function isManifest(value) {
|
|
886
|
+
const manifest = record(value);
|
|
887
|
+
return manifest?.schema === 1
|
|
888
|
+
&& typeof manifest.invocationId === "string"
|
|
889
|
+
&& typeof manifest.connectionId === "string"
|
|
890
|
+
&& typeof manifest.runId === "string"
|
|
891
|
+
&& typeof manifest.conversationId === "string"
|
|
892
|
+
&& (manifest.deliveryConversationId === undefined || typeof manifest.deliveryConversationId === "string")
|
|
893
|
+
&& typeof manifest.serverName === "string"
|
|
894
|
+
&& typeof manifest.toolName === "string"
|
|
895
|
+
&& typeof manifest.toolCallId === "string"
|
|
896
|
+
&& typeof manifest.resourceUri === "string"
|
|
897
|
+
&& isSupportedProtocolVersion(manifest.protocolVersion)
|
|
898
|
+
&& manifest.mediaType === MCP_APP_RESOURCE_MIME_TYPE
|
|
899
|
+
&& Array.isArray(manifest.appVisibleTools)
|
|
900
|
+
&& manifest.appVisibleTools.every(safeToolName)
|
|
901
|
+
&& Array.isArray(manifest.appVisibleResources)
|
|
902
|
+
&& manifest.appVisibleResources.length > 0
|
|
903
|
+
&& manifest.appVisibleResources.every((uri) => typeof uri === "string" && uri.startsWith("ui://"))
|
|
904
|
+
&& typeof manifest.createdAt === "string"
|
|
905
|
+
&& Number.isFinite(Date.parse(manifest.createdAt))
|
|
906
|
+
&& typeof manifest.expiresAt === "string"
|
|
907
|
+
&& Number.isFinite(Date.parse(manifest.expiresAt));
|
|
908
|
+
}
|
|
909
|
+
function partFromManifest(manifest) {
|
|
910
|
+
return {
|
|
911
|
+
type: "mcp_app",
|
|
912
|
+
id: manifest.invocationId,
|
|
913
|
+
invocationId: manifest.invocationId,
|
|
914
|
+
connectionId: manifest.connectionId,
|
|
915
|
+
serverName: manifest.serverName,
|
|
916
|
+
toolName: manifest.toolName,
|
|
917
|
+
resourceUri: manifest.resourceUri,
|
|
918
|
+
mediaType: manifest.mediaType,
|
|
919
|
+
protocolVersion: manifest.protocolVersion,
|
|
920
|
+
...(manifest.title === undefined ? {} : { title: manifest.title }),
|
|
921
|
+
...(manifest.description === undefined ? {} : { description: manifest.description }),
|
|
922
|
+
expiresAt: manifest.expiresAt,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function authorizeApp(manifest, request) {
|
|
926
|
+
if (request.connectionId !== manifest.connectionId
|
|
927
|
+
|| (request.conversationId !== manifest.conversationId
|
|
928
|
+
&& request.conversationId !== manifest.deliveryConversationId)) {
|
|
929
|
+
throw new CodedError("app_forbidden", "The MCP App does not belong to this conversation or connection.");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async function bindDeliveryConversation(root, part, conversationId, storage) {
|
|
933
|
+
const manifest = await readManifest(root, part.invocationId);
|
|
934
|
+
if (manifest.deliveryConversationId === conversationId)
|
|
935
|
+
return;
|
|
936
|
+
const directory = join(root, manifest.invocationId);
|
|
937
|
+
const temp = join(directory, `manifest.${randomUUID()}.partial`);
|
|
938
|
+
const manifestJson = serializeBoundedJsonLine({ ...manifest, deliveryConversationId: conversationId }, MAX_MANIFEST_BYTES);
|
|
939
|
+
const reservation = await storage.reserve(Buffer.byteLength(manifestJson, "utf8"));
|
|
940
|
+
if (reservation === undefined)
|
|
941
|
+
throw new ReplyArtifactStorageFullError();
|
|
942
|
+
let committed = false;
|
|
943
|
+
try {
|
|
944
|
+
await writeFile(temp, manifestJson, {
|
|
945
|
+
encoding: "utf8",
|
|
946
|
+
mode: 0o600,
|
|
947
|
+
flag: "wx",
|
|
948
|
+
});
|
|
949
|
+
await rename(temp, join(directory, "manifest.json"));
|
|
950
|
+
committed = true;
|
|
951
|
+
}
|
|
952
|
+
finally {
|
|
953
|
+
if (!committed)
|
|
954
|
+
await rm(temp, { force: true }).catch(() => undefined);
|
|
955
|
+
await reservation.release();
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function requireConfirmation(input) {
|
|
959
|
+
if (input.confirmed !== true)
|
|
960
|
+
throw new CodedError("app_confirmation_required", "This MCP App action requires confirmation.");
|
|
961
|
+
}
|
|
962
|
+
function safeExternalUrl(input) {
|
|
963
|
+
if (Buffer.byteLength(input, "utf8") > 8_192)
|
|
964
|
+
return undefined;
|
|
965
|
+
try {
|
|
966
|
+
const url = new URL(input);
|
|
967
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : undefined;
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
return undefined;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function safeToolName(value) {
|
|
974
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,255}$/u.test(value);
|
|
975
|
+
}
|
|
976
|
+
function stableAppIdentity(registration) {
|
|
977
|
+
return `mcp_app:${stableDigest({
|
|
978
|
+
serverName: registration.serverName,
|
|
979
|
+
toolName: registration.toolName,
|
|
980
|
+
toolCallId: registration.toolCallId,
|
|
981
|
+
resourceUri: registration.resourceUri,
|
|
982
|
+
})}`;
|
|
983
|
+
}
|
|
984
|
+
function stableDigest(value) {
|
|
985
|
+
return createHash("sha256").update(JSON.stringify(value) ?? "null").digest("hex");
|
|
986
|
+
}
|
|
987
|
+
function safeIdSegment(value) {
|
|
988
|
+
return value.replace(/[^A-Za-z0-9_.-]/gu, "_").slice(0, 80) || "failure";
|
|
989
|
+
}
|
|
990
|
+
function stablePartId(prefix, identity) {
|
|
991
|
+
return `${prefix.slice(0, 120)}-${stableDigest(identity).slice(0, 24)}`;
|
|
992
|
+
}
|
|
993
|
+
async function cleanupStaging(stagingRoot, staleBeforeMs, isProtected) {
|
|
994
|
+
let entries;
|
|
995
|
+
try {
|
|
996
|
+
entries = await readdir(stagingRoot);
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
if (error.code === "ENOENT")
|
|
1000
|
+
return;
|
|
1001
|
+
throw error;
|
|
1002
|
+
}
|
|
1003
|
+
await Promise.all(entries.filter((entry) => APP_ID.test(entry) && !isProtected(entry)).map(async (entry) => {
|
|
1004
|
+
const path = join(stagingRoot, entry);
|
|
1005
|
+
try {
|
|
1006
|
+
const entryStat = await lstat(path);
|
|
1007
|
+
if (entryStat.mtimeMs > staleBeforeMs)
|
|
1008
|
+
return;
|
|
1009
|
+
await rm(path, { recursive: true, force: true });
|
|
1010
|
+
}
|
|
1011
|
+
catch (error) {
|
|
1012
|
+
if (error.code !== "ENOENT")
|
|
1013
|
+
throw error;
|
|
1014
|
+
}
|
|
1015
|
+
}));
|
|
1016
|
+
}
|
|
1017
|
+
async function appendMcpAppAudit(state, directory, line, additionalReservedBytes = 0) {
|
|
1018
|
+
assertMcpAppAuditDirectory(state, directory);
|
|
1019
|
+
const incomingBytes = Buffer.byteLength(line, "utf8");
|
|
1020
|
+
if (incomingBytes > state.fileMaxBytes) {
|
|
1021
|
+
throw new Error("MCP App audit entry exceeded its file ceiling.");
|
|
1022
|
+
}
|
|
1023
|
+
await initializeMcpAppAuditStorage(state, directory);
|
|
1024
|
+
let owner = await reconcileTargetMcpAppAuditOwner(state, directory);
|
|
1025
|
+
// Foreign poisoned owners retain their bounded reservation for this process.
|
|
1026
|
+
// Only a fresh service process may inventory their filesystem paths again.
|
|
1027
|
+
const currentBytes = owner.files.get(0)?.size ?? 0;
|
|
1028
|
+
const rotates = currentBytes > 0 && currentBytes > state.fileMaxBytes - incomingBytes;
|
|
1029
|
+
const releasedFile = rotates ? owner.files.get(state.retainedFiles) : undefined;
|
|
1030
|
+
await admitMcpAppAuditAppend(state, directory, releasedFile, incomingBytes, additionalReservedBytes);
|
|
1031
|
+
if (rotates)
|
|
1032
|
+
await rotateTrackedMcpAppAuditFiles(state, directory);
|
|
1033
|
+
owner = state.owners.get(directory) ?? emptyMcpAppAuditOwner(directory);
|
|
1034
|
+
if (owner.identity === undefined) {
|
|
1035
|
+
throw new Error("MCP App audit owner identity is unavailable.");
|
|
1036
|
+
}
|
|
1037
|
+
const active = owner.files.get(0);
|
|
1038
|
+
const expectedBytes = active?.size ?? 0;
|
|
1039
|
+
let committed;
|
|
1040
|
+
try {
|
|
1041
|
+
committed = await appendMcpAppAuditLine(state, owner, join(directory, "audit.jsonl"), line, expectedBytes, active);
|
|
1042
|
+
}
|
|
1043
|
+
catch (error) {
|
|
1044
|
+
quarantineMcpAppAuditOwner(state, directory, addMcpAppAuditBytes(mcpAppAuditOwnerBytes(owner), incomingBytes));
|
|
1045
|
+
throw error;
|
|
1046
|
+
}
|
|
1047
|
+
const files = new Map(owner.files);
|
|
1048
|
+
files.set(0, {
|
|
1049
|
+
directory,
|
|
1050
|
+
index: 0,
|
|
1051
|
+
size: committed.size,
|
|
1052
|
+
path: join(directory, "audit.jsonl"),
|
|
1053
|
+
modifiedAtMs: committed.modifiedAtMs,
|
|
1054
|
+
identity: committed.identity,
|
|
1055
|
+
reclaimable: true,
|
|
1056
|
+
});
|
|
1057
|
+
replaceMcpAppAuditOwner(state, directory, {
|
|
1058
|
+
directory,
|
|
1059
|
+
identity: owner.identity,
|
|
1060
|
+
files,
|
|
1061
|
+
reservedBytes: 0,
|
|
1062
|
+
poisoned: false,
|
|
1063
|
+
});
|
|
1064
|
+
if (projectedMcpAppAuditBytes(state, undefined, 0, additionalReservedBytes) > state.maxBytes) {
|
|
1065
|
+
throw new Error("MCP App audit storage exceeded its aggregate ceiling.");
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async function admitMcpAppAuditAppend(state, targetDirectory, naturallyReleased, incomingBytes, additionalReservedBytes) {
|
|
1069
|
+
if (projectedMcpAppAuditBytes(state, naturallyReleased, incomingBytes, additionalReservedBytes) <= state.maxBytes)
|
|
1070
|
+
return;
|
|
1071
|
+
const attempted = new Set();
|
|
1072
|
+
while (projectedMcpAppAuditBytes(state, naturallyReleased, incomingBytes, additionalReservedBytes) > state.maxBytes) {
|
|
1073
|
+
const reclaimablePaths = mcpAppAuditReclaimablePaths(state, targetDirectory, naturallyReleased, attempted);
|
|
1074
|
+
if (projectedMcpAppAuditBytes(state, naturallyReleased, incomingBytes, additionalReservedBytes, reclaimablePaths) > state.maxBytes) {
|
|
1075
|
+
throw new Error("MCP App audit storage is full.");
|
|
1076
|
+
}
|
|
1077
|
+
const candidate = nextMcpAppAuditReclamationCandidate(state, targetDirectory, naturallyReleased, incomingBytes, attempted);
|
|
1078
|
+
if (candidate === undefined)
|
|
1079
|
+
break;
|
|
1080
|
+
attempted.add(candidate.path);
|
|
1081
|
+
try {
|
|
1082
|
+
const removed = await removeMcpAppAuditPathUnlessProtected(state, candidate, { force: true });
|
|
1083
|
+
if (!removed)
|
|
1084
|
+
continue;
|
|
1085
|
+
removeTrackedMcpAppAuditFile(state, candidate);
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
markMcpAppAuditFileUnreclaimable(state, candidate);
|
|
1089
|
+
if (candidate.directory === targetDirectory) {
|
|
1090
|
+
throw new Error("MCP App audit history could not be reclaimed safely.");
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (projectedMcpAppAuditBytes(state, naturallyReleased, incomingBytes, additionalReservedBytes) > state.maxBytes) {
|
|
1095
|
+
throw new Error("MCP App audit storage is full.");
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async function initializeMcpAppAuditStorage(state, targetDirectory) {
|
|
1099
|
+
if (state.initialized)
|
|
1100
|
+
return;
|
|
1101
|
+
let root;
|
|
1102
|
+
let entries;
|
|
1103
|
+
try {
|
|
1104
|
+
root = await openMcpAppAuditRoot(state);
|
|
1105
|
+
entries = await readMcpAppAuditDirectory(state, state.root, async () => await assertOpenedMcpAppAuditDirectory(state, root));
|
|
1106
|
+
}
|
|
1107
|
+
catch (error) {
|
|
1108
|
+
if (error.code === "ENOENT") {
|
|
1109
|
+
state.initialized = true;
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
throw error;
|
|
1113
|
+
}
|
|
1114
|
+
finally {
|
|
1115
|
+
await root?.handle.close().catch(() => undefined);
|
|
1116
|
+
}
|
|
1117
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
1118
|
+
if (!entry.isDirectory() || !APP_ID.test(entry.name))
|
|
1119
|
+
continue;
|
|
1120
|
+
const directory = join(state.root, entry.name);
|
|
1121
|
+
const owner = await inventoryMcpAppAuditOwner(state, directory, directory === targetDirectory);
|
|
1122
|
+
if (directory === targetDirectory || mcpAppAuditOwnerBytes(owner) > 0) {
|
|
1123
|
+
replaceMcpAppAuditOwner(state, directory, owner);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
state.initialized = true;
|
|
1127
|
+
}
|
|
1128
|
+
async function reconcileTargetMcpAppAuditOwner(state, directory) {
|
|
1129
|
+
const tracked = state.owners.get(directory);
|
|
1130
|
+
if (tracked?.poisoned === true) {
|
|
1131
|
+
throw new Error("MCP App audit target is quarantined for this process.");
|
|
1132
|
+
}
|
|
1133
|
+
const owner = await inventoryMcpAppAuditOwner(state, directory, true, tracked?.identity);
|
|
1134
|
+
replaceMcpAppAuditOwner(state, directory, owner);
|
|
1135
|
+
return owner;
|
|
1136
|
+
}
|
|
1137
|
+
async function inventoryMcpAppAuditOwner(state, directory, strict, expectedIdentity) {
|
|
1138
|
+
let observedIdentity = expectedIdentity;
|
|
1139
|
+
let opened;
|
|
1140
|
+
let entries;
|
|
1141
|
+
try {
|
|
1142
|
+
const initial = await lstatMcpAppAuditPath(state, directory);
|
|
1143
|
+
if (!initial.isDirectory() || initial.isSymbolicLink()) {
|
|
1144
|
+
throw new Error("MCP App audit target is not a safe invocation directory.");
|
|
1145
|
+
}
|
|
1146
|
+
const initialIdentity = mcpAppAuditIdentity(initial);
|
|
1147
|
+
if (expectedIdentity !== undefined && !sameMcpAppAuditIdentity(initialIdentity, expectedIdentity)) {
|
|
1148
|
+
throw new Error("MCP App audit invocation directory identity changed.");
|
|
1149
|
+
}
|
|
1150
|
+
observedIdentity = initialIdentity;
|
|
1151
|
+
opened = await openMcpAppAuditOwner(state, directory, expectedIdentity ?? initialIdentity);
|
|
1152
|
+
entries = await readMcpAppAuditDirectory(state, directory, async () => await assertOpenedMcpAppAuditOwner(state, opened));
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
if (opened !== undefined)
|
|
1156
|
+
await closeOpenedMcpAppAuditOwner(opened);
|
|
1157
|
+
if (error.code === "ENOENT" && expectedIdentity === undefined) {
|
|
1158
|
+
return emptyMcpAppAuditOwner(directory);
|
|
1159
|
+
}
|
|
1160
|
+
if (strict)
|
|
1161
|
+
throw error;
|
|
1162
|
+
return poisonedMcpAppAuditOwner(state, directory, 0, observedIdentity);
|
|
1163
|
+
}
|
|
1164
|
+
const identity = opened.owner.identity;
|
|
1165
|
+
const files = new Map();
|
|
1166
|
+
try {
|
|
1167
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
1168
|
+
const index = mcpAppAuditFileIndex(entry.name);
|
|
1169
|
+
if (index === undefined)
|
|
1170
|
+
continue;
|
|
1171
|
+
const path = join(directory, entry.name);
|
|
1172
|
+
let fileStat;
|
|
1173
|
+
try {
|
|
1174
|
+
fileStat = await lstatMcpAppAuditPath(state, path, async () => await assertOpenedMcpAppAuditOwner(state, opened));
|
|
1175
|
+
}
|
|
1176
|
+
catch (error) {
|
|
1177
|
+
if (error.code === "ENOENT")
|
|
1178
|
+
continue;
|
|
1179
|
+
if (strict)
|
|
1180
|
+
throw error;
|
|
1181
|
+
return poisonedMcpAppAuditOwner(state, directory, mcpAppAuditFilesBytes(files), identity);
|
|
1182
|
+
}
|
|
1183
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink() || fileStat.nlink !== 1) {
|
|
1184
|
+
if (strict)
|
|
1185
|
+
throw new Error("MCP App audit storage contains an unsafe file.");
|
|
1186
|
+
return poisonedMcpAppAuditOwner(state, directory, addMcpAppAuditBytes(mcpAppAuditFilesBytes(files), fileStat.isFile() && Number.isSafeInteger(fileStat.size) ? fileStat.size : 0), identity);
|
|
1187
|
+
}
|
|
1188
|
+
const file = {
|
|
1189
|
+
directory,
|
|
1190
|
+
index,
|
|
1191
|
+
size: fileStat.size,
|
|
1192
|
+
path,
|
|
1193
|
+
modifiedAtMs: fileStat.mtimeMs,
|
|
1194
|
+
identity: mcpAppAuditIdentity(fileStat),
|
|
1195
|
+
reclaimable: true,
|
|
1196
|
+
};
|
|
1197
|
+
if (index === 0 && fileStat.size > state.fileMaxBytes) {
|
|
1198
|
+
if (strict) {
|
|
1199
|
+
throw new Error("MCP App audit file exceeded its configured ceiling.");
|
|
1200
|
+
}
|
|
1201
|
+
files.set(index, file);
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (index > state.retainedFiles || fileStat.size > state.fileMaxBytes) {
|
|
1205
|
+
try {
|
|
1206
|
+
await removeMcpAppAuditPath(state, file, { force: true }, opened);
|
|
1207
|
+
}
|
|
1208
|
+
catch {
|
|
1209
|
+
if (strict)
|
|
1210
|
+
throw new Error("MCP App audit history could not be bounded safely.");
|
|
1211
|
+
return poisonedMcpAppAuditOwner(state, directory, addMcpAppAuditBytes(mcpAppAuditFilesBytes(files), fileStat.size), identity);
|
|
1212
|
+
}
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
files.set(index, {
|
|
1216
|
+
...file,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
await assertOpenedMcpAppAuditOwner(state, opened);
|
|
1220
|
+
return { directory, identity, files, reservedBytes: 0, poisoned: false };
|
|
1221
|
+
}
|
|
1222
|
+
finally {
|
|
1223
|
+
await closeOpenedMcpAppAuditOwner(opened);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function nextMcpAppAuditReclamationCandidate(state, targetDirectory, naturallyReleased, incomingBytes, attempted) {
|
|
1227
|
+
const fairShare = Math.floor(state.maxBytes / Math.max(1, state.owners.size));
|
|
1228
|
+
const eligible = [...state.owners.values()].flatMap((owner) => [...owner.files.values()])
|
|
1229
|
+
.filter((candidate) => isMcpAppAuditReclamationCandidate(state, candidate, targetDirectory, naturallyReleased, attempted));
|
|
1230
|
+
const tier = eligible.some((candidate) => candidate.index > 0) ? "rotated" : "inactive-active";
|
|
1231
|
+
const candidates = [...state.owners.entries()].flatMap(([directory, owner]) => {
|
|
1232
|
+
const file = [...owner.files.values()]
|
|
1233
|
+
.filter((candidate) => (isMcpAppAuditReclamationCandidate(state, candidate, targetDirectory, naturallyReleased, attempted)
|
|
1234
|
+
&& (tier === "rotated" ? candidate.index > 0 : candidate.index === 0)))
|
|
1235
|
+
.sort(compareMcpAppAuditAge)[0];
|
|
1236
|
+
if (file === undefined)
|
|
1237
|
+
return [];
|
|
1238
|
+
const bytes = addMcpAppAuditBytes(Math.max(0, mcpAppAuditOwnerBytes(owner) - (directory === targetDirectory ? naturallyReleased?.size ?? 0 : 0)), directory === targetDirectory ? incomingBytes : 0);
|
|
1239
|
+
return [{ directory, bytes, file }];
|
|
1240
|
+
});
|
|
1241
|
+
if (candidates.length === 0)
|
|
1242
|
+
return undefined;
|
|
1243
|
+
const aboveShare = candidates.filter((candidate) => candidate.bytes > fairShare);
|
|
1244
|
+
const pool = aboveShare.length > 0 ? aboveShare : candidates;
|
|
1245
|
+
pool.sort((left, right) => (right.bytes - left.bytes
|
|
1246
|
+
|| compareMcpAppAuditAge(left.file, right.file)
|
|
1247
|
+
|| left.directory.localeCompare(right.directory)));
|
|
1248
|
+
return pool[0].file;
|
|
1249
|
+
}
|
|
1250
|
+
function mcpAppAuditReclaimablePaths(state, targetDirectory, naturallyReleased, attempted) {
|
|
1251
|
+
return new Set([...state.owners.values()].flatMap((owner) => [...owner.files.values()])
|
|
1252
|
+
.filter((candidate) => isMcpAppAuditReclamationCandidate(state, candidate, targetDirectory, naturallyReleased, attempted))
|
|
1253
|
+
.map((candidate) => candidate.path));
|
|
1254
|
+
}
|
|
1255
|
+
function isMcpAppAuditReclamationCandidate(state, candidate, targetDirectory, naturallyReleased, attempted) {
|
|
1256
|
+
if (!candidate.reclaimable
|
|
1257
|
+
|| candidate.path === naturallyReleased?.path
|
|
1258
|
+
|| attempted.has(candidate.path))
|
|
1259
|
+
return false;
|
|
1260
|
+
if (candidate.index > 0)
|
|
1261
|
+
return true;
|
|
1262
|
+
return candidate.directory !== targetDirectory
|
|
1263
|
+
&& !isMcpAppAuditOwnerProtected(state, candidate.directory);
|
|
1264
|
+
}
|
|
1265
|
+
function isMcpAppAuditOwnerProtected(state, directory) {
|
|
1266
|
+
for (const reservation of state.reservations.values()) {
|
|
1267
|
+
if (reservation.directory === directory)
|
|
1268
|
+
return true;
|
|
1269
|
+
}
|
|
1270
|
+
for (const predicate of state.protectionPredicates) {
|
|
1271
|
+
try {
|
|
1272
|
+
if (predicate(directory))
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
catch {
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
function projectedMcpAppAuditBytes(state, naturallyReleased, incomingBytes, additionalReservedBytes = 0, excludedPaths = new Set()) {
|
|
1282
|
+
const ceiling = state.maxBytes + 1;
|
|
1283
|
+
let total = 0;
|
|
1284
|
+
const add = (bytes) => {
|
|
1285
|
+
total = Math.min(ceiling, addMcpAppAuditBytes(total, bytes));
|
|
1286
|
+
};
|
|
1287
|
+
for (const owner of state.owners.values()) {
|
|
1288
|
+
if (owner.poisoned)
|
|
1289
|
+
add(Math.min(state.maxBytes, owner.reservedBytes));
|
|
1290
|
+
for (const file of owner.files.values()) {
|
|
1291
|
+
if (file.path === naturallyReleased?.path || excludedPaths.has(file.path))
|
|
1292
|
+
continue;
|
|
1293
|
+
add(file.size);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
for (const reservation of state.reservations.values())
|
|
1297
|
+
add(reservation.bytes);
|
|
1298
|
+
add(incomingBytes);
|
|
1299
|
+
add(additionalReservedBytes);
|
|
1300
|
+
return total;
|
|
1301
|
+
}
|
|
1302
|
+
function emptyMcpAppAuditOwner(directory) {
|
|
1303
|
+
return { directory, files: new Map(), reservedBytes: 0, poisoned: false };
|
|
1304
|
+
}
|
|
1305
|
+
function poisonedMcpAppAuditOwner(state, directory, minimumBytes = 0, identity) {
|
|
1306
|
+
const ownerCapacity = state.fileMaxBytes * (state.retainedFiles + 1);
|
|
1307
|
+
return {
|
|
1308
|
+
directory,
|
|
1309
|
+
...(identity === undefined ? {} : { identity }),
|
|
1310
|
+
files: new Map(),
|
|
1311
|
+
// Unknown owners consume their full safe owner capacity, while the
|
|
1312
|
+
// aggregate projection saturates just above maxBytes. More poisoned owners
|
|
1313
|
+
// can therefore never overflow accounting or create admission headroom.
|
|
1314
|
+
reservedBytes: Math.min(state.maxBytes, Math.max(ownerCapacity, minimumBytes)),
|
|
1315
|
+
poisoned: true,
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
function quarantineMcpAppAuditOwner(state, directory, minimumBytes) {
|
|
1319
|
+
replaceMcpAppAuditOwner(state, directory, poisonedMcpAppAuditOwner(state, directory, minimumBytes, state.owners.get(directory)?.identity));
|
|
1320
|
+
}
|
|
1321
|
+
function mcpAppAuditFilesBytes(files) {
|
|
1322
|
+
let total = 0;
|
|
1323
|
+
for (const file of files.values())
|
|
1324
|
+
total = addMcpAppAuditBytes(total, file.size);
|
|
1325
|
+
return total;
|
|
1326
|
+
}
|
|
1327
|
+
function mcpAppAuditOwnerBytes(owner) {
|
|
1328
|
+
return addMcpAppAuditBytes(owner.reservedBytes, mcpAppAuditFilesBytes(owner.files));
|
|
1329
|
+
}
|
|
1330
|
+
function replaceMcpAppAuditOwner(state, directory, owner) {
|
|
1331
|
+
if (owner === undefined) {
|
|
1332
|
+
state.owners.delete(directory);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
state.owners.set(directory, owner);
|
|
1336
|
+
}
|
|
1337
|
+
function removeTrackedMcpAppAuditFile(state, file) {
|
|
1338
|
+
const owner = state.owners.get(file.directory);
|
|
1339
|
+
const tracked = owner?.files.get(file.index);
|
|
1340
|
+
if (owner === undefined || tracked?.path !== file.path)
|
|
1341
|
+
return;
|
|
1342
|
+
const files = new Map(owner.files);
|
|
1343
|
+
files.delete(file.index);
|
|
1344
|
+
replaceMcpAppAuditOwner(state, file.directory, {
|
|
1345
|
+
...owner,
|
|
1346
|
+
files,
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
function markMcpAppAuditFileUnreclaimable(state, file) {
|
|
1350
|
+
const owner = state.owners.get(file.directory);
|
|
1351
|
+
const tracked = owner?.files.get(file.index);
|
|
1352
|
+
if (owner === undefined || tracked?.path !== file.path)
|
|
1353
|
+
return;
|
|
1354
|
+
const files = new Map(owner.files);
|
|
1355
|
+
files.set(file.index, { ...tracked, reclaimable: false });
|
|
1356
|
+
replaceMcpAppAuditOwner(state, file.directory, { ...owner, files });
|
|
1357
|
+
}
|
|
1358
|
+
async function rotateTrackedMcpAppAuditFiles(state, directory) {
|
|
1359
|
+
let owner = state.owners.get(directory) ?? emptyMcpAppAuditOwner(directory);
|
|
1360
|
+
const released = owner.files.get(state.retainedFiles);
|
|
1361
|
+
if (released !== undefined) {
|
|
1362
|
+
await removeMcpAppAuditPath(state, released, { force: true });
|
|
1363
|
+
removeTrackedMcpAppAuditFile(state, released);
|
|
1364
|
+
}
|
|
1365
|
+
for (let index = state.retainedFiles; index >= 1; index -= 1) {
|
|
1366
|
+
owner = state.owners.get(directory) ?? emptyMcpAppAuditOwner(directory);
|
|
1367
|
+
const source = owner.files.get(index - 1);
|
|
1368
|
+
if (source === undefined)
|
|
1369
|
+
continue;
|
|
1370
|
+
const destinationPath = join(directory, `audit.${index}.jsonl`);
|
|
1371
|
+
await renameMcpAppAuditPath(state, source, destinationPath);
|
|
1372
|
+
const files = new Map(owner.files);
|
|
1373
|
+
files.delete(index - 1);
|
|
1374
|
+
files.set(index, {
|
|
1375
|
+
...source,
|
|
1376
|
+
index,
|
|
1377
|
+
path: destinationPath,
|
|
1378
|
+
});
|
|
1379
|
+
replaceMcpAppAuditOwner(state, directory, { ...owner, files });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
async function appendMcpAppAuditLine(state, owner, path, line, expectedBytes, expectedFile) {
|
|
1383
|
+
if (owner.identity === undefined)
|
|
1384
|
+
throw new Error("MCP App audit owner identity is unavailable.");
|
|
1385
|
+
await beforeMcpAppAuditOperation(state, "append", path);
|
|
1386
|
+
const opened = await openMcpAppAuditOwner(state, owner.directory, owner.identity);
|
|
1387
|
+
const childPath = mcpAppAuditChildPath(opened.owner, basename(path));
|
|
1388
|
+
const flags = fsConstants.O_APPEND
|
|
1389
|
+
| fsConstants.O_WRONLY
|
|
1390
|
+
| (expectedFile === undefined ? fsConstants.O_CREAT | fsConstants.O_EXCL : 0)
|
|
1391
|
+
| (process.platform === "darwin" ? DARWIN_O_NOFOLLOW_ANY : (fsConstants.O_NOFOLLOW ?? 0));
|
|
1392
|
+
let descriptor;
|
|
1393
|
+
try {
|
|
1394
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
1395
|
+
descriptor = openSync(childPath, flags, 0o600);
|
|
1396
|
+
const before = fstatSync(descriptor);
|
|
1397
|
+
const current = lstatSync(childPath);
|
|
1398
|
+
if (!before.isFile()
|
|
1399
|
+
|| before.nlink !== 1
|
|
1400
|
+
|| before.size !== expectedBytes
|
|
1401
|
+
|| !sameMcpAppAuditFileIdentity(before, current)
|
|
1402
|
+
|| (expectedFile !== undefined && !sameMcpAppAuditIdentity(mcpAppAuditIdentity(before), expectedFile.identity))) {
|
|
1403
|
+
throw new Error("MCP App audit storage changed during append admission.");
|
|
1404
|
+
}
|
|
1405
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
1406
|
+
const lineBuffer = Buffer.from(line, "utf8");
|
|
1407
|
+
let written = 0;
|
|
1408
|
+
while (written < lineBuffer.length) {
|
|
1409
|
+
const bytesWritten = writeSync(descriptor, lineBuffer, written, lineBuffer.length - written, null);
|
|
1410
|
+
if (bytesWritten === 0)
|
|
1411
|
+
throw new Error("MCP App audit append made no forward progress.");
|
|
1412
|
+
written += bytesWritten;
|
|
1413
|
+
}
|
|
1414
|
+
const after = fstatSync(descriptor);
|
|
1415
|
+
const currentAfter = lstatSync(childPath);
|
|
1416
|
+
const lineBytes = lineBuffer.length;
|
|
1417
|
+
if (written !== lineBytes
|
|
1418
|
+
|| !after.isFile()
|
|
1419
|
+
|| after.nlink !== 1
|
|
1420
|
+
|| after.size !== expectedBytes + lineBytes
|
|
1421
|
+
|| after.size > state.fileMaxBytes
|
|
1422
|
+
|| !sameMcpAppAuditFileIdentity(after, currentAfter)) {
|
|
1423
|
+
throw new Error("MCP App audit append did not preserve its physical file ceiling.");
|
|
1424
|
+
}
|
|
1425
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
1426
|
+
return {
|
|
1427
|
+
size: after.size,
|
|
1428
|
+
modifiedAtMs: after.mtimeMs,
|
|
1429
|
+
identity: mcpAppAuditIdentity(after),
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
finally {
|
|
1433
|
+
try {
|
|
1434
|
+
if (descriptor !== undefined)
|
|
1435
|
+
closeSync(descriptor);
|
|
1436
|
+
}
|
|
1437
|
+
finally {
|
|
1438
|
+
await closeOpenedMcpAppAuditOwner(opened);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
function assertMcpAppAuditDirectory(state, directory) {
|
|
1443
|
+
if (resolve(directory, "..") !== state.root || !APP_ID.test(basename(directory))) {
|
|
1444
|
+
throw new Error("MCP App audit target escaped its owned invocation directory.");
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
async function beforeMcpAppAuditOperation(state, operation, path) {
|
|
1448
|
+
await state.beforeOperation?.(operation, path);
|
|
1449
|
+
}
|
|
1450
|
+
async function readMcpAppAuditDirectory(state, path, assertParent) {
|
|
1451
|
+
await beforeMcpAppAuditOperation(state, "readdir", path);
|
|
1452
|
+
await assertParent?.();
|
|
1453
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
1454
|
+
await assertParent?.();
|
|
1455
|
+
return entries;
|
|
1456
|
+
}
|
|
1457
|
+
async function lstatMcpAppAuditPath(state, path, assertParent) {
|
|
1458
|
+
await beforeMcpAppAuditOperation(state, "lstat", path);
|
|
1459
|
+
await assertParent?.();
|
|
1460
|
+
const details = await lstat(path);
|
|
1461
|
+
await assertParent?.();
|
|
1462
|
+
return details;
|
|
1463
|
+
}
|
|
1464
|
+
async function removeMcpAppAuditPath(state, file, options, existingOwner) {
|
|
1465
|
+
await removeMcpAppAuditFile(state, file, options, false, existingOwner);
|
|
1466
|
+
}
|
|
1467
|
+
async function removeMcpAppAuditPathUnlessProtected(state, file, options) {
|
|
1468
|
+
return await removeMcpAppAuditFile(state, file, options, true);
|
|
1469
|
+
}
|
|
1470
|
+
async function renameMcpAppAuditPath(state, source, destination) {
|
|
1471
|
+
const owner = state.owners.get(source.directory);
|
|
1472
|
+
if (owner?.identity === undefined)
|
|
1473
|
+
throw new Error("MCP App audit owner identity is unavailable.");
|
|
1474
|
+
await beforeMcpAppAuditOperation(state, "rename", source.path);
|
|
1475
|
+
const opened = await openMcpAppAuditOwner(state, source.directory, owner.identity);
|
|
1476
|
+
try {
|
|
1477
|
+
const sourcePath = mcpAppAuditChildPath(opened.owner, basename(source.path));
|
|
1478
|
+
const destinationPath = mcpAppAuditChildPath(opened.owner, basename(destination));
|
|
1479
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
1480
|
+
const currentSource = lstatSync(sourcePath);
|
|
1481
|
+
assertMcpAppAuditFileSnapshot(currentSource, source);
|
|
1482
|
+
try {
|
|
1483
|
+
lstatSync(destinationPath);
|
|
1484
|
+
throw new Error("MCP App audit rotation destination changed unexpectedly.");
|
|
1485
|
+
}
|
|
1486
|
+
catch (error) {
|
|
1487
|
+
if (error.code !== "ENOENT")
|
|
1488
|
+
throw error;
|
|
1489
|
+
}
|
|
1490
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
1491
|
+
renameSync(sourcePath, destinationPath);
|
|
1492
|
+
const currentDestination = lstatSync(destinationPath);
|
|
1493
|
+
assertMcpAppAuditFileSnapshot(currentDestination, source);
|
|
1494
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
1495
|
+
}
|
|
1496
|
+
finally {
|
|
1497
|
+
await closeOpenedMcpAppAuditOwner(opened);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
async function removeMcpAppAuditFile(state, file, options, respectProtection, existingOwner) {
|
|
1501
|
+
if (options.recursive === true) {
|
|
1502
|
+
throw new Error("MCP App audit reclamation never recursively removes an owner entry.");
|
|
1503
|
+
}
|
|
1504
|
+
const trackedOwner = state.owners.get(file.directory);
|
|
1505
|
+
const identity = existingOwner?.owner.identity ?? trackedOwner?.identity;
|
|
1506
|
+
if (identity === undefined)
|
|
1507
|
+
throw new Error("MCP App audit owner identity is unavailable.");
|
|
1508
|
+
await beforeMcpAppAuditOperation(state, "remove", file.path);
|
|
1509
|
+
const opened = existingOwner ?? await openMcpAppAuditOwner(state, file.directory, identity);
|
|
1510
|
+
try {
|
|
1511
|
+
const childPath = mcpAppAuditChildPath(opened.owner, basename(file.path));
|
|
1512
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
1513
|
+
const current = lstatSync(childPath);
|
|
1514
|
+
assertMcpAppAuditFileSnapshot(current, file);
|
|
1515
|
+
if (respectProtection && file.index === 0 && isMcpAppAuditOwnerProtected(state, file.directory)) {
|
|
1516
|
+
return false;
|
|
1517
|
+
}
|
|
1518
|
+
// Keep the final validation and unlink in one synchronous critical section:
|
|
1519
|
+
// no in-process task can swap the parent between check and use. Linux child
|
|
1520
|
+
// paths are descriptor-relative; Darwin append opens also use NOFOLLOW_ANY.
|
|
1521
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
1522
|
+
rmSync(childPath, options);
|
|
1523
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
1524
|
+
return true;
|
|
1525
|
+
}
|
|
1526
|
+
finally {
|
|
1527
|
+
if (existingOwner === undefined)
|
|
1528
|
+
await closeOpenedMcpAppAuditOwner(opened);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
async function openMcpAppAuditRoot(state) {
|
|
1532
|
+
const opened = await openVerifiedMcpAppAuditDirectory(state.root, state.rootIdentity, state.rootRealPath);
|
|
1533
|
+
if (state.rootIdentity === undefined) {
|
|
1534
|
+
state.rootIdentity = opened.identity;
|
|
1535
|
+
state.rootRealPath = opened.realPath;
|
|
1536
|
+
}
|
|
1537
|
+
return opened;
|
|
1538
|
+
}
|
|
1539
|
+
async function openMcpAppAuditOwner(state, directory, expectedIdentity) {
|
|
1540
|
+
assertMcpAppAuditDirectory(state, directory);
|
|
1541
|
+
const root = await openMcpAppAuditRoot(state);
|
|
1542
|
+
try {
|
|
1543
|
+
const owner = await openVerifiedMcpAppAuditDirectory(directory, expectedIdentity, join(root.realPath, basename(directory)));
|
|
1544
|
+
const opened = { root, owner };
|
|
1545
|
+
await assertOpenedMcpAppAuditOwner(state, opened);
|
|
1546
|
+
return opened;
|
|
1547
|
+
}
|
|
1548
|
+
catch (error) {
|
|
1549
|
+
await root.handle.close().catch(() => undefined);
|
|
1550
|
+
throw error;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async function openVerifiedMcpAppAuditDirectory(path, expectedIdentity, expectedRealPath) {
|
|
1554
|
+
const initial = await lstat(path);
|
|
1555
|
+
if (!initial.isDirectory() || initial.isSymbolicLink()) {
|
|
1556
|
+
throw new Error("MCP App audit storage traversed an unsafe directory entry.");
|
|
1557
|
+
}
|
|
1558
|
+
const handle = await open(path, fsConstants.O_RDONLY
|
|
1559
|
+
| (fsConstants.O_DIRECTORY ?? 0)
|
|
1560
|
+
| (fsConstants.O_NOFOLLOW ?? 0)
|
|
1561
|
+
| (fsConstants.O_NONBLOCK ?? 0));
|
|
1562
|
+
try {
|
|
1563
|
+
const [opened, current, currentRealPath] = await Promise.all([
|
|
1564
|
+
handle.stat(),
|
|
1565
|
+
lstat(path),
|
|
1566
|
+
realpath(path),
|
|
1567
|
+
]);
|
|
1568
|
+
const identity = mcpAppAuditIdentity(opened);
|
|
1569
|
+
if (!opened.isDirectory()
|
|
1570
|
+
|| !current.isDirectory()
|
|
1571
|
+
|| current.isSymbolicLink()
|
|
1572
|
+
|| !sameMcpAppAuditIdentity(mcpAppAuditIdentity(initial), identity)
|
|
1573
|
+
|| !sameMcpAppAuditIdentity(mcpAppAuditIdentity(current), identity)
|
|
1574
|
+
|| (expectedIdentity !== undefined && !sameMcpAppAuditIdentity(identity, expectedIdentity))
|
|
1575
|
+
|| (expectedRealPath !== undefined && currentRealPath !== expectedRealPath)) {
|
|
1576
|
+
throw new Error("MCP App audit directory identity changed during verification.");
|
|
1577
|
+
}
|
|
1578
|
+
return { path, identity, realPath: currentRealPath, handle };
|
|
1579
|
+
}
|
|
1580
|
+
catch (error) {
|
|
1581
|
+
await handle.close().catch(() => undefined);
|
|
1582
|
+
throw error;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
async function assertOpenedMcpAppAuditDirectory(state, opened) {
|
|
1586
|
+
assertOpenedMcpAppAuditDirectorySync(state, opened);
|
|
1587
|
+
}
|
|
1588
|
+
async function assertOpenedMcpAppAuditOwner(state, opened) {
|
|
1589
|
+
assertOpenedMcpAppAuditOwnerSync(state, opened);
|
|
1590
|
+
}
|
|
1591
|
+
function assertOpenedMcpAppAuditDirectorySync(state, opened) {
|
|
1592
|
+
const retained = fstatSync(opened.handle.fd);
|
|
1593
|
+
const current = lstatSync(opened.path);
|
|
1594
|
+
const currentRealPath = realpathSync(opened.path);
|
|
1595
|
+
const rootIdentityChanged = opened.path === state.root
|
|
1596
|
+
&& state.rootIdentity !== undefined
|
|
1597
|
+
&& !sameMcpAppAuditIdentity(opened.identity, state.rootIdentity);
|
|
1598
|
+
const rootRealPathChanged = opened.path === state.root
|
|
1599
|
+
&& state.rootRealPath !== undefined
|
|
1600
|
+
&& opened.realPath !== state.rootRealPath;
|
|
1601
|
+
if (!retained.isDirectory()
|
|
1602
|
+
|| !current.isDirectory()
|
|
1603
|
+
|| current.isSymbolicLink()
|
|
1604
|
+
|| !sameMcpAppAuditIdentity(mcpAppAuditIdentity(retained), opened.identity)
|
|
1605
|
+
|| !sameMcpAppAuditIdentity(mcpAppAuditIdentity(current), opened.identity)
|
|
1606
|
+
|| currentRealPath !== opened.realPath
|
|
1607
|
+
|| rootIdentityChanged
|
|
1608
|
+
|| rootRealPathChanged) {
|
|
1609
|
+
throw new Error("MCP App audit directory changed while in use.");
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
function assertOpenedMcpAppAuditOwnerSync(state, opened) {
|
|
1613
|
+
assertOpenedMcpAppAuditDirectorySync(state, opened.root);
|
|
1614
|
+
assertOpenedMcpAppAuditDirectorySync(state, opened.owner);
|
|
1615
|
+
if (dirname(opened.owner.realPath) !== opened.root.realPath
|
|
1616
|
+
|| basename(opened.owner.path) !== basename(opened.owner.realPath)) {
|
|
1617
|
+
throw new Error("MCP App audit owner escaped its verified audit root.");
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
function mcpAppAuditChildPath(opened, name) {
|
|
1621
|
+
if (basename(name) !== name || name === "." || name === "..") {
|
|
1622
|
+
throw new Error("MCP App audit child name is unsafe.");
|
|
1623
|
+
}
|
|
1624
|
+
return process.platform === "linux"
|
|
1625
|
+
? `/proc/self/fd/${String(opened.handle.fd)}/${name}`
|
|
1626
|
+
: join(opened.realPath, name);
|
|
1627
|
+
}
|
|
1628
|
+
async function closeOpenedMcpAppAuditOwner(opened) {
|
|
1629
|
+
await Promise.all([
|
|
1630
|
+
opened.owner.handle.close().catch(() => undefined),
|
|
1631
|
+
opened.root.handle.close().catch(() => undefined),
|
|
1632
|
+
]);
|
|
1633
|
+
}
|
|
1634
|
+
function mcpAppAuditIdentity(details) {
|
|
1635
|
+
return { dev: details.dev, ino: details.ino };
|
|
1636
|
+
}
|
|
1637
|
+
function sameMcpAppAuditIdentity(left, right) {
|
|
1638
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
1639
|
+
}
|
|
1640
|
+
function sameMcpAppAuditFileIdentity(opened, current) {
|
|
1641
|
+
return opened.isFile()
|
|
1642
|
+
&& opened.nlink === 1
|
|
1643
|
+
&& current.isFile()
|
|
1644
|
+
&& !current.isSymbolicLink()
|
|
1645
|
+
&& current.nlink === 1
|
|
1646
|
+
&& sameMcpAppAuditIdentity(mcpAppAuditIdentity(opened), mcpAppAuditIdentity(current));
|
|
1647
|
+
}
|
|
1648
|
+
function assertMcpAppAuditFileSnapshot(current, expected) {
|
|
1649
|
+
if (!current.isFile()
|
|
1650
|
+
|| current.isSymbolicLink()
|
|
1651
|
+
|| current.nlink !== 1
|
|
1652
|
+
|| current.size !== expected.size
|
|
1653
|
+
|| !sameMcpAppAuditIdentity(mcpAppAuditIdentity(current), expected.identity)) {
|
|
1654
|
+
throw new Error("MCP App audit file identity changed before mutation.");
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function compareMcpAppAuditAge(left, right) {
|
|
1658
|
+
return left.modifiedAtMs - right.modifiedAtMs
|
|
1659
|
+
|| (left.directory === right.directory ? right.index - left.index : left.path.localeCompare(right.path));
|
|
1660
|
+
}
|
|
1661
|
+
function mcpAppAuditFileIndex(name) {
|
|
1662
|
+
if (name === "audit.jsonl")
|
|
1663
|
+
return 0;
|
|
1664
|
+
const matched = /^audit\.([1-9][0-9]*)\.jsonl$/u.exec(name);
|
|
1665
|
+
if (matched === null)
|
|
1666
|
+
return undefined;
|
|
1667
|
+
const index = Number(matched[1]);
|
|
1668
|
+
return Number.isSafeInteger(index) ? index : undefined;
|
|
1669
|
+
}
|
|
1670
|
+
function addMcpAppAuditBytes(left, right) {
|
|
1671
|
+
return left > Number.MAX_SAFE_INTEGER - right ? Number.MAX_SAFE_INTEGER : left + right;
|
|
1672
|
+
}
|
|
1673
|
+
async function runMcpAppAuditExclusive(state, operation) {
|
|
1674
|
+
const previous = state.gate;
|
|
1675
|
+
let release;
|
|
1676
|
+
state.gate = new Promise((resolveGate) => { release = resolveGate; });
|
|
1677
|
+
await previous;
|
|
1678
|
+
try {
|
|
1679
|
+
return await operation();
|
|
1680
|
+
}
|
|
1681
|
+
finally {
|
|
1682
|
+
release();
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function isSupportedProtocolVersion(value) {
|
|
1686
|
+
return typeof value === "string"
|
|
1687
|
+
&& MCP_APP_SUPPORTED_VERSIONS.includes(value);
|
|
1688
|
+
}
|
|
1689
|
+
function record(value) {
|
|
1690
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1691
|
+
? value
|
|
1692
|
+
: undefined;
|
|
1693
|
+
}
|
|
1694
|
+
function boundedText(value, maxBytes) {
|
|
1695
|
+
const text = wellFormed(value).normalize("NFC")
|
|
1696
|
+
.replace(BIDI_CONTROL, "")
|
|
1697
|
+
.replace(/[\u0000-\u001f\u007f]/gu, " ")
|
|
1698
|
+
.trim();
|
|
1699
|
+
let bounded = "";
|
|
1700
|
+
let bytes = 0;
|
|
1701
|
+
for (const codePoint of text) {
|
|
1702
|
+
const codePointBytes = Buffer.byteLength(codePoint, "utf8");
|
|
1703
|
+
if (bytes + codePointBytes > maxBytes)
|
|
1704
|
+
break;
|
|
1705
|
+
bounded += codePoint;
|
|
1706
|
+
bytes += codePointBytes;
|
|
1707
|
+
}
|
|
1708
|
+
return bounded;
|
|
1709
|
+
}
|
|
1710
|
+
function wellFormed(input) {
|
|
1711
|
+
let output = "";
|
|
1712
|
+
for (const codePoint of input) {
|
|
1713
|
+
const unit = codePoint.charCodeAt(0);
|
|
1714
|
+
output += codePoint.length === 1 && unit >= 0xd800 && unit <= 0xdfff ? "\ufffd" : codePoint;
|
|
1715
|
+
}
|
|
1716
|
+
return output;
|
|
1717
|
+
}
|
|
1718
|
+
function serializedBytes(value) {
|
|
1719
|
+
try {
|
|
1720
|
+
return Buffer.byteLength(JSON.stringify(value) ?? "null", "utf8");
|
|
1721
|
+
}
|
|
1722
|
+
catch {
|
|
1723
|
+
return Number.POSITIVE_INFINITY;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
function serializeBoundedJsonLine(value, maxBytes) {
|
|
1727
|
+
const serialized = `${JSON.stringify(value) ?? "null"}\n`;
|
|
1728
|
+
if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
|
|
1729
|
+
throw new RangeError("MCP App persisted metadata exceeded its byte limit.");
|
|
1730
|
+
}
|
|
1731
|
+
return serialized;
|
|
1732
|
+
}
|
|
1733
|
+
function boundedJsonValue(value, maxBytes) {
|
|
1734
|
+
try {
|
|
1735
|
+
const serialized = JSON.stringify(value) ?? "null";
|
|
1736
|
+
if (Buffer.byteLength(serialized, "utf8") <= maxBytes)
|
|
1737
|
+
return JSON.parse(serialized);
|
|
1738
|
+
}
|
|
1739
|
+
catch {
|
|
1740
|
+
// Fall through to a deterministic omission marker.
|
|
1741
|
+
}
|
|
1742
|
+
return { truncated: true, reason: "value exceeded the MCP App host limit" };
|
|
1743
|
+
}
|
|
1744
|
+
async function readBoundedText(path, maxBytes) {
|
|
1745
|
+
const fileStat = await stat(path);
|
|
1746
|
+
if (!fileStat.isFile() || fileStat.size > maxBytes)
|
|
1747
|
+
throw new CodedError("app_missing", "The MCP App resource is unavailable.");
|
|
1748
|
+
return await readFile(path, "utf8");
|
|
1749
|
+
}
|
|
1750
|
+
async function readBoundedJson(path, maxBytes) {
|
|
1751
|
+
return JSON.parse(await readBoundedText(path, maxBytes));
|
|
1752
|
+
}
|
|
1753
|
+
//# sourceMappingURL=mcp-apps.js.map
|