praxis-agent 0.40.0 → 0.42.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 CHANGED
@@ -170,7 +170,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
170
170
  exclusions, violation reporting, and bare-repository control-file cleanup,
171
171
  safe-property Skill auto-allow, interactive
172
172
  workspace-directory add/remove controls, path confinement, credential
173
- redaction, and sanitized child processes.
173
+ redaction, sanitized child processes, and exact-fingerprint workspace trust
174
+ that blocks automatically discovered project/local hooks and MCP servers
175
+ until the canonical workspace configuration is explicitly accepted.
174
176
  - **Durable local work** — resumable sessions, full-history forks, file
175
177
  checkpoints, tasks, foreground/background subagents, top-level agents, and
176
178
  Claude-compatible main-thread agent definitions with native prompt, model,
@@ -198,7 +200,8 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
198
200
  imports, memory, skills, commands, agents, hooks, settings, MCP servers,
199
201
  plugins, and append-only `praxis.transcript` JSONL sessions under `~/.praxis`.
200
202
  - **Provider-neutral models** — native Anthropic Messages and OpenAI-compatible
201
- streaming adapters with explicit capability checks and metering controls.
203
+ streaming adapters with explicit capability checks, metering controls, and
204
+ a bounded absolute deadline for every provider attempt.
202
205
 
203
206
  Detailed feature status and executable evidence live in the
204
207
  [parity matrix](https://github.com/Forest-Isle/Praxis/blob/main/docs/PARITY_MATRIX.md),
@@ -7,6 +7,7 @@ export const DEFAULT_CLI_CONTROLS = {
7
7
  settingSources: undefined,
8
8
  safeMode: false,
9
9
  bare: false,
10
+ trustProject: false,
10
11
  systemPrompt: undefined,
11
12
  systemPromptFile: undefined,
12
13
  appendSystemPrompt: undefined,
@@ -180,6 +181,7 @@ export async function resolveCliControls(controls, cwd) {
180
181
  settingSources: controls.settingSources,
181
182
  safeMode: controls.safeMode,
182
183
  bare: controls.bare,
184
+ trustProject: controls.trustProject,
183
185
  systemPrompt,
184
186
  appendSystemPrompt,
185
187
  excludeDynamicSystemPromptSections: controls.excludeDynamicSystemPromptSections,
@@ -14,6 +14,7 @@ export interface CliControls {
14
14
  settingSources: readonly ('user' | 'project' | 'local')[] | undefined;
15
15
  safeMode: boolean;
16
16
  bare: boolean;
17
+ trustProject: boolean;
17
18
  systemPrompt: string | undefined;
18
19
  systemPromptFile: string | undefined;
19
20
  appendSystemPrompt: string | undefined;
@@ -359,6 +359,7 @@ export function parseCliInvocation(argv) {
359
359
  let autocompact;
360
360
  let safeMode = false;
361
361
  let bare = false;
362
+ let trustProject = false;
362
363
  let systemPrompt;
363
364
  let systemPromptFile;
364
365
  let appendSystemPrompt;
@@ -1082,6 +1083,10 @@ export function parseCliInvocation(argv) {
1082
1083
  bare = true;
1083
1084
  continue;
1084
1085
  }
1086
+ if (value === '--trust-project') {
1087
+ trustProject = true;
1088
+ continue;
1089
+ }
1085
1090
  if (value === '--init') {
1086
1091
  init = true;
1087
1092
  continue;
@@ -1343,6 +1348,7 @@ export function parseCliInvocation(argv) {
1343
1348
  settingSources,
1344
1349
  safeMode,
1345
1350
  bare,
1351
+ trustProject,
1346
1352
  systemPrompt,
1347
1353
  systemPromptFile,
1348
1354
  appendSystemPrompt,
@@ -0,0 +1,10 @@
1
+ import { type WorkspaceTrustInventory } from '../security/workspace-trust.js';
2
+ export interface WorkspaceTrustPromptIO {
3
+ input?: AsyncIterable<string>;
4
+ output?: (text: string) => void;
5
+ signal?: AbortSignal;
6
+ }
7
+ export declare function createWorkspaceTrustDecisionCache(decide: (inventory: WorkspaceTrustInventory) => boolean | Promise<boolean>): (inventory: WorkspaceTrustInventory) => Promise<boolean>;
8
+ export declare function safeWorkspaceTrustDisplayField(value: string): string;
9
+ export declare function promptWorkspaceTrust(inventory: WorkspaceTrustInventory, io?: WorkspaceTrustPromptIO): Promise<boolean>;
10
+ //# sourceMappingURL=workspace-trust-prompt.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { workspaceTrustDecisionKey, } from '../security/workspace-trust.js';
2
+ export function createWorkspaceTrustDecisionCache(decide) {
3
+ const decisions = new Map();
4
+ return async (inventory) => {
5
+ const key = workspaceTrustDecisionKey(inventory);
6
+ const cached = decisions.get(key);
7
+ if (cached !== undefined)
8
+ return cached;
9
+ const decision = await decide(inventory);
10
+ decisions.set(key, decision);
11
+ return decision;
12
+ };
13
+ }
14
+ export function safeWorkspaceTrustDisplayField(value) {
15
+ return [...value]
16
+ .map((character) => {
17
+ return /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(character) ? '?' : character;
18
+ })
19
+ .join('')
20
+ .slice(0, 512);
21
+ }
22
+ export async function promptWorkspaceTrust(inventory, io = {}) {
23
+ const output = io.output ?? ((text) => process.stderr.write(text));
24
+ if (io.signal?.aborted)
25
+ return false;
26
+ output(`Workspace executable resources found for ${safeWorkspaceTrustDisplayField(inventory.canonicalPath)}\n`);
27
+ for (const origin of inventory.origins)
28
+ output(` ${origin.kind} (${origin.scope}) ${safeWorkspaceTrustDisplayField(origin.path)}: ${safeWorkspaceTrustDisplayField(origin.label)}\n`);
29
+ output('Trust these workspace executables? [y/N] ');
30
+ const input = io.input ?? process.stdin;
31
+ const iterator = input[Symbol.asyncIterator]();
32
+ const abortedResult = Symbol('workspace-trust-prompt-aborted');
33
+ let abort;
34
+ try {
35
+ const aborted = new Promise((resolve) => {
36
+ abort = () => resolve(abortedResult);
37
+ io.signal?.addEventListener('abort', abort, { once: true });
38
+ if (io.signal?.aborted)
39
+ abort();
40
+ });
41
+ const result = await Promise.race([iterator.next(), aborted]);
42
+ if (result === abortedResult)
43
+ return false;
44
+ if (result.done)
45
+ return false;
46
+ return /^(?:y|yes)$/iu.test(String(result.value).trim());
47
+ }
48
+ finally {
49
+ if (abort)
50
+ io.signal?.removeEventListener('abort', abort);
51
+ }
52
+ }
53
+ //# sourceMappingURL=workspace-trust-prompt.js.map
@@ -11,6 +11,7 @@ import type { TuiSlashCommand } from './cli/tui/slash-commands.js';
11
11
  import { type TuiHookConfiguration } from './cli/tui/hook-settings.js';
12
12
  import { type PraxisRuntimeSettings } from './cli/tui/runtime-settings.js';
13
13
  import { type ClaudePermissionMode } from './permissions/claude-permission-resolver.js';
14
+ import { type WorkspaceTrustInventory } from './security/workspace-trust.js';
14
15
  import { type ClaudeMcpServerStatus, type ClaudeMcpToolInspection } from './mcp/claude-mcp-tools.js';
15
16
  import { authenticateMcpServer } from './mcp/claude-mcp-oauth.js';
16
17
  import { servePraxisMcpStdio } from './mcp/praxis-mcp-server.js';
@@ -138,6 +139,7 @@ export interface CliDependencies extends InteractiveServiceFactory {
138
139
  onElicitation?: (request: CliElicitationRequest) => Promise<CliElicitationResult>;
139
140
  askUser?: ClaudeInteractiveToolCallbacks['askUser'];
140
141
  approvePlan?: ClaudeInteractiveToolCallbacks['approvePlan'];
142
+ approveWorkspaceTrust?: (request: WorkspaceTrustInventory) => boolean | Promise<boolean>;
141
143
  }): Promise<SessionCommands>;
142
144
  createAutoModeCritic?(options: {
143
145
  model?: string;
@@ -33,6 +33,8 @@ import { createClaudeModelAutoClassifier, defaultClaudeAutoModeConfig, loadClaud
33
33
  import { ClaudeExtensionPermissionResolver, ClaudeExtensionToolRegistry, } from './extensions/claude-extension-tools.js';
34
34
  import { ClaudeExtensionCatalog } from './extensions/claude-extensions.js';
35
35
  import { ClaudeHookRunner } from './hooks/claude-hooks.js';
36
+ import { allowedWorkspaceHookSettings, allowedWorkspaceMcpResources, assessWorkspaceTrust, persistWorkspaceTrust, workspaceTrustDecisionKey, workspaceTrustInventory, } from './security/workspace-trust.js';
37
+ import { createWorkspaceTrustDecisionCache, promptWorkspaceTrust, safeWorkspaceTrustDisplayField, } from './cli/workspace-trust-prompt.js';
36
38
  import { ClaudeSessionEnvironment } from './hooks/claude-session-environment.js';
37
39
  import { ClaudeMcpToolRegistry, } from './mcp/claude-mcp-tools.js';
38
40
  import { ClaudeMcpManagement, filterDisabledMcpResources, mcpScope, } from './mcp/claude-mcp-management.js';
@@ -45,6 +47,7 @@ import { redactSensitiveText, sensitiveEnvironmentValues, } from './platform/sen
45
47
  import { AnthropicCompatibleProvider } from './providers/anthropic-compatible.js';
46
48
  import { createAnthropicPromptCachePolicyResolver } from './providers/anthropic-prompt-cache.js';
47
49
  import { FallbackModelProvider } from './providers/fallback-provider.js';
50
+ import { DeadlineModelProvider } from './providers/deadline-provider.js';
48
51
  import { OpenAICompatibleProvider } from './providers/openai-compatible.js';
49
52
  import { parseContextEnvironment, parseProviderEnvironment, } from './providers/environment.js';
50
53
  import { ModelPricingRegistry, usageCostUsd } from './core/usage.js';
@@ -105,7 +108,7 @@ function createProviderForModel({ apiKey, environment, provider, context, contro
105
108
  ? { contextWindowTokens: context.contextWindowTokens }
106
109
  : {}),
107
110
  };
108
- return provider.provider === 'anthropic'
111
+ const concrete = provider.provider === 'anthropic'
109
112
  ? new AnthropicCompatibleProvider({
110
113
  ...providerOptions,
111
114
  promptCaching: resolvePromptCachePolicy({
@@ -124,7 +127,9 @@ function createProviderForModel({ apiKey, environment, provider, context, contro
124
127
  ...('anthropicVersion' in provider
125
128
  ? { anthropicVersion: provider.anthropicVersion }
126
129
  : {}),
127
- ...('webSearch' in provider ? { webSearch: provider.webSearch } : {}),
130
+ ...('webSearch' in provider
131
+ ? { webSearch: provider.webSearch }
132
+ : {}),
128
133
  })
129
134
  : new OpenAICompatibleProvider({
130
135
  ...providerOptions,
@@ -140,6 +145,10 @@ function createProviderForModel({ apiKey, environment, provider, context, contro
140
145
  },
141
146
  }),
142
147
  });
148
+ return new DeadlineModelProvider({
149
+ provider: concrete,
150
+ deadlineMs: provider.deadlineMs,
151
+ });
143
152
  };
144
153
  }
145
154
  const HELP = `Praxis — local-first general agent
@@ -216,6 +225,7 @@ Options:
216
225
  --setting-sources <sources> user, project, local, or an empty list
217
226
  --safe-mode Disable shared customizations
218
227
  --bare Use only explicitly supplied context
228
+ --trust-project Trust current workspace executables
219
229
  --system-prompt <prompt> Set system prompt
220
230
  --append-system-prompt <prompt> Append system prompt
221
231
  --exclude-dynamic-system-prompt-sections
@@ -755,6 +765,12 @@ const consoleIO = {
755
765
  isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
756
766
  readStdinLines: () => process.stdin,
757
767
  };
768
+ function warningEventSink(io) {
769
+ return (event) => {
770
+ if (event.type === 'warning')
771
+ io.stderr(`${event.message}\n`);
772
+ };
773
+ }
758
774
  /**
759
775
  * Shared runtime model precedence used by every consumer (provider
760
776
  * construction, status/doctor output, and the interactive display):
@@ -789,7 +805,7 @@ export function resolveUnknownCostSidecarPath(dataPlane, configRoot) {
789
805
  assertNativeDataPlane(dataPlane);
790
806
  return join(configRoot, 'state', 'unknown-cost-sidecar.json');
791
807
  }
792
- const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, emitToolUseSummaries = false, cwd: requestedCwd, sandboxOriginalCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
808
+ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, approveWorkspaceTrust, emitToolUseSummaries = false, cwd: requestedCwd, sandboxOriginalCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
793
809
  const runtimeEnvironment = requestedProviderEnvironment ?? process.env;
794
810
  const sandboxEnvironment = { ...runtimeEnvironment, ...environment };
795
811
  const claudeVersion = VERIFIED_CLAUDE_SCHEMA_VERSION;
@@ -971,11 +987,58 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
971
987
  }
972
988
  : {}),
973
989
  };
990
+ const warnedWorkspaceFingerprints = new Set();
991
+ let trustProjectRequestAvailable = cli.trustProject;
992
+ const authorizeWorkspaceExecutables = async (automaticSettings, automaticMcp, runtimeCwd) => {
993
+ if (cli.safeMode || simpleMode)
994
+ return true;
995
+ const trustRequested = trustProjectRequestAvailable;
996
+ trustProjectRequestAvailable = false;
997
+ const inventory = await workspaceTrustInventory({
998
+ cwd: runtimeCwd,
999
+ settings: automaticSettings,
1000
+ mcp: automaticMcp,
1001
+ });
1002
+ const assessment = await assessWorkspaceTrust(inventory, claudeStatePath);
1003
+ if (assessment.status !== 'untrusted')
1004
+ return true;
1005
+ let approved = trustRequested;
1006
+ if (!approved && approveWorkspaceTrust) {
1007
+ try {
1008
+ approved = await approveWorkspaceTrust(assessment);
1009
+ }
1010
+ catch (error) {
1011
+ if (!(error instanceof Error &&
1012
+ (error.name === 'AbortError' || error.name === 'CancellationError'))) {
1013
+ throw error;
1014
+ }
1015
+ approved = false;
1016
+ }
1017
+ }
1018
+ if (approved) {
1019
+ await persistWorkspaceTrust(assessment, claudeStatePath);
1020
+ return true;
1021
+ }
1022
+ const warningKey = workspaceTrustDecisionKey(assessment);
1023
+ if (!warnedWorkspaceFingerprints.has(warningKey)) {
1024
+ warnedWorkspaceFingerprints.add(warningKey);
1025
+ runtimeEventSink({
1026
+ type: 'warning',
1027
+ message: `Workspace executable resources blocked for ${safeWorkspaceTrustDisplayField(assessment.canonicalPath)}; restart to review them interactively, or rerun with --trust-project to approve the current fingerprint.`,
1028
+ });
1029
+ }
1030
+ return false;
1031
+ };
974
1032
  const hookConfiguration = async () => {
975
- const [settings, pluginResources] = await Promise.all([
1033
+ const [sharedResources, pluginResources] = await Promise.all([
976
1034
  cli.safeMode || simpleMode
977
- ? []
978
- : loadNativeSettings({ root: configRoot, cwd }),
1035
+ ? Promise.resolve({ settings: [], mcp: [] })
1036
+ : loadNativeSharedResources({
1037
+ root: configRoot,
1038
+ cwd,
1039
+ environment: runtimeEnvironment,
1040
+ includeProjectMemory: false,
1041
+ }),
979
1042
  loadClaudePlugins({
980
1043
  configRoot,
981
1044
  cwd,
@@ -983,13 +1046,20 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
983
1046
  pluginUrls: cli.pluginUrls,
984
1047
  strictPluginDirectories: cli.pluginDirectories.length + cli.pluginUrls.length > 0,
985
1048
  loadInstalled: !cli.safeMode && !simpleMode,
986
- readOnlyHooks: true,
1049
+ readOnlyExecutables: true,
987
1050
  environment: runtimeEnvironment,
988
1051
  }),
989
1052
  ]);
990
- return projectTuiHooks([
991
- ...settings,
1053
+ const automaticSettings = [
1054
+ ...sharedResources.settings,
992
1055
  ...pluginResources.settings,
1056
+ ];
1057
+ const automaticMcp = cli.strictMcpConfig
1058
+ ? []
1059
+ : [...sharedResources.mcp, ...pluginResources.mcp];
1060
+ const trusted = await authorizeWorkspaceExecutables(automaticSettings, automaticMcp, cwd);
1061
+ return projectTuiHooks([
1062
+ ...allowedWorkspaceHookSettings(automaticSettings, trusted),
993
1063
  ...(cli.additionalSettings ? [cli.additionalSettings] : []),
994
1064
  ]);
995
1065
  };
@@ -1001,7 +1071,10 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1001
1071
  const service = new ClaudeSessionService(options);
1002
1072
  if (!interactive)
1003
1073
  return service;
1004
- return Object.assign(service, { hookConfiguration });
1074
+ const initialHookConfiguration = await hookConfiguration();
1075
+ return Object.assign(service, {
1076
+ hookConfiguration: () => Promise.resolve(initialHookConfiguration),
1077
+ });
1005
1078
  }
1006
1079
  const toolProvider = provider ?? {
1007
1080
  model: 'praxis/provider',
@@ -1034,7 +1107,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1034
1107
  const baseSettings = nativeSharedResourcesEnabled
1035
1108
  ? await loadNativeSettings({ root: configRoot, cwd })
1036
1109
  : [];
1037
- const pluginResources = await loadClaudePlugins({
1110
+ const pluginLoadOptions = {
1038
1111
  configRoot,
1039
1112
  cwd,
1040
1113
  pluginDirectories: cli.pluginDirectories,
@@ -1042,6 +1115,10 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1042
1115
  strictPluginDirectories: cli.pluginDirectories.length + cli.pluginUrls.length > 0,
1043
1116
  loadInstalled: !cli.safeMode && !simpleMode,
1044
1117
  environment: runtimeEnvironment,
1118
+ };
1119
+ const executablePluginResources = await loadClaudePlugins({
1120
+ ...pluginLoadOptions,
1121
+ readOnlyExecutables: true,
1045
1122
  });
1046
1123
  const projectMemoryPolicy = cli.safeMode || simpleMode
1047
1124
  ? { enabled: false, extraction: false, recall: false }
@@ -1049,7 +1126,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1049
1126
  dataPlane,
1050
1127
  settings: [
1051
1128
  ...baseSettings,
1052
- ...pluginResources.settings,
1129
+ ...executablePluginResources.settings,
1053
1130
  ...(cli.additionalSettings ? [cli.additionalSettings] : []),
1054
1131
  ],
1055
1132
  environment: runtimeEnvironment,
@@ -1071,7 +1148,42 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1071
1148
  settings: [],
1072
1149
  mcp: [],
1073
1150
  };
1074
- const settings = [
1151
+ const trustSettings = [
1152
+ ...loadedResources.settings,
1153
+ ...executablePluginResources.settings,
1154
+ ];
1155
+ const trustMcp = cli.strictMcpConfig
1156
+ ? []
1157
+ : [...loadedResources.mcp, ...executablePluginResources.mcp];
1158
+ const workspaceExecutablesTrusted = await authorizeWorkspaceExecutables(trustSettings, trustMcp, cwd);
1159
+ const loadedPluginResources = await loadClaudePlugins({
1160
+ ...pluginLoadOptions,
1161
+ allowWorkspaceMcpb: workspaceExecutablesTrusted,
1162
+ });
1163
+ const materializedPluginMcp = new Map(loadedPluginResources.mcp.map((resource) => [resource.path, resource]));
1164
+ const pluginResources = {
1165
+ ...loadedPluginResources,
1166
+ settings: executablePluginResources.settings,
1167
+ mcp: executablePluginResources.mcp.flatMap((resource) => {
1168
+ if (resource.pluginExecutableSource?.kind !== 'mcpb')
1169
+ return [resource];
1170
+ const materialized = materializedPluginMcp.get(resource.path);
1171
+ if (!materialized)
1172
+ return [];
1173
+ if (materialized.pluginExecutableSource?.source !==
1174
+ resource.pluginExecutableSource.source ||
1175
+ materialized.pluginExecutableSource.fingerprint !==
1176
+ resource.pluginExecutableSource.fingerprint) {
1177
+ runtimeEventSink({
1178
+ type: 'warning',
1179
+ message: `Workspace MCPB source changed during trust preflight and was blocked: ${safeWorkspaceTrustDisplayField(resource.path)}. Restart to review the new fingerprint.`,
1180
+ });
1181
+ return [];
1182
+ }
1183
+ return [materialized];
1184
+ }),
1185
+ };
1186
+ const allSettings = [
1075
1187
  ...loadedResources.settings,
1076
1188
  ...pluginResources.settings,
1077
1189
  ...(cli.additionalSettings ? [cli.additionalSettings] : []),
@@ -1084,7 +1196,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1084
1196
  });
1085
1197
  }
1086
1198
  }
1087
- const configuredAgent = [...settings]
1199
+ const configuredAgent = [...allSettings]
1088
1200
  .reverse()
1089
1201
  .map((resource) => resource.value &&
1090
1202
  typeof resource.value === 'object' &&
@@ -1093,6 +1205,21 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1093
1205
  : undefined)
1094
1206
  .find((value) => typeof value === 'string' && value.length > 0);
1095
1207
  const selectedMainAgent = agent ?? configuredAgent;
1208
+ const automaticSettings = [
1209
+ ...loadedResources.settings,
1210
+ ...pluginResources.settings,
1211
+ ];
1212
+ const automaticMcp = cli.strictMcpConfig
1213
+ ? []
1214
+ : [...loadedResources.mcp, ...pluginResources.mcp];
1215
+ const hookSettings = [
1216
+ ...allowedWorkspaceHookSettings(automaticSettings, workspaceExecutablesTrusted),
1217
+ ...(cli.additionalSettings ? [cli.additionalSettings] : []),
1218
+ ];
1219
+ const executableMcp = [
1220
+ ...allowedWorkspaceMcpResources(automaticMcp, workspaceExecutablesTrusted),
1221
+ ...cli.mcpResources,
1222
+ ];
1096
1223
  const resources = {
1097
1224
  ...loadedResources,
1098
1225
  commands: [...loadedResources.commands, ...pluginResources.commands],
@@ -1102,10 +1229,8 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1102
1229
  ...pluginResources.agents,
1103
1230
  ...cli.inlineAgents,
1104
1231
  ],
1105
- settings,
1106
- mcp: cli.strictMcpConfig
1107
- ? cli.mcpResources
1108
- : [...loadedResources.mcp, ...pluginResources.mcp, ...cli.mcpResources],
1232
+ settings: allSettings,
1233
+ mcp: executableMcp,
1109
1234
  };
1110
1235
  const extensions = new ClaudeExtensionCatalog(resources, {
1111
1236
  disableSlashCommands: cli.disableSlashCommands,
@@ -1174,7 +1299,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1174
1299
  ...(exposePlanDirectory ? [resolve(configRoot, 'plans')] : []),
1175
1300
  ];
1176
1301
  const sandboxSettings = loadClaudeSandboxSettings({
1177
- resources: settings.filter((resource) => resource.plugin !== true),
1302
+ resources: allSettings.filter((resource) => resource.plugin !== true),
1178
1303
  cwd: workspace.cwd(),
1179
1304
  originalCwd: sandboxOriginalCwd ?? workspace.cwd(),
1180
1305
  configRoot,
@@ -1200,7 +1325,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1200
1325
  cwd,
1201
1326
  cwdProvider: () => workspace.cwd(),
1202
1327
  configRoot,
1203
- settings,
1328
+ settings: allSettings,
1204
1329
  allowedTools: cli.allowedTools,
1205
1330
  disallowedTools: cli.disallowedTools,
1206
1331
  additionalDirectories: permissionAdditionalDirectories,
@@ -1275,9 +1400,14 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1275
1400
  settings: [],
1276
1401
  mcp: [],
1277
1402
  };
1403
+ const refreshedSettings = [
1404
+ ...refreshed.settings,
1405
+ ...pluginResources.settings,
1406
+ ];
1407
+ const refreshedMcp = [...refreshed.mcp, ...pluginResources.mcp];
1408
+ const trusted = await authorizeWorkspaceExecutables(refreshedSettings, refreshedMcp, workspace.cwd());
1278
1409
  return runtimeMcpResources([
1279
- ...refreshed.mcp,
1280
- ...pluginResources.mcp,
1410
+ ...allowedWorkspaceMcpResources(refreshedMcp, trusted),
1281
1411
  ...cli.mcpResources,
1282
1412
  ]);
1283
1413
  },
@@ -1439,7 +1569,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1439
1569
  const hooks = cli.safeMode || simpleMode
1440
1570
  ? undefined
1441
1571
  : new ClaudeHookRunner({
1442
- settings,
1572
+ settings: hookSettings,
1443
1573
  cwd,
1444
1574
  onEvent: (event) => runtimeEventSink({ type: 'hook', event }),
1445
1575
  sessionEnvironment: hookSessionEnvironment,
@@ -1771,7 +1901,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1771
1901
  ? {}
1772
1902
  : { progressMessage: definition.progressMessage }),
1773
1903
  })),
1774
- hookConfiguration: async () => projectTuiHooks(settings),
1904
+ hookConfiguration: async () => projectTuiHooks(hookSettings),
1775
1905
  mcpInspect: () => service.mcpInspect(),
1776
1906
  mcpReconnect: (name) => service.mcpReconnect(name),
1777
1907
  mcpAuthenticate: (name) => service.mcpAuthenticate(name),
@@ -2051,6 +2181,14 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
2051
2181
  runInteractive: async ({ agent, controls, initialPrompt, resume, signal, }) => {
2052
2182
  const { runInteractive } = await import('./cli/interactive.js');
2053
2183
  const interactiveControls = controls ?? DEFAULT_CLI_CONTROLS;
2184
+ let workspaceTrustPreflightOpen = true;
2185
+ const cachedWorkspaceTrustDecision = createWorkspaceTrustDecisionCache((request) => promptWorkspaceTrust(request, {
2186
+ output: (text) => process.stderr.write(text),
2187
+ ...(signal ? { signal } : {}),
2188
+ }));
2189
+ const approveInteractiveWorkspaceTrust = (request) => workspaceTrustPreflightOpen
2190
+ ? cachedWorkspaceTrustDecision(request)
2191
+ : Promise.resolve(false);
2054
2192
  const initialAdditionalDirectories = interactiveControls.addDirectories.map((directory) => realpathSync(resolve(process.cwd(), directory)));
2055
2193
  const interactiveDataPlane = interactiveControls.dataPlane ?? resolveDataPlane();
2056
2194
  const { configRoot: interactiveConfigRoot, statePath: interactiveStatePath, } = resolveInteractiveRuntimeSettingsLocation(interactiveDataPlane);
@@ -2079,11 +2217,14 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
2079
2217
  ...(agent === undefined ? {} : { agent }),
2080
2218
  controls: {
2081
2219
  ...interactiveControls,
2220
+ trustProject: workspaceTrustPreflightOpen && interactiveControls.trustProject,
2082
2221
  addDirectories: options.additionalDirectories ??
2083
2222
  interactiveControls.addDirectories,
2084
2223
  },
2085
2224
  interactive: true,
2225
+ approveWorkspaceTrust: approveInteractiveWorkspaceTrust,
2086
2226
  });
2227
+ workspaceTrustPreflightOpen = false;
2087
2228
  if (resumePath !== undefined) {
2088
2229
  await commands.registerResumePath?.(resumePath);
2089
2230
  }
@@ -3491,7 +3632,7 @@ async function executeMcpCommand(args, invocation, io, dependencies, signal) {
3491
3632
  writeError: (message) => io.stderr(message),
3492
3633
  createToolRegistry: async () => {
3493
3634
  const service = await dependencies.createService({
3494
- eventSink: () => undefined,
3635
+ eventSink: warningEventSink(io),
3495
3636
  requireProvider: false,
3496
3637
  exposeToolRegistry: true,
3497
3638
  approveTool: async () => true,
@@ -4273,7 +4414,7 @@ async function executeTeamCommand(args, io, dependencies, signal) {
4273
4414
  dataPlane: 'native',
4274
4415
  };
4275
4416
  const service = await dependencies.createService({
4276
- eventSink: () => undefined,
4417
+ eventSink: warningEventSink(io),
4277
4418
  requireProvider: !['list', 'status', 'logs', 'attach'].includes(parsed.command),
4278
4419
  exposeToolRegistry: true,
4279
4420
  controls: nativeControls,
@@ -4594,7 +4735,7 @@ async function execute(argv, io, dependencies, signal) {
4594
4735
  }
4595
4736
  if (invocation.initOnly) {
4596
4737
  const lifecycleService = await dependencies.createService({
4597
- eventSink: () => undefined,
4738
+ eventSink: warningEventSink(io),
4598
4739
  requireProvider: false,
4599
4740
  exposeToolRegistry: true,
4600
4741
  ...(signal ? { signal } : {}),
@@ -13,6 +13,11 @@ export interface JsonResource {
13
13
  plugin?: true;
14
14
  pluginName?: string;
15
15
  pluginSource?: string;
16
+ pluginExecutableSource?: {
17
+ kind: 'mcpb';
18
+ source: string;
19
+ fingerprint: string;
20
+ };
16
21
  environment?: Readonly<Record<string, string>>;
17
22
  sensitiveValues?: readonly string[];
18
23
  }
@@ -1,4 +1,5 @@
1
1
  import { type DataPlane } from '../persistence/data-plane.js';
2
+ export declare const CLAUDE_PLUGIN_MCPB_ARCHIVE_BYTES: number;
2
3
  export type ClaudePluginMcpbUserValue = string | number | boolean | readonly string[];
3
4
  export interface ClaudePluginMcpbManifest {
4
5
  manifest_version: string;
@@ -60,6 +61,7 @@ export interface LoadClaudePluginMcpbOptions {
60
61
  signal?: AbortSignal;
61
62
  timeoutMs?: number;
62
63
  refresh?: boolean;
64
+ requireHttps?: boolean;
63
65
  limits?: ClaudePluginMcpbLimits;
64
66
  fetch?: typeof fetch;
65
67
  }
@@ -13,6 +13,7 @@ import manifestSchemaV04 from './mcpb-schemas/mcpb-manifest-v0.4.schema.json' wi
13
13
  import { ExclusiveFileLease, } from '../platform/exclusive-file-lease.js';
14
14
  import { resolveDataPlaneRoot, } from '../persistence/data-plane.js';
15
15
  const DEFAULT_ARCHIVE_BYTES = 512 * 1024 * 1024;
16
+ export const CLAUDE_PLUGIN_MCPB_ARCHIVE_BYTES = DEFAULT_ARCHIVE_BYTES;
16
17
  const DEFAULT_EXTRACTED_BYTES = 1024 * 1024 * 1024;
17
18
  const DEFAULT_FILE_BYTES = 512 * 1024 * 1024;
18
19
  const DEFAULT_FILES = 100_000;
@@ -540,9 +541,14 @@ async function fetchArchive(source, cached, options, signal, archiveLimit) {
540
541
  throw new Error('MCPB download redirect is missing Location');
541
542
  }
542
543
  const redirected = new URL(location, requestUrl);
543
- if (redirected.protocol !== 'http:' && redirected.protocol !== 'https:') {
544
+ if ((options.requireHttps && redirected.protocol !== 'https:') ||
545
+ (!options.requireHttps &&
546
+ redirected.protocol !== 'http:' &&
547
+ redirected.protocol !== 'https:')) {
544
548
  await response.body?.cancel();
545
- throw new Error('MCPB download redirect must use HTTP or HTTPS');
549
+ throw new Error(options.requireHttps
550
+ ? 'MCPB download redirect must use HTTPS'
551
+ : 'MCPB download redirect must use HTTP or HTTPS');
546
552
  }
547
553
  await response.body?.cancel();
548
554
  if (++redirects > 5) {
@@ -627,6 +633,9 @@ async function resolveMcpbSource(options, signal) {
627
633
  const pluginRoot = await realpath(resolve(options.pluginRoot));
628
634
  signal.throwIfAborted();
629
635
  const remote = /^https?:\/\//u.test(options.source);
636
+ if (options.requireHttps && remote && !/^https:\/\//u.test(options.source)) {
637
+ throw new Error('MCPB download source must use HTTPS');
638
+ }
630
639
  if (!remote && /^[a-z][a-z0-9+.-]*:\/\//iu.test(options.source))
631
640
  throw new Error('MCPB source URL must use HTTP or HTTPS');
632
641
  if (!['.mcpb', '.dxt'].some((extension) => options.source.endsWith(extension))) {
@@ -108,6 +108,8 @@ export declare function loadClaudePlugins(options: {
108
108
  pluginUrls?: readonly string[];
109
109
  loadInstalled?: boolean;
110
110
  readOnlyHooks?: boolean;
111
+ readOnlyExecutables?: boolean;
112
+ allowWorkspaceMcpb?: boolean;
111
113
  environment?: Readonly<Record<string, string | undefined>>;
112
114
  }): Promise<ClaudePluginResources>;
113
115
  export declare function validateClaudePlugin(path: string, options?: {