@epoch-agent/runtime 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { loadCommandDefinitions, SLASH_COMMANDS_DIAG_MODULE, createToolScope, expandCommand, RecentWorkspaces, clearModelCache, discoverModels, ScheduleStore, isWithinWindow, acquireScheduleLock, triggerPeriodMs, ScheduleRecorder, pruneRecordings, suggestRuleFromApproval, needsAllowlist, isNonInteractive, loadPlugins, assertLevelAllowed, discoverProject, ProviderRouter, ProviderError, PROVIDER_NO_CREDENTIAL, SessionManager, GoalService, MemoryManager, MemoryReviewer, resolveSkillDirs, SkillSystem, SkillLearner, HookManager, loadHookSources, ApprovalCache, PermissionManager, resolvePolicyDirs, resolvePoliciesDir, loadPolicyFiles, checkShadowing, compileRuleLists, ContextCompressor, compressionOptions, TrackerDB, getConfigOrigins, mapOperationType, affectedPaths, workspaceDisplayPath, persistParts, persistToolResults, ToolRegistry, createTodoTool, createGoalTool, createMemoryTool, createCodeExecTool, createRunCodeTool, createSkillViewTool, createToolSearchTool, createToolRevealState, createAskQuestionTool, resolveWorkspace, unloadedInstructionFiles, resolveProjectRoot, loadConfig, CodeSandbox, CheckpointManager, createDelegateTool, findRole, resolveInWorkspace, listWorkspaceFiles, loadRoleDefinitions, mergeRoles, AGENT_ROLE_DIAG_MODULE, builtinRoles, unknownRoleTools, BLOCKED_TOOLS, blockedRoleTools, unknownRoleSkills, BudgetStore, mergeHeadlessPolicy, describeHeadlessPolicy, importSkills, previewSkillImport, removeSessionArtifacts, gateForTool, managedSwitchNotes, originOf, EpochConfigSchema, defaultConfig, SettingsFileSchema, isWritableSettingKey, writeSettingValue, SETTING_WRITE_APPLY, resolveSettings, SETTING_WRITE_LAYERS, readProviderKeyEnv, validateSchedule, createAllowAllGate, setTrustGate, TrustManager, createTrustGate, createDenyAllGate, getImportGate, setImportGate, createAllowAllImportGate, ImportTrustStore, createDenyAllImportGate, createImportGate, getCapability, restoreParts, createSessionSearchTools, PlanModeState, createPlanModeTools, toolTableTokens, explainSetting, settingWriteTargets, uninstallPlugin, readMarketplaces, searchMarketplaces, resolveEntrySource, resolveSource, readPluginRecords, createRoleFile, AgentLoop, resolveArtifactRetention, roleScopedProvider, createRoleScope, revealScopedProvider, withGoalPrompt, withGoalCompression, ScheduleRegistrar, resolveScheduleBackend, updateMarketplace, addMarketplace, SETTINGS_LAYER_ORDER, resolveMarketplaceRef, BudgetGuard, installPlugin, currentEpochVersion } from '@epoch-agent/core';
1
+ import { loadCommandDefinitions, SLASH_COMMANDS_DIAG_MODULE, createToolScope, expandCommand, RecentWorkspaces, clearModelCache, discoverModels, substitutePluginRoot, ScheduleStore, isWithinWindow, acquireScheduleLock, triggerPeriodMs, ScheduleRecorder, pruneRecordings, suggestRuleFromApproval, needsAllowlist, isNonInteractive, loadPlugins, assertLevelAllowed, discoverProject, ProviderRouter, ProviderError, PROVIDER_NO_CREDENTIAL, SessionManager, GoalService, MemoryManager, MemoryReviewer, resolveSkillDirs, SkillSystem, SkillLearner, HookManager, loadHookSources, ApprovalCache, PermissionManager, resolvePolicyDirs, resolvePoliciesDir, loadPolicyFiles, checkShadowing, compileRuleLists, ContextCompressor, compressionOptions, TrackerDB, getConfigOrigins, resolveComplianceDirs, resolveComplianceDir, loadLexiconFiles, ComplianceEngine, DEFAULT_COMPLIANCE_SETTINGS, mapOperationType, affectedPaths, workspaceDisplayPath, persistParts, persistToolResults, ToolRegistry, createTodoTool, createGoalTool, createMemoryTool, createCodeExecTool, createRunCodeTool, createSkillViewTool, createSkillWriteTools, createToolSearchTool, createToolRevealState, createAskQuestionTool, resolveWorkspace, unloadedInstructionFiles, resolveProjectRoot, loadConfig, CodeSandbox, CheckpointManager, createDelegateTool, findRole, persistedUsage, systemOpenArgv, resolveInWorkspace, listWorkspaceFiles, loadRoleDefinitions, mergeRoles, AGENT_ROLE_DIAG_MODULE, builtinRoles, unknownRoleTools, BLOCKED_TOOLS, blockedRoleTools, unknownRoleSkills, BudgetStore, mergeHeadlessPolicy, describeHeadlessPolicy, importSkills, previewSkillImport, removeSessionArtifacts, gateForTool, managedSwitchNotes, originOf, EpochConfigSchema, defaultConfig, SettingsFileSchema, isWritableSettingKey, writeSettingValue, SETTING_WRITE_APPLY, resolveSettings, SETTING_WRITE_LAYERS, readProviderKeyEnv, validateSchedule, createAllowAllGate, setTrustGate, TrustManager, createTrustGate, createDenyAllGate, getImportGate, setImportGate, createAllowAllImportGate, ImportTrustStore, createDenyAllImportGate, createImportGate, getCapability, restoreParts, createSessionSearchTools, PlanModeState, createPlanModeTools, toolTableTokens, explainSetting, settingWriteTargets, settingValueKind, settingValueChoices, uninstallPlugin, removeMarketplace, addMarketplace, readMarketplaces, searchMarketplaces, resolveEntrySource, resolveSource, readPluginRecords, createRoleFile, AgentLoop, resolveArtifactRetention, roleScopedProvider, createRoleScope, revealScopedProvider, withGoalPrompt, withGoalCompression, ScheduleRegistrar, resolveScheduleBackend, updateMarketplace, SETTINGS_LAYER_ORDER, resolveMarketplaceRef, BudgetGuard, installPlugin, currentEpochVersion } from '@epoch-agent/core';
2
2
  export { DEFAULT_GOAL_MAX_ROUNDS, MAX_GOAL_MAX_ROUNDS, MAX_OBJECTIVE_CHARS, MAX_REASON_CHARS, SANDBOX_COVERS, SANDBOX_EXCLUDES, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, nextRunAt, readRecording, skillIndexDescription } from '@epoch-agent/core';
3
- import { projectCommandsDir, commandsDir, t, issueDetails, resolveHomeDir, configPath, readKey, setScalar, setSectionField, mcpConfigPath, workspacesPath, dbPath, automationLogsDir, pluginsStatePath, safeInit, memoriesDir, skillsDir, claudeUserSettingsPath, hooksPath, artifactsDir, resolveRipgrep, withTimeout, createProcessTable, marketplacesPath, pluginsDir, agentsDir, budgetStatePath, createSecretStore, setSecretStore, prefetchProviderSecrets, setSecretValues, migrateEnvSecrets, envPath, setDataKey, parseDataKey, generateDataKey, i18nDiagnostics, automationWorkDir, trustPath, trustedImportsPath, setShell, probeShell } from '@epoch-agent/infra';
3
+ import { projectCommandsDir, commandsDir, t, issueDetails, resolveHomeDir, configPath, readKey, setScalar, setSectionField, mcpConfigPath, workspacesPath, writeProviderSecret, envPath, maskApiKey, dbPath, automationLogsDir, pluginsStatePath, safeInit, memoriesDir, skillsDir, claudeUserSettingsPath, hooksPath, artifactsDir, resolveRipgrep, withTimeout, createProcessTable, marketplacesPath, pluginsDir, agentsDir, budgetStatePath, createSecretStore, setSecretStore, prefetchProviderSecrets, setSecretValues, migrateEnvSecrets, setDataKey, parseDataKey, generateDataKey, i18nDiagnostics, automationWorkDir, trustPath, trustedImportsPath, setShell, probeShell } from '@epoch-agent/infra';
4
4
  export { t } from '@epoch-agent/infra';
5
- import { isAbsolute, dirname, delimiter, resolve, join } from 'path';
5
+ import { isAbsolute, dirname, delimiter, resolve, join, relative, sep } from 'path';
6
6
  import { AGENT_ROLE_NAME_PATTERN, isValidAgentRoleName, apiKeyEnvVar, getProviderInfo, PROVIDER_INFOS, parseModelRef, contentToText, assistantTurnText, systemNote, ToolExposure, isMentionableFilePath, DiagnosticSink, METRIC, API_KEY_ENV_VARS, MCP_DATA_KEY_NAME, isPermissionLevel, NOOP_TELEMETRY, DEFAULT_AGENT_ROLE } from '@epoch-agent/protocol';
7
- import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync, renameSync, rmSync, appendFileSync } from 'fs';
7
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync, renameSync, rmSync, realpathSync, openSync, readSync, closeSync, appendFileSync } from 'fs';
8
8
  import { randomUUID, createHash } from 'crypto';
9
9
  import { parseMcpConfig, McpRegistry, McpAuthStore, mcpAuthStatus, loginToMcpServer, logoutFromMcpServer } from '@epoch-agent/plugin-mcp';
10
10
  import { filePlugin } from '@epoch-agent/plugin-file';
@@ -58,7 +58,7 @@ function wireCommands(opts) {
58
58
  "no-command-loaded"
59
59
  );
60
60
  }
61
- const available = new Set(toolProvider.list().map((t23) => t23.name));
61
+ const available = new Set(toolProvider.list().map((t25) => t25.name));
62
62
  for (const cmd of summary.commands) {
63
63
  const unknown = (cmd.allowedTools ?? []).filter((name) => !available.has(name));
64
64
  if (unknown.length > 0) {
@@ -589,6 +589,223 @@ var init_host_preset = __esm({
589
589
  HOST_PRESET_MODULE = "HostPreset";
590
590
  }
591
591
  });
592
+ function fail(message, start, code, suggestion) {
593
+ return {
594
+ success: false,
595
+ output: "",
596
+ error: { code, message, ...suggestion ? { suggestion } : {} },
597
+ duration: Date.now() - start
598
+ };
599
+ }
600
+ function done(output, start) {
601
+ return { success: true, output, duration: Date.now() - start };
602
+ }
603
+ function str(args, key) {
604
+ const raw = args[key];
605
+ return typeof raw === "string" ? raw : "";
606
+ }
607
+ function toAddInput(args) {
608
+ const name = str(args, "name").trim();
609
+ if (!name) return "name must not be empty";
610
+ const transport = str(args, "transport").trim();
611
+ if (transport !== "stdio" && transport !== "sse" && transport !== "http") {
612
+ return `transport must be one of "stdio" / "sse" / "http", got ${JSON.stringify(transport)}`;
613
+ }
614
+ const rawArgs = args["args"];
615
+ if (rawArgs !== void 0 && !isStringArray(rawArgs)) {
616
+ return "args must be an array of strings";
617
+ }
618
+ const rawEnv = args["env"];
619
+ if (rawEnv !== void 0 && !isStringRecord(rawEnv)) {
620
+ return "env must be an object whose values are all strings";
621
+ }
622
+ const command = str(args, "command").trim();
623
+ const url = str(args, "url").trim();
624
+ return {
625
+ name,
626
+ transport,
627
+ ...command ? { command } : {},
628
+ ...rawArgs === void 0 ? {} : { args: rawArgs },
629
+ ...url ? { url } : {},
630
+ ...rawEnv === void 0 ? {} : { env: rawEnv }
631
+ };
632
+ }
633
+ function isStringArray(value) {
634
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
635
+ }
636
+ function isStringRecord(value) {
637
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
638
+ return Object.values(value).every((v) => typeof v === "string");
639
+ }
640
+ function listTool(deps) {
641
+ return {
642
+ name: "mcp_list",
643
+ description: "\u5217\u51FA\u8FD9\u4E2A\u8FDB\u7A0B\u91CC\u6240\u6709 MCP server \u7684\u72B6\u6001\uFF1A\u540D\u5B57\u3001\u6765\u6E90\u3001\u8FDE\u4E0A\u6CA1\u6709\u3001\u51E0\u4E2A\u5DE5\u5177\u3001\u4E0A\u6B21\u9519\u5728\u54EA\u3002\u6539\u4E00\u53F0\u4E4B\u524D\u5148\u770B\u4E00\u773C\u3002\nsource \u51B3\u5B9A\u6539\u4E0D\u6539\u5F97\u52A8\uFF1A\u53EA\u6709 user \u90A3\u6279\u662F\u7528\u6237\u914D\u7684\uFF1Bhost / plugin \u4E24\u6279\u4E0D\u5728 mcp.json \u91CC\uFF0C\u6539\u4E0D\u4E86\u3002\n\u26A0\uFE0F \u4E0D\u56DE command / url / env \u2014\u2014 \u90A3\u51E0\u683C\u5E38\u85CF\u4EE4\u724C\uFF0C\u8981\u770B\u8BA9\u7528\u6237\u81EA\u5DF1\u6253\u5F00 ~/.epoch/mcp.json\u3002",
644
+ parameters: { type: "object", properties: {} },
645
+ exposure: ToolExposure.Direct,
646
+ supportsParallel: true,
647
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
648
+ operation: "file_read",
649
+ describeTarget: () => MCP_LIST_TOOL,
650
+ execute() {
651
+ const start = Date.now();
652
+ const rows = deps.servers();
653
+ if (rows.length === 0) {
654
+ return Promise.resolve(
655
+ done("No MCP servers are configured in this process. Use mcp_add to add one.", start)
656
+ );
657
+ }
658
+ const lines = rows.map((s) => {
659
+ const state = s.connected ? "connected" : "disconnected";
660
+ const login = s.needsLogin === true ? ", needs login" : "";
661
+ const err = s.lastError === void 0 ? "" : ` \u2014 ${s.lastError}`;
662
+ return `${s.name} [${s.source}] ${state}, ${s.toolCount} tools${login}${err}`;
663
+ });
664
+ return Promise.resolve(done(lines.join("\n"), start));
665
+ }
666
+ };
667
+ }
668
+ function runWrite(deps, args, kind) {
669
+ const start = Date.now();
670
+ const input = toAddInput(args);
671
+ if (typeof input === "string") return Promise.resolve(fail(input, start, 3201));
672
+ const call = kind === "add" ? deps.control.add(input) : deps.control.update(input);
673
+ return call.then((outcome) => {
674
+ if (!outcome.ok) {
675
+ return fail(
676
+ `${outcome.reason}: ${outcome.detail}`,
677
+ start,
678
+ 3202,
679
+ HINT_BY_REASON[outcome.reason]
680
+ );
681
+ }
682
+ return done(describeWrite(kind, outcome.configPath, outcome.status), start);
683
+ });
684
+ }
685
+ function describeWrite(kind, configPath2, status) {
686
+ const verb = kind === "add" ? "Added" : "Updated";
687
+ const why = status.lastError === void 0 ? "" : ` \u2014 ${status.lastError}`;
688
+ const login = status.needsLogin === true ? " It needs a login: run `epoch mcp login`." : "";
689
+ const live = status.connected ? `It is connected and exposes ${status.toolCount} tools now.` : `It is NOT connected right now${why}. The config is saved either way \u2014 the server may just not be up yet.`;
690
+ return `${verb} MCP server "${status.name}" in ${configPath2}. ${live}${login}`;
691
+ }
692
+ function addTool(deps) {
693
+ return {
694
+ name: "mcp_add",
695
+ description: "\u52A0\u4E00\u53F0 MCP server\uFF1A\u5199\u8FDB ~/.epoch/mcp.json\uFF0C\u7136\u540E\u5F53\u573A\u8FDE\u4E00\u6B21\u3002\n\u26A0\uFE0F \u5F71\u54CD\u7684\u4E0D\u53EA\u662F\u8FD9\u6BB5\u4F1A\u8BDD \u2014\u2014 \u90A3\u53F0\u4EE5\u540E\u6BCF\u4E2A epoch \u8FDB\u7A0B\u90FD\u4F1A\u8FDE\u5B83\uFF08stdio \u90A3\u6863\u8981\u8D77\u5B50\u8FDB\u7A0B\uFF09\u3002\u52A0\u4E4B\u524D\u5148\u8DDF\u7528\u6237\u8BF4\u6E05\u4F60\u8981\u52A0\u4EC0\u4E48\u3002\n\u8FDE\u4E0D\u4E0A\u4E0D\u7B97\u5931\u8D25\uFF08\u914D\u7F6E\u5BF9\u3001\u5BF9\u9762\u6CA1\u8D77\u6765\u7167\u6837\u52A0\u5F97\u8FDB\u53BB\uFF0C\u56DE\u6267\u4F1A\u8BF4\u6E05\uFF09\u3002\u540D\u5B57\u88AB\u5360\u4F1A\u88AB\u62D2 \u2014\u2014 \u90A3\u65F6\u6539\u7528 mcp_update\u3002",
696
+ parameters: {
697
+ type: "object",
698
+ properties: SERVER_PROPERTIES,
699
+ required: ["name", "transport"]
700
+ },
701
+ exposure: ToolExposure.Direct,
702
+ supportsParallel: false,
703
+ annotations: ADMIN_ANNOTATIONS,
704
+ operation: "command",
705
+ describeTarget: (args) => `${MCP_ADD_TOOL}:${String(args["name"] ?? "")} ${String(args["command"] ?? args["url"] ?? "")}`.trim(),
706
+ execute: (args) => runWrite(deps, args, "add")
707
+ };
708
+ }
709
+ function updateTool(deps) {
710
+ return {
711
+ name: "mcp_update",
712
+ description: "\u6539\u4E00\u53F0\u5DF2\u6709 MCP server\uFF1A\u5C31\u5730\u6362\u6389 ~/.epoch/mcp.json \u91CC\u90A3\u4E00\u6BB5\uFF0C\u518D\u6309\u65B0\u914D\u7F6E\u91CD\u8FDE\u5B83\u3002\u522B\u7684 server \u548C\u6587\u4EF6\u91CC\u7684\u6CE8\u91CA\u6392\u7248\u4E00\u4E2A\u5B57\u8282\u4E0D\u52A8\u3002\n\u26A0\uFE0F \u6536\u7684\u662F**\u6574\u5957**\u914D\u7F6E\u4E0D\u662F\u8865\u4E01 \u2014\u2014 \u6CA1\u7ED9\u7684\u683C\u5B50\uFF08args / url / env\uFF09\u4F1A\u88AB\u5F53\u6210\u300C\u4E0D\u8981\u4E86\u300D\uFF0C\u8981\u4FDD\u4F4F\u7684\u6BCF\u6B21\u90FD\u5F97\u539F\u6837\u518D\u7ED9\u3002\n\u540D\u5B57\u6539\u4E0D\u4E86\uFF08\u90A3\u7B49\u4E8E\u6362\u4E00\u6279\u5DE5\u5177\u540D\uFF0C\u8981\u6539\u540D\u5C31 mcp_remove \u518D mcp_add\uFF09\u3002\u6587\u4EF6\u91CC\u6CA1\u8FD9\u4E2A\u540D\u5B57\u4F1A\u88AB\u62D2\u3002",
713
+ parameters: {
714
+ type: "object",
715
+ properties: SERVER_PROPERTIES,
716
+ required: ["name", "transport"]
717
+ },
718
+ exposure: ToolExposure.Direct,
719
+ supportsParallel: false,
720
+ annotations: ADMIN_ANNOTATIONS,
721
+ operation: "command",
722
+ describeTarget: (args) => `${MCP_UPDATE_TOOL}:${String(args["name"] ?? "")} ${String(args["command"] ?? args["url"] ?? "")}`.trim(),
723
+ execute: (args) => runWrite(deps, args, "update")
724
+ };
725
+ }
726
+ function removeTool(deps) {
727
+ return {
728
+ name: "mcp_remove",
729
+ description: "\u5220\u6389\u4E00\u53F0 MCP server\uFF1A\u4ECE ~/.epoch/mcp.json \u91CC\u53BB\u6389\u90A3\u4E00\u6BB5\uFF0C\u5E76\u65AD\u5F00\u5B83\u3001\u628A\u5B83\u90A3\u6279\u5DE5\u5177\u6458\u6389\u3002\u522B\u7684 server \u548C\u6CE8\u91CA\u6392\u7248\u4E00\u4E2A\u5B57\u8282\u4E0D\u52A8\u3002\n\u26A0\uFE0F \u4E0D\u53EF\u64A4\u9500 \u2014\u2014 \u90A3\u4E00\u6BB5\u914D\u7F6E\uFF08\u542B env \u91CC\u7684\u4EE4\u724C\uFF09\u4ECE\u6587\u4EF6\u91CC\u6D88\u5931\uFF0C\u8981\u56DE\u6765\u53EA\u80FD\u91CD\u586B\u3002\u5220\u4E4B\u524D\u5148\u8DDF\u7528\u6237\u786E\u8BA4\u3002\n\u6587\u4EF6\u91CC\u6CA1\u8FD9\u4E2A\u540D\u5B57\u4F1A\u88AB\u62D2\uFF08host / plugin \u90A3\u4E24\u6279\u5220\u4E0D\u6389\uFF09\u3002",
730
+ parameters: {
731
+ type: "object",
732
+ properties: { name: SERVER_PROPERTIES.name },
733
+ required: ["name"]
734
+ },
735
+ exposure: ToolExposure.Direct,
736
+ supportsParallel: false,
737
+ annotations: ADMIN_ANNOTATIONS,
738
+ operation: "command",
739
+ describeTarget: (args) => `${MCP_REMOVE_TOOL}:${String(args["name"] ?? "")}`,
740
+ async execute(args) {
741
+ const start = Date.now();
742
+ const name = str(args, "name").trim();
743
+ if (!name) return fail("name must not be empty", start, 3201);
744
+ const outcome = await deps.control.remove(name);
745
+ if (!outcome.ok) {
746
+ return fail(
747
+ `${outcome.reason}: ${outcome.detail}`,
748
+ start,
749
+ 3202,
750
+ HINT_BY_REASON[outcome.reason]
751
+ );
752
+ }
753
+ return done(
754
+ `removed MCP server "${name}" from ${outcome.configPath}. Its tools are gone from this process.`,
755
+ start
756
+ );
757
+ }
758
+ };
759
+ }
760
+ function createMcpTools(deps) {
761
+ return [listTool(deps), addTool(deps), updateTool(deps), removeTool(deps)];
762
+ }
763
+ var MCP_LIST_TOOL, MCP_ADD_TOOL, MCP_UPDATE_TOOL, MCP_REMOVE_TOOL, SERVER_PROPERTIES, ADMIN_ANNOTATIONS, HINT_BY_REASON;
764
+ var init_mcp_tools = __esm({
765
+ "src/mcp-tools.ts"() {
766
+ MCP_LIST_TOOL = "mcp_list";
767
+ MCP_ADD_TOOL = "mcp_add";
768
+ MCP_UPDATE_TOOL = "mcp_update";
769
+ MCP_REMOVE_TOOL = "mcp_remove";
770
+ SERVER_PROPERTIES = {
771
+ name: {
772
+ type: "string",
773
+ description: "server \u540D\u3002\u4F1A\u539F\u6837\u53D8\u6210\u5DE5\u5177\u540D\u7684\u4E00\u90E8\u5206\uFF08mcp__<name>__<\u5DE5\u5177>\uFF09"
774
+ },
775
+ transport: {
776
+ type: "string",
777
+ enum: ["stdio", "sse", "http"],
778
+ description: "\u4F20\u8F93\u7C7B\u578B\uFF1Astdio \u8D77\u4E00\u4E2A\u672C\u5730\u5B50\u8FDB\u7A0B\uFF0Csse / http \u8FDE\u4E00\u4E2A\u5730\u5740"
779
+ },
780
+ command: { type: "string", description: "stdio \u90A3\u4E00\u6863\u7684\u53EF\u6267\u884C\u6587\u4EF6\uFF0C\u5982 npx" },
781
+ args: {
782
+ type: "array",
783
+ items: { type: "string" },
784
+ description: "stdio \u90A3\u4E00\u6863\u7684\u547D\u4EE4\u884C\u53C2\u6570\uFF0C\u5143\u7D20\u5FC5\u987B\u5168\u662F\u5B57\u7B26\u4E32"
785
+ },
786
+ url: { type: "string", description: "sse / http \u90A3\u4E24\u6863\u7684\u5730\u5740" },
787
+ env: {
788
+ type: "object",
789
+ additionalProperties: { type: "string" },
790
+ description: "\u5B50\u8FDB\u7A0B\u73AF\u5883\u53D8\u91CF\uFF0C\u503C\u5FC5\u987B\u5168\u662F\u5B57\u7B26\u4E32"
791
+ }
792
+ };
793
+ ADMIN_ANNOTATIONS = {
794
+ readOnlyHint: false,
795
+ destructiveHint: true,
796
+ idempotentHint: false,
797
+ openWorldHint: true
798
+ };
799
+ HINT_BY_REASON = {
800
+ "invalid-name": 'Use only letters, digits, "_" and "-", starting with a letter or digit (max 48 chars) \u2014 the name becomes part of every tool name this server exposes.',
801
+ "invalid-config": 'stdio needs "command"; sse and http need a valid "url". Fix that one field and retry.',
802
+ duplicate: "That name is already taken \u2014 call mcp_list, then use mcp_update instead of mcp_add.",
803
+ "not-found": "That name is not in ~/.epoch/mcp.json \u2014 call mcp_list first, or use mcp_add.",
804
+ unwritable: "The config file could not be read, written, or parsed. Do not retry \u2014 tell the user to look at ~/.epoch/mcp.json themselves.",
805
+ "apply-failed": "The config file was written but this process could not be brought in line. The change takes effect on the next epoch start."
806
+ };
807
+ }
808
+ });
592
809
  function providerLabel(info, lang) {
593
810
  switch (info.type) {
594
811
  case "ollama":
@@ -607,12 +824,16 @@ function buildModelCatalogControl(opts) {
607
824
  };
608
825
  return {
609
826
  providers() {
610
- return PROVIDER_INFOS.filter((p) => p.interactive).map((p) => ({
611
- type: p.type,
612
- label: p.label,
613
- envVar: p.apiKeyEnv,
614
- hasKey: p.apiKeyEnv === null || keyOf(p.apiKeyEnv) !== ""
615
- }));
827
+ return PROVIDER_INFOS.filter((p) => p.interactive).map((p) => {
828
+ const key = p.apiKeyEnv ? keyOf(p.apiKeyEnv) : "";
829
+ return {
830
+ type: p.type,
831
+ label: p.label,
832
+ envVar: p.apiKeyEnv,
833
+ hasKey: p.apiKeyEnv === null || key !== "",
834
+ keyHint: key === "" ? null : maskApiKey(key)
835
+ };
836
+ });
616
837
  },
617
838
  async discover(provider, discoverOpts) {
618
839
  const envVar = getProviderInfo(provider)?.apiKeyEnv ?? null;
@@ -632,6 +853,19 @@ function buildModelCatalogControl(opts) {
632
853
  source: result.source,
633
854
  status: probeStatus(result, { needsKey: envVar !== null, hasKey: apiKey !== "" })
634
855
  };
856
+ },
857
+ async setKey(provider, apiKey) {
858
+ const envVar = getProviderInfo(provider)?.apiKeyEnv ?? null;
859
+ if (!envVar) throw new Error(t("provider.key_not_applicable", { provider }));
860
+ const written = await writeProviderSecret(envVar, apiKey, envPath(homeDir));
861
+ clearModelCache(homeDir, provider);
862
+ return {
863
+ envVar,
864
+ backend: written.backend,
865
+ encrypted: written.encrypted,
866
+ envPath: written.envPath,
867
+ keyHint: maskApiKey(apiKey)
868
+ };
635
869
  }
636
870
  };
637
871
  }
@@ -761,6 +995,15 @@ function createPluginControl(deps) {
761
995
  }
762
996
  return { ok: true, source: hit.ref.source, marketplace: hit.ref.marketplace };
763
997
  };
998
+ const resolveLocal = (path, lang) => {
999
+ const refuse2 = {
1000
+ ok: false,
1001
+ reason: "not-local",
1002
+ detail: t("plugin_control.not_local_path", { path: path === "" ? "(empty)" : path }, lang)
1003
+ };
1004
+ if (path === "" || !isAbsolute(path)) return refuse2;
1005
+ return resolveSource(path)?.type === "local" ? { ok: true, source: path } : refuse2;
1006
+ };
764
1007
  const recordOf = (name, lang) => {
765
1008
  const record = readPluginRecords(deps.statePath).records.find((r) => r.name === name);
766
1009
  if (!record) {
@@ -817,6 +1060,58 @@ function createPluginControl(deps) {
817
1060
  const target = resolveRef(ref, lang);
818
1061
  return target.ok ? mark(await flow.install(target.source, token, { marketplace: target.marketplace }, lang)) : target;
819
1062
  },
1063
+ previewLocal: async (path, lang) => {
1064
+ const target = resolveLocal(path, lang);
1065
+ return target.ok ? flow.preview(target.source, {}, lang) : target;
1066
+ },
1067
+ installLocal: async (path, token, lang) => {
1068
+ const target = resolveLocal(path, lang);
1069
+ return target.ok ? mark(await flow.install(target.source, token, {}, lang)) : target;
1070
+ },
1071
+ marketplaces: () => readMarketplaces(deps.marketplacesPath).records.map((r) => ({
1072
+ name: r.name,
1073
+ source: r.source,
1074
+ entries: r.catalog.plugins.length,
1075
+ addedAt: r.addedAt,
1076
+ local: isLocalSource(r.source)
1077
+ })),
1078
+ addMarketplace: async (source, lang) => {
1079
+ const raw = source.trim();
1080
+ if (raw === "") {
1081
+ return {
1082
+ ok: false,
1083
+ reason: "unknown-ref",
1084
+ detail: t("plugin_control.market_source_empty", void 0, lang)
1085
+ };
1086
+ }
1087
+ if (!deps.allowRemote && !isLocalSource(raw)) {
1088
+ return {
1089
+ ok: false,
1090
+ reason: "remote-disabled",
1091
+ detail: t("plugin_control.market_add_remote_off", { source: raw }, lang)
1092
+ };
1093
+ }
1094
+ const outcome = await addMarketplace(raw, { statePath: deps.marketplacesPath });
1095
+ if (!outcome.ok) return { ok: false, reason: "failed", detail: outcome.reason };
1096
+ return {
1097
+ ok: true,
1098
+ marketplace: {
1099
+ name: outcome.record.name,
1100
+ source: outcome.record.source,
1101
+ entries: outcome.record.catalog.plugins.length,
1102
+ addedAt: outcome.record.addedAt,
1103
+ local: isLocalSource(outcome.record.source)
1104
+ }
1105
+ };
1106
+ },
1107
+ removeMarketplace: async (name, lang) => {
1108
+ const outcome = await removeMarketplace(name, { statePath: deps.marketplacesPath });
1109
+ return outcome.ok ? { ok: true } : {
1110
+ ok: false,
1111
+ reason: "not-installed",
1112
+ detail: outcome.reason ?? t("plugin_control.market_missing", { name }, lang)
1113
+ };
1114
+ },
820
1115
  previewUpdate: async (name, lang) => {
821
1116
  const found = recordOf(name, lang);
822
1117
  return found.ok ? flow.preview(found.record.source, replaceOpts(found.record), lang) : found;
@@ -871,6 +1166,7 @@ function readOne(ref, diags) {
871
1166
  for (const issue of parsed.issues) {
872
1167
  diags.warn(`${MODULE} ${issue.path}`, issue.message, "mcp-config-invalid");
873
1168
  }
1169
+ const root = dirname(ref.path);
874
1170
  const servers = [];
875
1171
  for (const server of parsed.servers) {
876
1172
  if (!AGENT_ROLE_NAME_PATTERN.test(server.name)) {
@@ -881,10 +1177,21 @@ function readOne(ref, diags) {
881
1177
  );
882
1178
  continue;
883
1179
  }
884
- servers.push({ ...server, name: `${ref.plugin}__${server.name}` });
1180
+ servers.push(withPluginRoot({ ...server, name: `${ref.plugin}__${server.name}` }, root));
885
1181
  }
886
1182
  return servers;
887
1183
  }
1184
+ function withPluginRoot(server, root) {
1185
+ const out = { ...server };
1186
+ if (out.command !== void 0) out.command = substitutePluginRoot(out.command, root);
1187
+ if (out.args !== void 0) out.args = out.args.map((a) => substitutePluginRoot(a, root));
1188
+ if (out.env !== void 0) {
1189
+ out.env = Object.fromEntries(
1190
+ Object.entries(out.env).map(([k, v]) => [k, substitutePluginRoot(v, root)])
1191
+ );
1192
+ }
1193
+ return out;
1194
+ }
888
1195
  function reason(err) {
889
1196
  return err instanceof Error ? err.message : String(err);
890
1197
  }
@@ -1204,7 +1511,7 @@ async function runOnce(store, def, opts, startedAt, now) {
1204
1511
  });
1205
1512
  return { status: "failed", exitCode: SCHEDULE_EXIT_CODES.FAILURE, run, detail: reason2 };
1206
1513
  }
1207
- const level = runtime.permissions.setLevel(def.permission);
1514
+ const level = runtime.permissions.setLevel(def.permission, { remember: false });
1208
1515
  if (!level.ok) {
1209
1516
  const reason2 = `managed-policy: ${def.permission}`;
1210
1517
  const run = store.recordRun({
@@ -1498,7 +1805,6 @@ function resolveModelCapability(config, diags) {
1498
1805
  const cap = getCapability(config.model, config.homeDir);
1499
1806
  return {
1500
1807
  contextLength: cap.contextLength,
1501
- supportsImages: cap.supportsImages,
1502
1808
  ...cap.pricing ? { pricing: cap.pricing } : {}
1503
1809
  };
1504
1810
  } catch {
@@ -1507,7 +1813,7 @@ function resolveModelCapability(config, diags) {
1507
1813
  t("boot.no_model_metadata", { model: config.model }),
1508
1814
  "no-model-metadata"
1509
1815
  );
1510
- return { contextLength: config.contextLength, supportsImages: false };
1816
+ return { contextLength: config.contextLength };
1511
1817
  }
1512
1818
  }
1513
1819
  function reportUtilityModel(info, diags) {
@@ -1534,7 +1840,7 @@ function buildTrust(config, workDir, diags, interactive) {
1534
1840
  const gate2 = createAllowAllGate(t("boot.trust_disabled"));
1535
1841
  setTrustGate(gate2);
1536
1842
  const trust2 = gate2.decide(workDir);
1537
- diags.warn("Trust", trust2.reason, "trust-disabled");
1843
+ diags.skipped("Trust", trust2.reason);
1538
1844
  const disabled = safeInit("Trust", () => new TrustManager(trustPath(config.homeDir)), diags);
1539
1845
  for (const err of disabled?.loadErrors ?? []) diags.warn("Trust", err, "trust-load-error");
1540
1846
  return { manager: disabled, trust: trust2, root, gate: gate2 };
@@ -1630,11 +1936,19 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
1630
1936
  }
1631
1937
  }
1632
1938
  if (provider) provider.refreshMetadata();
1633
- const providerBridge = provider ? {
1634
- generate: provider.generate.bind(provider),
1635
- stream: provider.stream.bind(provider),
1939
+ const router = provider;
1940
+ const providerBridge = router ? {
1941
+ generate: router.generate.bind(router),
1942
+ stream: router.stream.bind(router),
1636
1943
  get capabilities() {
1637
- return { supportsImages: capability.supportsImages };
1944
+ try {
1945
+ return { supportsImages: router.getCapability().supportsImages };
1946
+ } catch {
1947
+ return { supportsImages: false };
1948
+ }
1949
+ },
1950
+ get modelId() {
1951
+ return router.getSelection().model;
1638
1952
  }
1639
1953
  } : null;
1640
1954
  const session = safeInit("Session", () => new SessionManager(config.dbPath), diags);
@@ -1765,6 +2079,7 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
1765
2079
  }
1766
2080
  }
1767
2081
  }
2082
+ const compliance = buildCompliance(config, projectContext.rootDir, trust.trusted, diags);
1768
2083
  let contextEngine = null;
1769
2084
  if (provider) {
1770
2085
  reportUtilityModel(provider.getUtilityInfo(), diags);
@@ -1816,9 +2131,53 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
1816
2131
  policy: policyStatus,
1817
2132
  hook: hookStatus,
1818
2133
  plugins,
2134
+ compliance,
1819
2135
  cleanups
1820
2136
  };
1821
2137
  }
2138
+ function buildCompliance(config, projectRoot, trusted, diags) {
2139
+ const settings = resolveComplianceSettings(config.compliance);
2140
+ if (!settings.enabled) {
2141
+ diags.skipped("Compliance", t("boot.compliance_off"));
2142
+ return null;
2143
+ }
2144
+ const rules = [];
2145
+ const exempt = [];
2146
+ try {
2147
+ const dirs = resolveComplianceDirs({
2148
+ userDir: resolveComplianceDir(config.homeDir),
2149
+ ...projectRoot ? { projectRoot } : {},
2150
+ trusted,
2151
+ diags
2152
+ });
2153
+ for (const spec of dirs.dirs) {
2154
+ const loaded = loadLexiconFiles(spec.dir, spec.source, diags);
2155
+ rules.push(...loaded.rules);
2156
+ exempt.push(...loaded.exempt);
2157
+ }
2158
+ } catch {
2159
+ diags.warn("Compliance", t("boot.compliance_load_failed"), "compliance-load-failed");
2160
+ }
2161
+ const engine = new ComplianceEngine({ settings, rules, exempt });
2162
+ diags.ok(
2163
+ "Compliance",
2164
+ t("boot.compliance_ready", {
2165
+ active: engine.activeRuleCount,
2166
+ builtin: engine.counts.builtin,
2167
+ external: engine.counts.user + engine.counts.project
2168
+ })
2169
+ );
2170
+ return engine;
2171
+ }
2172
+ function resolveComplianceSettings(raw) {
2173
+ return {
2174
+ enabled: raw?.enabled ?? DEFAULT_COMPLIANCE_SETTINGS.enabled,
2175
+ scope: { ...DEFAULT_COMPLIANCE_SETTINGS.scope, ...raw?.scope },
2176
+ skipCodeBlocks: raw?.skipCodeBlocks ?? DEFAULT_COMPLIANCE_SETTINGS.skipCodeBlocks,
2177
+ maxHoldChars: raw?.maxHoldChars ?? DEFAULT_COMPLIANCE_SETTINGS.maxHoldChars,
2178
+ actions: { ...DEFAULT_COMPLIANCE_SETTINGS.actions, ...raw?.actions }
2179
+ };
2180
+ }
1822
2181
  var init_services = __esm({
1823
2182
  "src/services.ts"() {
1824
2183
  }
@@ -1966,11 +2325,11 @@ var init_agent_session = __esm({
1966
2325
  }
1967
2326
  };
1968
2327
  return {
1969
- onApprovalRequest: (req) => new Promise((resolve3) => {
1970
- enqueue({ kind: "approval", req, resolve: resolve3 });
2328
+ onApprovalRequest: (req) => new Promise((resolve4) => {
2329
+ enqueue({ kind: "approval", req, resolve: resolve4 });
1971
2330
  }),
1972
- onQuestionRequest: (req) => new Promise((resolve3) => {
1973
- enqueue({ kind: "question", req, resolve: resolve3 });
2331
+ onQuestionRequest: (req) => new Promise((resolve4) => {
2332
+ enqueue({ kind: "question", req, resolve: resolve4 });
1974
2333
  }),
1975
2334
  attach: (session) => {
1976
2335
  target = session;
@@ -2090,8 +2449,8 @@ var init_agent_session = __esm({
2090
2449
  }
2091
2450
  continue;
2092
2451
  }
2093
- const { value: event, done } = await pendingNext;
2094
- if (done) break;
2452
+ const { value: event, done: done2 } = await pendingNext;
2453
+ if (done2) break;
2095
2454
  pendingNext = iter.next();
2096
2455
  if (event.type === "text-delta") assistantText += event.text;
2097
2456
  turn.observe(event);
@@ -2208,7 +2567,7 @@ function buildSessionGuards(input) {
2208
2567
  permission,
2209
2568
  planState,
2210
2569
  managed,
2211
- startLevel,
2570
+ defaultLevel,
2212
2571
  requestApproval,
2213
2572
  interactive,
2214
2573
  deferredTools
@@ -2218,7 +2577,7 @@ function buildSessionGuards(input) {
2218
2577
  const reveal = own ? createToolRevealState() : bootstrapReveal;
2219
2578
  return {
2220
2579
  permission: perm,
2221
- permissions: perm ? buildSessionPermissions(perm, managed, state) : null,
2580
+ permissions: perm ? buildSessionPermissions(perm, managed, state, defaultLevel) : null,
2222
2581
  plan: state ? buildPlanControl(state) : null,
2223
2582
  planState: state,
2224
2583
  tools: own ? [
@@ -2233,22 +2592,24 @@ function buildSessionGuards(input) {
2233
2592
  bootstrap: () => bootstrapScope,
2234
2593
  open: () => {
2235
2594
  if (!permission) return scopeOf(null, null, true);
2236
- const own = permission.scoped(startLevel);
2595
+ const own = permission.scoped(defaultLevel.get());
2237
2596
  return scopeOf(own, planState ? new PlanModeState(own) : null, true);
2238
2597
  }
2239
2598
  };
2240
2599
  }
2241
- function buildSessionPermissions(permission, managed, planState) {
2600
+ function buildSessionPermissions(permission, managed, planState, defaultLevel) {
2242
2601
  return {
2243
2602
  level: () => permission.getLevel(),
2244
2603
  managed: () => ({ bypassDisabled: managed.disableBypassPermissionsMode }),
2245
- setLevel: (level) => {
2604
+ setLevel: (level, opts) => {
2246
2605
  if (level === "bypass" && managed.disableBypassPermissionsMode) {
2247
2606
  return { ok: false, reason: "managed-bypass-disabled" };
2248
2607
  }
2249
2608
  permission.setLevel(level);
2250
2609
  planState?.forget();
2251
- return { ok: true };
2610
+ if (!opts.remember) return { ok: true };
2611
+ const failure = defaultLevel.remember(level);
2612
+ return failure ? { ok: true, notRemembered: failure } : { ok: true };
2252
2613
  }
2253
2614
  };
2254
2615
  }
@@ -2699,9 +3060,6 @@ var init_mcp_config_apply = __esm({
2699
3060
  init_mcp_config_file();
2700
3061
  }
2701
3062
  });
2702
- function isWritableName(name) {
2703
- return /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(name);
2704
- }
2705
3063
  function skipTrivia(raw, from) {
2706
3064
  let i = from;
2707
3065
  for (; ; ) {
@@ -2765,13 +3123,15 @@ function walkObject(raw, braceAt) {
2765
3123
  const entries = [];
2766
3124
  let i = skipTrivia(raw, braceAt + 1);
2767
3125
  while (i < raw.length && raw[i] !== "}") {
3126
+ const keyAt = i;
2768
3127
  const key = readString(raw, i);
2769
3128
  if (!key) return { entries, clean: false };
2770
3129
  const afterKey = skipTrivia(raw, key[1]);
2771
3130
  if (raw[afterKey] !== ":") return { entries, clean: false };
2772
3131
  const valueAt2 = skipTrivia(raw, afterKey + 1);
2773
- entries.push({ key: key[0], valueAt: valueAt2 });
2774
- i = skipTrivia(raw, skipValue(raw, valueAt2));
3132
+ const valueEnd = skipValue(raw, valueAt2);
3133
+ entries.push({ key: key[0], keyAt, valueAt: valueAt2, valueEnd });
3134
+ i = skipTrivia(raw, valueEnd);
2775
3135
  if (raw[i] === ",") i = skipTrivia(raw, i + 1);
2776
3136
  else if (raw[i] !== "}") return { entries, clean: false };
2777
3137
  }
@@ -2789,10 +3149,14 @@ function locate(raw) {
2789
3149
  }
2790
3150
  const inner = walkObject(raw, servers.valueAt);
2791
3151
  if (!inner.clean) return { ok: false, detail: t("mcp_write.servers_unreadable") };
2792
- const keys = inner.entries.map((e) => e.key);
2793
3152
  return {
2794
3153
  ok: true,
2795
- landing: { braceAt: servers.valueAt, hasMembers: keys.length > 0, keys, kind: "servers" }
3154
+ landing: {
3155
+ braceAt: servers.valueAt,
3156
+ hasMembers: inner.entries.length > 0,
3157
+ entries: inner.entries,
3158
+ kind: "servers"
3159
+ }
2796
3160
  };
2797
3161
  }
2798
3162
  if (top.entries.some((e) => e.key === "mcpServers")) {
@@ -2800,15 +3164,29 @@ function locate(raw) {
2800
3164
  }
2801
3165
  return {
2802
3166
  ok: true,
2803
- landing: { braceAt: start, hasMembers: top.entries.length > 0, keys: [], kind: "top" }
3167
+ landing: { braceAt: start, hasMembers: top.entries.length > 0, entries: [], kind: "top" }
2804
3168
  };
2805
3169
  }
2806
3170
  function indentOf(raw) {
2807
3171
  return /\n([ \t]+)\S/.exec(raw)?.[1] ?? " ";
2808
3172
  }
2809
- function renderEntry(input, indent) {
2810
- const pad = indent.repeat(2);
2811
- const inner = indent.repeat(3);
3173
+ function eolOf(raw) {
3174
+ const at = raw.indexOf("\n");
3175
+ if (at < 0) return "\n";
3176
+ return raw[at - 1] === "\r" ? "\r\n" : "\n";
3177
+ }
3178
+ function withEol(block, eol) {
3179
+ return eol === "\n" ? block : block.replaceAll("\n", eol);
3180
+ }
3181
+ var init_mcp_json_scan = __esm({
3182
+ "src/mcp-json-scan.ts"() {
3183
+ }
3184
+ });
3185
+ function isWritableName(name) {
3186
+ return /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(name);
3187
+ }
3188
+ function renderEntry(input, indent, pad = indent.repeat(2)) {
3189
+ const inner = pad + indent;
2812
3190
  const fields = [`${inner}"transport": ${JSON.stringify(input.transport)}`];
2813
3191
  if (input.command) fields.push(`${inner}"command": ${JSON.stringify(input.command)}`);
2814
3192
  if (input.args && input.args.length > 0) {
@@ -2832,7 +3210,7 @@ function insertServerText(raw, input) {
2832
3210
  const scan = locate(raw);
2833
3211
  if (!scan.ok) return { ok: false, reason: "unwritable", detail: scan.detail };
2834
3212
  const { landing } = scan;
2835
- if (landing.keys.includes(input.name)) {
3213
+ if (landing.entries.some((e) => e.key === input.name)) {
2836
3214
  return { ok: false, reason: "duplicate", detail: input.name };
2837
3215
  }
2838
3216
  const indent = indentOf(raw);
@@ -2844,7 +3222,8 @@ ${indent}"servers": {
2844
3222
  ${entry}
2845
3223
  ${indent}}${landing.hasMembers ? "," : "\n"}`;
2846
3224
  const at = landing.braceAt + 1;
2847
- return { ok: true, text: raw.slice(0, at) + block + raw.slice(at) };
3225
+ const eol = eolOf(raw);
3226
+ return { ok: true, text: raw.slice(0, at) + withEol(block, eol) + raw.slice(at) };
2848
3227
  }
2849
3228
  function writeMcpServer(input, opts = {}) {
2850
3229
  if (!isWritableName(input.name)) {
@@ -2853,17 +3232,8 @@ function writeMcpServer(input, opts = {}) {
2853
3232
  for (const name of opts.taken ?? []) {
2854
3233
  if (name === input.name) return { ok: false, reason: "duplicate", detail: input.name };
2855
3234
  }
2856
- const probe = parseMcpConfig(
2857
- JSON.stringify({ servers: { [input.name]: rawFields(input) } }),
2858
- "(add-form)"
2859
- );
2860
- if (probe.servers.length === 0) {
2861
- return {
2862
- ok: false,
2863
- reason: "invalid-config",
2864
- detail: probe.issues.map((i) => i.message).join("; ") || t("mcp_write.invalid")
2865
- };
2866
- }
3235
+ const invalid = validateAddInput(input);
3236
+ if (invalid !== null) return { ok: false, reason: "invalid-config", detail: invalid };
2867
3237
  const configPath2 = mcpConfigPath(opts.homeDir);
2868
3238
  let raw;
2869
3239
  try {
@@ -2881,6 +3251,14 @@ function writeMcpServer(input, opts = {}) {
2881
3251
  }
2882
3252
  return { ok: true, configPath: configPath2 };
2883
3253
  }
3254
+ function validateAddInput(input, lang) {
3255
+ const probe = parseMcpConfig(
3256
+ JSON.stringify({ servers: { [input.name]: rawFields(input) } }),
3257
+ "(add-form)"
3258
+ );
3259
+ if (probe.servers.length > 0) return null;
3260
+ return probe.issues.map((i) => i.message).join("; ") || t("mcp_write.invalid", {}, lang);
3261
+ }
2884
3262
  function rawFields(input) {
2885
3263
  return {
2886
3264
  transport: input.transport,
@@ -2907,9 +3285,172 @@ function toServerConfig(input) {
2907
3285
  var EMPTY_CONFIG;
2908
3286
  var init_mcp_config_write = __esm({
2909
3287
  "src/mcp-config-write.ts"() {
3288
+ init_mcp_json_scan();
2910
3289
  EMPTY_CONFIG = '{\n "servers": {}\n}\n';
2911
3290
  }
2912
3291
  });
3292
+ function skipSpaces(raw, from) {
3293
+ let i = from;
3294
+ while (i < raw.length && /[ \t\r\n]/.test(raw[i])) i++;
3295
+ return i;
3296
+ }
3297
+ function lineStart(raw, at) {
3298
+ return raw.lastIndexOf("\n", at - 1) + 1;
3299
+ }
3300
+ function lineEnd(raw, at) {
3301
+ const nl = raw.indexOf("\n", at);
3302
+ return nl === -1 ? raw.length : nl;
3303
+ }
3304
+ function padBefore(raw, at) {
3305
+ const ls = lineStart(raw, at);
3306
+ const head = raw.slice(ls, at);
3307
+ return /^[ \t]*$/.test(head) ? head : "";
3308
+ }
3309
+ function findEntry(raw, name) {
3310
+ const scan = locate(raw);
3311
+ if (!scan.ok) return { detail: scan.detail };
3312
+ const entries = scan.landing.entries;
3313
+ const index = entries.findIndex((e) => e.key === name);
3314
+ return index === -1 ? null : { entries, index };
3315
+ }
3316
+ function replaceServerText(raw, input) {
3317
+ const found = findEntry(raw, input.name);
3318
+ if (found !== null && "detail" in found) {
3319
+ return { ok: false, reason: "unwritable", detail: found.detail };
3320
+ }
3321
+ if (found === null) return { ok: false, reason: "invalid-name", detail: input.name };
3322
+ const target = found.entries[found.index];
3323
+ const pad = padBefore(raw, target.keyAt);
3324
+ const entry = renderEntry(input, indentOf(raw), pad);
3325
+ const block = withEol(entry.slice(pad.length), eolOf(raw));
3326
+ const text = raw.slice(0, target.keyAt) + block + raw.slice(target.valueEnd);
3327
+ return { ok: true, text };
3328
+ }
3329
+ function removeCuts(raw, entries, index) {
3330
+ const self = entries[index];
3331
+ const afterValue = skipTrivia(raw, self.valueEnd);
3332
+ if (raw[afterValue] === ",") {
3333
+ return skipSpaces(raw, self.valueEnd) === afterValue ? [tidyLine(raw, self.keyAt, afterValue + 1)] : [
3334
+ [self.keyAt, self.valueEnd],
3335
+ [afterValue, afterValue + 1]
3336
+ ];
3337
+ }
3338
+ const prev = index > 0 ? entries[index - 1] : void 0;
3339
+ const prevComma = prev === void 0 ? -1 : skipTrivia(raw, prev.valueEnd);
3340
+ if (prevComma < 0 || raw[prevComma] !== ",") {
3341
+ return [tidyLine(raw, self.keyAt, self.valueEnd)];
3342
+ }
3343
+ return [
3344
+ [prevComma, prevComma + 1],
3345
+ [lineStartIfBlank(raw, self.keyAt), self.valueEnd]
3346
+ ];
3347
+ }
3348
+ function tidyLine(raw, keyAt, to) {
3349
+ const ls = lineStart(raw, keyAt);
3350
+ const le = lineEnd(raw, to);
3351
+ const headBlank = /^[ \t]*$/.test(raw.slice(ls, keyAt));
3352
+ const tailBlank = /^[ \t\r]*$/.test(raw.slice(to, le));
3353
+ if (!headBlank || !tailBlank) return [keyAt, to];
3354
+ return [ls, Math.min(le + 1, raw.length)];
3355
+ }
3356
+ function lineStartIfBlank(raw, at) {
3357
+ const ls = lineStart(raw, at);
3358
+ return /^[ \t]*$/.test(raw.slice(ls, at)) ? ls : at;
3359
+ }
3360
+ function removeServerText(raw, name) {
3361
+ const found = findEntry(raw, name);
3362
+ if (found !== null && "detail" in found) {
3363
+ return { ok: false, reason: "unwritable", detail: found.detail };
3364
+ }
3365
+ if (found === null) return { ok: false, reason: "invalid-name", detail: name };
3366
+ const cuts = [...removeCuts(raw, found.entries, found.index)].sort((a, b) => b[0] - a[0]);
3367
+ let text = raw;
3368
+ for (const [from, to] of cuts) text = text.slice(0, from) + text.slice(to);
3369
+ return { ok: true, text };
3370
+ }
3371
+ function commit(splice, expectKeys, opts) {
3372
+ const configPath2 = mcpConfigPath(opts.homeDir);
3373
+ if (!existsSync(configPath2)) return { ok: false, reason: "not-found", detail: configPath2 };
3374
+ let raw;
3375
+ try {
3376
+ raw = readFileSync(configPath2, "utf-8");
3377
+ } catch (err) {
3378
+ return { ok: false, reason: "unwritable", detail: describe5(err) };
3379
+ }
3380
+ const before = locate(raw);
3381
+ if (!before.ok) return { ok: false, reason: "unwritable", detail: before.detail };
3382
+ const spliced = splice(raw);
3383
+ if (!spliced.ok) {
3384
+ return spliced.reason === "invalid-name" ? { ok: false, reason: "not-found", detail: spliced.detail } : spliced;
3385
+ }
3386
+ const bad = checkSpliced(
3387
+ spliced.text,
3388
+ expectKeys(before.landing.entries.map((e) => e.key)),
3389
+ opts.lang
3390
+ );
3391
+ if (bad !== null) return { ok: false, reason: "unwritable", detail: bad };
3392
+ return write(configPath2, raw, spliced.text);
3393
+ }
3394
+ function checkSpliced(text, expect, lang) {
3395
+ const after = locate(text);
3396
+ if (!after.ok) return after.detail;
3397
+ const got = after.landing.entries.map((e) => e.key);
3398
+ if (got.length !== expect.length || got.some((k, i) => k !== expect[i])) {
3399
+ return `${t("mcp_write.invalid", {}, lang)} (servers: [${got.join(", ")}] != [${expect.join(", ")}])`;
3400
+ }
3401
+ return null;
3402
+ }
3403
+ function write(configPath2, expectDisk, body) {
3404
+ let disk;
3405
+ try {
3406
+ disk = readFileSync(configPath2, "utf-8");
3407
+ } catch (err) {
3408
+ return { ok: false, reason: "unwritable", detail: describe5(err) };
3409
+ }
3410
+ if (disk !== expectDisk) {
3411
+ return { ok: false, reason: "unwritable", detail: t("mcp_config.stale_disk") };
3412
+ }
3413
+ const tmp = `${configPath2}.${process.pid}.tmp`;
3414
+ try {
3415
+ mkdirSync(dirname(configPath2), { recursive: true });
3416
+ const mode = statSync(configPath2).mode & 511;
3417
+ try {
3418
+ writeFileSync(tmp, body, { encoding: "utf-8", mode });
3419
+ renameSync(tmp, configPath2);
3420
+ } catch (err) {
3421
+ rmSync(tmp, { force: true });
3422
+ throw err;
3423
+ }
3424
+ } catch (err) {
3425
+ return { ok: false, reason: "unwritable", detail: describe5(err) };
3426
+ }
3427
+ return { ok: true, configPath: configPath2 };
3428
+ }
3429
+ function updateMcpServer(input, opts = {}) {
3430
+ const invalid = validateAddInput(input, opts.lang);
3431
+ if (invalid !== null) return { ok: false, reason: "invalid-config", detail: invalid };
3432
+ return commit(
3433
+ (raw) => replaceServerText(raw, input),
3434
+ (before) => before,
3435
+ opts
3436
+ );
3437
+ }
3438
+ function removeMcpServer(name, opts = {}) {
3439
+ return commit(
3440
+ (raw) => removeServerText(raw, name),
3441
+ (before) => before.filter((k) => k !== name),
3442
+ opts
3443
+ );
3444
+ }
3445
+ function describe5(err) {
3446
+ return err instanceof Error ? err.message : String(err);
3447
+ }
3448
+ var init_mcp_config_edit = __esm({
3449
+ "src/mcp-config-edit.ts"() {
3450
+ init_mcp_config_write();
3451
+ init_mcp_json_scan();
3452
+ }
3453
+ });
2913
3454
  function foldToolMode(mode) {
2914
3455
  if (mode === "both") return "both";
2915
3456
  if (mode === "code") return "code";
@@ -2948,6 +3489,12 @@ function registerLocalTools(input) {
2948
3489
  const rg = resolveRipgrep();
2949
3490
  if (rg.mode === "missing") {
2950
3491
  diags.warn("Ripgrep", t("boot.ripgrep_missing", { hint: rg.hint ?? "" }), "ripgrep-missing");
3492
+ } else if (rg.mode === "not-executable") {
3493
+ diags.warn(
3494
+ "Ripgrep",
3495
+ t("boot.ripgrep_not_executable", { hint: rg.hint ?? "" }),
3496
+ "ripgrep-not-executable"
3497
+ );
2951
3498
  } else if (rg.mode === "system") {
2952
3499
  diags.ok("Ripgrep", t("boot.ripgrep_system"));
2953
3500
  } else {
@@ -2976,6 +3523,7 @@ function registerLocalTools(input) {
2976
3523
  registry.registerTool(createRunCodeTool());
2977
3524
  }
2978
3525
  if (skills) registry.registerTool(createSkillViewTool({ skills }));
3526
+ if (skills) registry.registerTools(createSkillWriteTools({ skills }));
2979
3527
  if (input.deferMcp === true) {
2980
3528
  registry.registerTool(
2981
3529
  createToolSearchTool({
@@ -3063,7 +3611,7 @@ function createToolSync(mcp, registry, defer) {
3063
3611
  registry.unregisterBySource(`mcp:${server}`);
3064
3612
  registerMcpBatch(
3065
3613
  registry,
3066
- fresh.filter((t23) => t23.name.startsWith(prefix)),
3614
+ fresh.filter((t25) => t25.name.startsWith(prefix)),
3067
3615
  server,
3068
3616
  defer
3069
3617
  );
@@ -3132,6 +3680,32 @@ async function addServer(mcp, sync, input, homeDir) {
3132
3680
  }
3133
3681
  };
3134
3682
  }
3683
+ async function updateServer(mcp, sync, input, homeDir) {
3684
+ const written = updateMcpServer(input, { homeDir });
3685
+ if (!written.ok) return written;
3686
+ await mcp.disconnect(input.name);
3687
+ const config = toServerConfig(input);
3688
+ const [status] = await mcp.connectAll([config], "user");
3689
+ await sync.replace(input.name);
3690
+ return {
3691
+ ok: true,
3692
+ configPath: written.configPath,
3693
+ status: status ?? mcp.getStatus(input.name) ?? {
3694
+ name: input.name,
3695
+ connected: false,
3696
+ toolCount: 0,
3697
+ reconnectAttempts: 0,
3698
+ source: "user"
3699
+ }
3700
+ };
3701
+ }
3702
+ async function removeServer(mcp, sync, name, homeDir) {
3703
+ const written = removeMcpServer(name, { homeDir });
3704
+ if (!written.ok) return written;
3705
+ await mcp.disconnect(name);
3706
+ await sync.replace(name);
3707
+ return { ok: true, configPath: written.configPath };
3708
+ }
3135
3709
  async function connectAllServers(mcp, batches, budgetMs = MCP_CONNECT_BUDGET_MS) {
3136
3710
  const all = await withTimeout(
3137
3711
  Promise.all(batches.map((b) => mcp.connectAll([...b.servers], b.source))),
@@ -3179,6 +3753,12 @@ async function registerMcpTools(registry, homeDir, diags, injected = [], defer =
3179
3753
  if (outcome.ok) wireToolRefresh(mcp, registry, sync);
3180
3754
  return outcome;
3181
3755
  },
3756
+ update: async (input) => {
3757
+ const outcome = await updateServer(mcp, sync, input, homeDir);
3758
+ if (outcome.ok) wireToolRefresh(mcp, registry, sync);
3759
+ return outcome;
3760
+ },
3761
+ remove: (name) => removeServer(mcp, sync, name, homeDir),
3182
3762
  readConfig: () => readMcpConfigFile({ homeDir }),
3183
3763
  writeConfig: (text, revision, lang) => writeMcpConfigFile(text, {
3184
3764
  revision,
@@ -3228,7 +3808,7 @@ async function registerMcpTools(registry, homeDir, diags, injected = [], defer =
3228
3808
  );
3229
3809
  continue;
3230
3810
  }
3231
- const own = mcpTools.filter((t23) => t23.name.startsWith(`mcp__${st.name}__`));
3811
+ const own = mcpTools.filter((t25) => t25.name.startsWith(`mcp__${st.name}__`));
3232
3812
  const registered = dropOverlongToolNames(
3233
3813
  own,
3234
3814
  `MCP ${st.name}`,
@@ -3257,6 +3837,7 @@ var init_tools = __esm({
3257
3837
  "src/tools.ts"() {
3258
3838
  init_host_capabilities();
3259
3839
  init_mcp_config_apply();
3840
+ init_mcp_config_edit();
3260
3841
  init_mcp_config_file();
3261
3842
  init_mcp_config_write();
3262
3843
  MCP_CONNECT_BUDGET_MS = 15e3;
@@ -3443,6 +4024,7 @@ var init_session_factory = __esm({
3443
4024
  contextLength: services.contextLength,
3444
4025
  artifactRetention: resolveArtifactRetention(config.artifacts),
3445
4026
  sandboxTerminal: config.sandbox?.terminal !== false,
4027
+ compliance: this.opts.compliance ?? void 0,
3446
4028
  ...role ? { role: () => ({ name: role.name, ...role.prompt ? { prompt: role.prompt } : {} }) } : {},
3447
4029
  ...permission ? { permissionManager: permission } : {},
3448
4030
  ...services.hookManager ? { hookManager: services.hookManager } : {},
@@ -3524,6 +4106,7 @@ ${context}` : goal,
3524
4106
  multimodal: { homeDir: config.homeDir },
3525
4107
  artifactRetention: resolveArtifactRetention(config.artifacts),
3526
4108
  sandboxTerminal: config.sandbox?.terminal !== false,
4109
+ compliance: opts.compliance ?? void 0,
3527
4110
  maxTurns: active?.maxTurns ?? config.maxTurns,
3528
4111
  role: () => {
3529
4112
  const now = roleScope.current;
@@ -3636,6 +4219,9 @@ function toProvider(router) {
3636
4219
  } catch {
3637
4220
  return { supportsImages: false };
3638
4221
  }
4222
+ },
4223
+ get modelId() {
4224
+ return router.getSelection().model;
3639
4225
  }
3640
4226
  };
3641
4227
  }
@@ -4018,20 +4604,96 @@ var init_utility = __esm({
4018
4604
  LABEL_SHAPE = /^[a-z0-9_-]{1,32}$/;
4019
4605
  }
4020
4606
  });
4607
+ function isSystemOpenable(path) {
4608
+ const dot = path.lastIndexOf(".");
4609
+ if (dot < 0) return false;
4610
+ return SYSTEM_OPENABLE_EXTENSIONS.has(path.slice(dot).toLowerCase());
4611
+ }
4612
+ function toWorkspaceRelative(root, userPath) {
4613
+ const trimmed = userPath.trim();
4614
+ if (trimmed === "") return null;
4615
+ if (!isAbsolute(trimmed)) return trimmed.replace(/^\.\/+/, "") || null;
4616
+ const rel = relative(resolve(root), resolve(trimmed));
4617
+ if (rel === "" || rel.startsWith("..")) return null;
4618
+ return rel.split(sep).join("/");
4619
+ }
4620
+ function insideRoot(root, abs) {
4621
+ let realRoot;
4622
+ try {
4623
+ realRoot = realpathSync(root);
4624
+ } catch {
4625
+ return false;
4626
+ }
4627
+ const rel = relative(realRoot, abs);
4628
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
4629
+ }
4630
+ function sniffKind(absPath) {
4631
+ let fd;
4632
+ try {
4633
+ fd = openSync(absPath, "r");
4634
+ const buf = Buffer.alloc(8192);
4635
+ const read = readSync(fd, buf, 0, buf.length, 0);
4636
+ return buf.subarray(0, read).includes(0) ? "binary" : "text";
4637
+ } catch {
4638
+ return null;
4639
+ } finally {
4640
+ if (fd !== void 0) {
4641
+ try {
4642
+ closeSync(fd);
4643
+ } catch {
4644
+ }
4645
+ }
4646
+ }
4647
+ }
4021
4648
  function withinWorkspace(resolveInWorkspace2, root, rel) {
4022
4649
  return resolveInWorkspace2(root, rel) !== null;
4023
4650
  }
4024
4651
  function buildWorkspaceFilesControl(deps, permission) {
4025
4652
  let cache = null;
4026
4653
  const now = () => Date.now();
4654
+ function statFileImpl(path, root) {
4655
+ const rel = toWorkspaceRelative(root, path);
4656
+ if (rel === null) return { ok: false, reason: "denied" };
4657
+ const full = deps.resolveInWorkspace(root, rel);
4658
+ if (!full) return { ok: false, reason: "denied" };
4659
+ const decision = permission?.check({
4660
+ type: "file_read",
4661
+ detail: rel,
4662
+ workDir: root,
4663
+ target: full,
4664
+ toolName: "file_read"
4665
+ });
4666
+ if (permission && !decision?.allowed) return { ok: false, reason: "denied" };
4667
+ let stat;
4668
+ try {
4669
+ stat = statSync(full);
4670
+ } catch (err) {
4671
+ const code = err.code;
4672
+ return {
4673
+ ok: false,
4674
+ reason: code === "ENOENT" || code === "ENOTDIR" ? "missing" : "unreadable"
4675
+ };
4676
+ }
4677
+ if (!stat.isFile()) return { ok: false, reason: "not-a-file" };
4678
+ let absPath;
4679
+ try {
4680
+ absPath = realpathSync(full);
4681
+ } catch {
4682
+ return { ok: false, reason: "unreadable" };
4683
+ }
4684
+ if (!insideRoot(root, absPath)) return { ok: false, reason: "denied" };
4685
+ const kind = sniffKind(absPath);
4686
+ if (kind === null) return { ok: false, reason: "unreadable" };
4687
+ return { ok: true, rel, absPath, bytes: stat.size, mtimeMs: stat.mtimeMs, kind };
4688
+ }
4027
4689
  function computeList(root) {
4028
4690
  const { files, truncated } = deps.listFiles(root);
4029
4691
  return { files, truncated, at: now(), root };
4030
4692
  }
4031
4693
  function listFor(root) {
4032
- const t23 = now();
4694
+ const t25 = now();
4033
4695
  if (!cache || cache.root !== root) cache = computeList(root);
4034
- else if (t23 - cache.at > LIST_TTL_MS) cache = computeList(root);
4696
+ else if (t25 - cache.at > LIST_TTL_MS) cache = computeList(root);
4035
4697
  return cache;
4036
4698
  }
4037
4699
  return {
@@ -4090,12 +4752,55 @@ function buildWorkspaceFilesControl(deps, permission) {
4090
4752
  return { ok: false, reason: "binary", bytes: stat.size };
4091
4753
  }
4092
4754
  return { ok: true, text: raw.toString("utf8"), bytes: stat.size };
4755
+ },
4756
+ statFile: statFileImpl,
4757
+ planSystemOpen(path, root) {
4758
+ const stat = statFileImpl(path, root);
4759
+ if (!stat.ok) return { ok: false, reason: stat.reason };
4760
+ if (!isSystemOpenable(stat.absPath)) return { ok: false, reason: "extension-not-allowed" };
4761
+ const { command, args } = deps.systemOpenArgv(stat.absPath);
4762
+ return { ok: true, command, args, absPath: stat.absPath };
4093
4763
  }
4094
4764
  };
4095
4765
  }
4096
- var LIST_TTL_MS;
4766
+ var SYSTEM_OPENABLE_EXTENSIONS, LIST_TTL_MS;
4097
4767
  var init_workspace_files = __esm({
4098
4768
  "src/workspace-files.ts"() {
4769
+ SYSTEM_OPENABLE_EXTENSIONS = /* @__PURE__ */ new Set([
4770
+ ".doc",
4771
+ ".docx",
4772
+ ".xls",
4773
+ ".xlsx",
4774
+ ".ppt",
4775
+ ".pptx",
4776
+ ".rtf",
4777
+ ".odt",
4778
+ ".ods",
4779
+ ".odp",
4780
+ ".pages",
4781
+ ".numbers",
4782
+ ".key",
4783
+ ".pdf",
4784
+ ".tif",
4785
+ ".tiff",
4786
+ ".heic",
4787
+ ".heif",
4788
+ ".psd",
4789
+ ".sketch",
4790
+ ".fig",
4791
+ ".mp4",
4792
+ ".mov",
4793
+ ".m4v",
4794
+ ".mkv",
4795
+ ".avi",
4796
+ ".webm",
4797
+ ".mp3",
4798
+ ".wav",
4799
+ ".m4a",
4800
+ ".flac",
4801
+ ".aac",
4802
+ ".ogg"
4803
+ ]);
4099
4804
  LIST_TTL_MS = 6e4;
4100
4805
  }
4101
4806
  });
@@ -4414,6 +5119,9 @@ async function assembleRuntime(opts, opened, processes) {
4414
5119
  config.tools?.deferMcp === true
4415
5120
  );
4416
5121
  cleanups.push(mcpWiring.dispose);
5122
+ registry.registerTools(
5123
+ createMcpTools({ control: mcpWiring.control, servers: () => mcpWiring.status() })
5124
+ );
4417
5125
  const bridges = new SessionBridges(opts.onApprovalRequest);
4418
5126
  const interactive = !services.permission?.isNonInteractive();
4419
5127
  const requestPlanApproval = (req, sessionId2) => bridges.approve(sessionId2, req);
@@ -4423,13 +5131,14 @@ async function assembleRuntime(opts, opened, processes) {
4423
5131
  requestPlanApproval,
4424
5132
  interactive
4425
5133
  );
5134
+ const defaultLevel = buildDefaultLevelSlot(services);
4426
5135
  const guards = buildSessionGuards({
4427
5136
  permission: services.permission,
4428
5137
  planState,
4429
5138
  managed: services.managedPolicy,
4430
5139
  deferredTools: () => registry.byExposure(ToolExposure.Deferred),
4431
5140
  bootstrapReveal,
4432
- startLevel: services.permissionLevel,
5141
+ defaultLevel,
4433
5142
  requestApproval: requestPlanApproval,
4434
5143
  interactive
4435
5144
  });
@@ -4503,6 +5212,7 @@ async function assembleRuntime(opts, opened, processes) {
4503
5212
  deferredTools: () => registry.byExposure(ToolExposure.Deferred),
4504
5213
  codeModeTools: () => registry.codeModeDeclared(),
4505
5214
  telemetry,
5215
+ compliance: services.compliance,
4506
5216
  budget: budgetWiring,
4507
5217
  ...opts.onApprovalRequest ? { hostApproval: opts.onApprovalRequest } : {},
4508
5218
  workspaces,
@@ -4604,7 +5314,7 @@ async function assembleRuntime(opts, opened, processes) {
4604
5314
  sessionStore: store ?? null,
4605
5315
  sessionReferences: services.session?.references ?? null,
4606
5316
  workspaceFiles: buildWorkspaceFilesControl(
4607
- { listFiles: listWorkspaceFiles, resolveInWorkspace },
5317
+ { listFiles: listWorkspaceFiles, resolveInWorkspace, systemOpenArgv },
4608
5318
  services.permission
4609
5319
  ),
4610
5320
  checkpoints: bootstrap?.checkpoints ?? buildCheckpointControl(checkpoints, null, () => sessionId),
@@ -4631,6 +5341,10 @@ async function assembleRuntime(opts, opened, processes) {
4631
5341
  }));
4632
5342
  },
4633
5343
  usageScope: budgetWiring.budgetStore ? "session" : "run",
5344
+ sessionUsage: (sessionId2) => {
5345
+ const saved = budgetWiring.budgetStore?.load(sessionId2);
5346
+ return saved ? persistedUsage(saved) : void 0;
5347
+ },
4634
5348
  compression: compressionLine(config, services),
4635
5349
  get providerInfo() {
4636
5350
  return provider ? { provider: provider.getProviderType(), apiKeyHint: provider.getApiKeyHint() } : null;
@@ -4657,7 +5371,7 @@ async function assembleRuntime(opts, opened, processes) {
4657
5371
  skillIndexResidency: (role) => services.skill?.indexResidency({
4658
5372
  only: role === null ? void 0 : findRole(agentRoles, role)?.skills
4659
5373
  }) ?? /* @__PURE__ */ new Map(),
4660
- skillImport: buildSkillControl(services),
5374
+ skillWrite: buildSkillControl(services),
4661
5375
  roleWrite: createRoleWriteControl({
4662
5376
  userDir: agentsDir(config.homeDir),
4663
5377
  roles: () => agentRoles,
@@ -4945,6 +5659,34 @@ function buildSkillControl(services) {
4945
5659
  const outcome = await importSkills(source, { ...opts, token });
4946
5660
  if (outcome.ok && outcome.imported.length > 0) services.skill?.invalidateCache();
4947
5661
  return outcome;
5662
+ },
5663
+ remove: async (name, lang) => {
5664
+ const skill = services.skill;
5665
+ if (!skill) {
5666
+ return {
5667
+ ok: false,
5668
+ reason: "missing",
5669
+ detail: t("skill.remove_no_system", void 0, lang)
5670
+ };
5671
+ }
5672
+ const found = skill.list().find((s) => s.name === name);
5673
+ if (!found) {
5674
+ return {
5675
+ ok: false,
5676
+ reason: "not-found",
5677
+ detail: t("skill.remove_unknown", { name }, lang)
5678
+ };
5679
+ }
5680
+ try {
5681
+ const path = await skill.delete(name);
5682
+ return { ok: true, name, path };
5683
+ } catch (err) {
5684
+ return {
5685
+ ok: false,
5686
+ reason: found.scope === "user" ? "failed" : "readonly",
5687
+ detail: err instanceof Error ? err.message : String(err)
5688
+ };
5689
+ }
4948
5690
  }
4949
5691
  };
4950
5692
  }
@@ -5080,7 +5822,7 @@ function buildPermissionsControl(services, bootstrap, tools) {
5080
5822
  bypassDisabled: p.disableBypassPermissionsMode
5081
5823
  };
5082
5824
  },
5083
- setLevel: (level) => bootstrap?.setLevel(level) ?? { ok: true },
5825
+ setLevel: (level, opts) => bootstrap?.setLevel(level, opts) ?? { ok: true },
5084
5826
  gates: (level) => {
5085
5827
  const rules = compileRuleLists(services.config.permissions);
5086
5828
  const isolation = services.permission?.getSandboxIsolation() ?? "process";
@@ -5125,15 +5867,20 @@ function buildSettingsControl(services, workspaces, opts) {
5125
5867
  row.scalar ? { key: row.scalar, layers: scope.layers } : void 0
5126
5868
  );
5127
5869
  const overridable = row.scalar !== void 0;
5870
+ const writes = scope.paths === null ? [] : settingWriteTargets({
5871
+ key: row.key,
5872
+ winner: chain.layer,
5873
+ paths: scope.paths,
5874
+ trusted: scope.trusted
5875
+ });
5876
+ const kind = writes.length === 0 ? null : settingValueKind(row.key);
5877
+ const choices = writes.length === 0 ? null : settingValueChoices(row.key);
5128
5878
  return {
5129
5879
  ...chain,
5130
5880
  overridable,
5131
- writes: scope.paths === null ? [] : settingWriteTargets({
5132
- winner: chain.layer,
5133
- overridable,
5134
- paths: scope.paths,
5135
- trusted: scope.trusted
5136
- })
5881
+ writes,
5882
+ ...kind === null ? {} : { valueKind: kind },
5883
+ ...choices === null ? {} : { choices }
5137
5884
  };
5138
5885
  });
5139
5886
  };
@@ -5152,7 +5899,7 @@ function buildSettingsControl(services, workspaces, opts) {
5152
5899
  value: input.value
5153
5900
  });
5154
5901
  if (!outcome.ok) return { ok: false, reason: outcome.reason, path };
5155
- const target = rows(input.sessionId).find((row) => row.key === input.key)?.writes.find((write) => write.layer === input.layer);
5902
+ const target = rows(input.sessionId).find((row) => row.key === input.key)?.writes.find((write2) => write2.layer === input.layer);
5156
5903
  return {
5157
5904
  ok: true,
5158
5905
  key: input.key,
@@ -5203,6 +5950,31 @@ function writePathsOf(layers) {
5203
5950
  }
5204
5951
  return paths;
5205
5952
  }
5953
+ function buildDefaultLevelSlot(services) {
5954
+ let current = services.permissionLevel;
5955
+ return {
5956
+ get: () => current,
5957
+ remember: (level) => {
5958
+ current = level;
5959
+ const paths = writePathsOf(services.settingsLayers);
5960
+ if (!paths) return { reason: "no-user-config" };
5961
+ const outcome = writeSettingValue({
5962
+ layer: "user",
5963
+ path: paths.user,
5964
+ key: "permission",
5965
+ value: level
5966
+ });
5967
+ if (!outcome.ok) {
5968
+ const reason2 = outcome.reason === "unwritable-file" ? "unwritable" : "io";
5969
+ return { reason: reason2, path: paths.user };
5970
+ }
5971
+ if (originOf(services.configOrigins, "permission") === "env") {
5972
+ return { reason: "env-pinned", path: paths.user };
5973
+ }
5974
+ return void 0;
5975
+ }
5976
+ };
5977
+ }
5206
5978
  function valueAt(config, key) {
5207
5979
  let cursor = config;
5208
5980
  for (const segment of key.split(".")) {
@@ -5236,6 +6008,7 @@ var init_build = __esm({
5236
6008
  init_host_capabilities();
5237
6009
  init_host_marketplaces();
5238
6010
  init_host_preset();
6011
+ init_mcp_tools();
5239
6012
  init_model_catalog();
5240
6013
  init_plugin_control();
5241
6014
  init_plugin_mcp_servers();
@@ -5339,7 +6112,7 @@ async function probeMcpServer(name, opts = {}) {
5339
6112
  const tools = status?.connected ? await registry.getAllTools() : [];
5340
6113
  return {
5341
6114
  connected: status?.connected ?? false,
5342
- toolNames: tools.map((t23) => t23.name),
6115
+ toolNames: tools.map((t25) => t25.name),
5343
6116
  ...status?.lastError ? { error: status.lastError } : {},
5344
6117
  needsLogin: status?.needsLogin ?? false
5345
6118
  };
@@ -5360,5 +6133,7 @@ async function requireServer(name, opts) {
5360
6133
  // src/index.ts
5361
6134
  init_mcp_config_file();
5362
6135
  init_mcp_config_apply();
6136
+ init_mcp_config_edit();
6137
+ init_mcp_tools();
5363
6138
 
5364
- export { AgentRoleError, AgentSession, ApprovalRelay, HOST_CAPABILITIES_MODULE, HOST_MARKETPLACE_MODULE, HOST_PRESET_MODULE, QuestionRelay, SCHEDULE_EXIT_CODES, SessionStore, SessionWorkspaces, applyHostPreset, applyMcpConfig, buildModelCatalogControl, buildRuntime, buildServices, buildWorkspaceFilesControl, collectFileChanges, createScheduleControl, createUserActions, ensureSecretsReady, exitCodeFor, fireSchedule, listMcpServers, mcpConfigRevision, mcpLogin, mcpLogout, probeMcpServer, providerLabel, readMcpConfigFile, registerAskQuestion, registerLocalTools, registerMcpTools, serializeStream, splitDirList, toSerializable, utilityBudgetKey, wireCommands, wireHostCapabilities, wirePluginMcpServers, wireWorkspace, withModelScope, withRoleScope, withToolScope, writeMcpConfigFile, writeMcpServer };
6139
+ export { AgentRoleError, AgentSession, ApprovalRelay, HOST_CAPABILITIES_MODULE, HOST_MARKETPLACE_MODULE, HOST_PRESET_MODULE, MCP_ADD_TOOL, MCP_LIST_TOOL, MCP_REMOVE_TOOL, MCP_UPDATE_TOOL, QuestionRelay, SCHEDULE_EXIT_CODES, SessionStore, SessionWorkspaces, applyHostPreset, applyMcpConfig, buildModelCatalogControl, buildRuntime, buildServices, buildWorkspaceFilesControl, collectFileChanges, createMcpTools, createScheduleControl, createUserActions, ensureSecretsReady, exitCodeFor, fireSchedule, isSystemOpenable, listMcpServers, mcpConfigRevision, mcpLogin, mcpLogout, probeMcpServer, providerLabel, readMcpConfigFile, registerAskQuestion, registerLocalTools, registerMcpTools, removeMcpServer, removeServerText, replaceServerText, serializeStream, splitDirList, toSerializable, updateMcpServer, utilityBudgetKey, wireCommands, wireHostCapabilities, wirePluginMcpServers, wireWorkspace, withModelScope, withRoleScope, withToolScope, writeMcpConfigFile, writeMcpServer };