@adhdev/daemon-core 1.0.18-rc.16 → 1.0.18-rc.18

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.
@@ -0,0 +1,14 @@
1
+ import type { AutoApproveMode, AutoApproveModeRisk, AutoApproveModeStrategy, ProviderModule } from './contracts.js';
2
+ export interface ResolvedAutoApproveMode {
3
+ active: boolean;
4
+ strategy: AutoApproveModeStrategy;
5
+ modeId: string;
6
+ }
7
+ /** Runtime defense-in-depth for provider definitions that bypass schema validation. */
8
+ export declare function deriveAutoApproveModeRisk(mode: Pick<AutoApproveMode, 'risk' | 'launchArgs'>): AutoApproveModeRisk;
9
+ /**
10
+ * Resolve new mode settings before the legacy boolean. A stale/unknown explicit
11
+ * mode id fails closed instead of falling through to an enabled legacy setting.
12
+ */
13
+ export declare function resolveProviderAutoApproveMode(provider: ProviderModule, settings: Record<string, unknown> | undefined): ResolvedAutoApproveMode;
14
+ export declare function findProviderAutoApproveMode(provider: ProviderModule | undefined, modeId: string): AutoApproveMode | undefined;
@@ -810,7 +810,11 @@ export declare class CliProviderInstance implements ProviderInstance {
810
810
  getAdapter(): ProviderCliAdapter;
811
811
  get cliType(): string;
812
812
  get cliName(): string;
813
+ private resolveAutoApproveMode;
814
+ /** Legacy boolean view retained for internal/test compatibility. */
813
815
  private shouldAutoApprove;
816
+ private shouldUsePtyAutoApprove;
817
+ private resetPtyAutoApproveState;
814
818
  /** @see ProviderInstance.noteManualInteraction */
815
819
  noteManualInteraction(now?: number, opts?: {
816
820
  passive?: boolean;
@@ -389,6 +389,21 @@ export interface ProviderCompatibilityEntry {
389
389
  ideVersion: string;
390
390
  scriptDir: string;
391
391
  }
392
+ export type AutoApproveModeStrategy = 'pty-parse-default' | 'launch-args' | 'post-boot-command';
393
+ export type AutoApproveModeRisk = 'safe' | 'caution' | 'dangerous';
394
+ export interface AutoApproveMode {
395
+ id: string;
396
+ label: string;
397
+ strategy: AutoApproveModeStrategy;
398
+ risk: AutoApproveModeRisk;
399
+ warning?: string;
400
+ launchArgs?: string[];
401
+ removeArgs?: string[];
402
+ }
403
+ export interface AutoApproveModesConfig {
404
+ default: string;
405
+ modes: AutoApproveMode[];
406
+ }
392
407
  export interface ProviderModule {
393
408
  /** Unique identifier (e.g. 'cline', 'cursor', 'gemini-cli') */
394
409
  type: string;
@@ -418,6 +433,8 @@ export interface ProviderModule {
418
433
  status?: string;
419
434
  /** Inventory/support detail string maintained in adhdev-providers */
420
435
  details?: string;
436
+ /** Provider-specific auto-approve choices and their launch/runtime strategy. */
437
+ autoApproveModes?: AutoApproveModesConfig;
421
438
  /** Install instructions (shown when command is missing) */
422
439
  install?: string;
423
440
  /** Custom version detection command (e.g. 'cursor --version', 'claude -v') */
@@ -3,3 +3,4 @@ export interface ProviderValidationResult {
3
3
  warnings: string[];
4
4
  }
5
5
  export declare function validateProviderDefinition(raw: unknown): ProviderValidationResult;
6
+ export declare function validateAutoApproveModes(raw: unknown, errors: string[]): void;
@@ -14,6 +14,8 @@ import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
14
14
  import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
15
15
  import type { MeshMagiActivitySummary } from './mesh/mesh-magi-status.js';
16
16
  import type { MagiKindPanelMap, DifficultyBrainMap, NodeCapabilitySlot } from '@adhdev/mesh-shared';
17
+ import type { ProviderModule } from './providers/contracts.js';
18
+ import type { RepoMeshDeclarativeConfig } from './config/mesh-json-config.js';
17
19
  export interface RepoMesh {
18
20
  id: string;
19
21
  name: string;
@@ -255,6 +257,8 @@ export interface RepoMeshPolicy {
255
257
  * A node policy may override this per-node (RepoMeshNodePolicy.delegatedWorkerAutoApprove).
256
258
  */
257
259
  delegatedWorkerAutoApprove?: boolean;
260
+ /** Explicit opt-in required before delegated workers may use a dangerous provider mode. */
261
+ delegatedWorkerDangerousModeAllow?: boolean;
258
262
  /**
259
263
  * MESH-SEND-KEYS (feature 3): opt-in to allow the coordinator to inject
260
264
  * DESTRUCTIVE keys (CTRL_C / ESC) into a worker PTY via mesh_send_keys. These
@@ -406,6 +410,8 @@ export interface RepoMeshNodePolicy {
406
410
  * precedence over the mesh-level policy for worker sessions launched onto this node.
407
411
  */
408
412
  delegatedWorkerAutoApprove?: boolean;
413
+ /** Per-node override for dangerous delegated worker mode authorization. */
414
+ delegatedWorkerDangerousModeAllow?: boolean;
409
415
  /**
410
416
  * MESH-SEND-KEYS (feature 3): per-node override for
411
417
  * RepoMeshPolicy.allowSendKeysDestructive.
@@ -524,13 +530,39 @@ export declare function normalizeAutoFastForwardPolicy(value: unknown): NonNulla
524
530
  */
525
531
  export declare function mergeAndNormalizePolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMeshPolicy> | undefined): RepoMeshPolicy;
526
532
  /**
527
- * Resolve whether a delegated worker session launched onto `nodePolicy` (within a mesh
528
- * governed by `meshPolicy`) should auto-approve. Precedence: node override mesh policy
529
- * default true. The result is stamped into the worker launch settings envelope as
530
- * `autoApprove`; it wins over the global per-provider-type autoApprove config because the
531
- * launch path merges the envelope as a settingsOverride on top of the provider defaults.
533
+ * Resolve delegated worker auto-approve. Legacy providers return a boolean. Providers
534
+ * with modes return a mode id, except a dangerous mode is downgraded to a
535
+ * non-dangerous PTY mode unless mesh/node policy explicitly opts in.
536
+ *
537
+ * THREE EXPLICIT STAGES do not collapse them; the ordering is a hard MAGI
538
+ * invariant:
539
+ *
540
+ * ① ENABLE gate (machine-local policy only): node boolean > mesh boolean.
541
+ * `enabled=false` returns `false` IMMEDIATELY, BEFORE any mode selection.
542
+ * The repo `mesh.json` providerDefaults has ZERO influence here — a
543
+ * node/mesh opt-out is never overridden by a repo-declared requested mode.
544
+ *
545
+ * ② MODE selection (only when enabled === true):
546
+ * task override [FUTURE — param reserved below, not yet wired] >
547
+ * repo mesh.json providerDefaults.autoApproveModes[providerType] >
548
+ * provider spec autoApproveModes.default.
549
+ * A repo-requested mode ID is adopted ONLY when it exists in the provider's
550
+ * own `autoApproveModes.modes`; an unknown/stale/typo'd ID is IGNORED and we
551
+ * fall back to the provider default (fail-closed: never coerce into a
552
+ * dangerous mode via a bad ID).
553
+ *
554
+ * ③ DANGEROUS gate: whichever mode stage ② picked, if it is dangerous and the
555
+ * machine-local delegatedWorkerDangerousModeAllow is not set, downgrade to a
556
+ * non-dangerous PTY-parse mode (or `false` if none exists).
532
557
  */
533
- export declare function resolveDelegatedWorkerAutoApprove(meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerAutoApprove'> | null, nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerAutoApprove'> | null): boolean;
558
+ export declare function resolveDelegatedWorkerAutoApprove(meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerAutoApprove' | 'delegatedWorkerDangerousModeAllow'> | null, nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerAutoApprove' | 'delegatedWorkerDangerousModeAllow'> | null, provider?: Pick<ProviderModule, 'autoApproveModes'> | null, repoConfig?: RepoMeshDeclarativeConfig | null, providerType?: string | null): boolean | string;
559
+ export declare function resolveDelegatedWorkerDangerousModeAllow(meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerDangerousModeAllow'> | null, nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerDangerousModeAllow'> | null): boolean;
560
+ /** Shape a boolean-or-mode resolution for the settings precedence contract. */
561
+ export declare function delegatedWorkerAutoApproveSettings(meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerAutoApprove' | 'delegatedWorkerDangerousModeAllow'> | null, nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerAutoApprove' | 'delegatedWorkerDangerousModeAllow'> | null, provider?: Pick<ProviderModule, 'autoApproveModes'> | null, repoConfig?: RepoMeshDeclarativeConfig | null, providerType?: string | null): {
562
+ autoApprove: boolean | undefined;
563
+ autoApproveMode: string | undefined;
564
+ delegatedWorkerDangerousModeAllow: boolean;
565
+ };
534
566
  /**
535
567
  * MESH-SEND-KEYS (feature 3): resolve whether DESTRUCTIVE key injection
536
568
  * (CTRL_C/ESC via mesh_send_keys) is permitted for a node. Node policy overrides
@@ -13,7 +13,7 @@ export type { ProviderState, ProviderStatus, ActiveChatData, IdeProviderState, C
13
13
  export type { ProviderErrorReason } from './providers/provider-instance.js';
14
14
  import type { ActiveChatData as _ActiveChatData, ProviderErrorReason as _ProviderErrorReason } from './providers/provider-instance.js';
15
15
  import type { WorkspaceEntry } from './config/workspaces.js';
16
- import type { ProviderMeshCoordinatorConfig, ProviderResumeCapability } from './providers/contracts.js';
16
+ import type { AutoApproveModesConfig, ProviderMeshCoordinatorConfig, ProviderResumeCapability } from './providers/contracts.js';
17
17
  import type { GitCompactSummary, GitWorkspaceUpdate, WorkspaceGitSubscriptionParams } from './git/git-types.js';
18
18
  import type { InteractivePrompt } from './providers/types/interactive-prompt.js';
19
19
  export type { GitCommandName, GitCompactSummary, GitDiffSummary, GitFailureReason, GitFileChange, GitFileChangeStatus, GitRepoIdentity, GitRepoStatus, GitSnapshot, GitSnapshotCompareSummary, GitSnapshotReason, GitWorkspaceUpdate, WorkspaceGitSubscriptionParams, } from './git/git-types.js';
@@ -428,6 +428,8 @@ export interface AvailableProviderInfo {
428
428
  lastVerification?: MachineProviderCheckResult;
429
429
  /** Provider-declared Repo Mesh coordinator/MCP behavior. */
430
430
  meshCoordinator?: ProviderMeshCoordinatorConfig;
431
+ /** Provider-declared auto-approve choices shown by session launch UIs. */
432
+ autoApproveModes?: AutoApproveModesConfig;
431
433
  /** BRAIN-ROUTING: suggested model values for the new-session model dropdown. */
432
434
  modelOptions?: string[];
433
435
  /** BRAIN-ROUTING: reasoning-effort values for the new-session thinking dropdown. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "1.0.18-rc.16",
3
+ "version": "1.0.18-rc.18",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,8 +47,8 @@
47
47
  "author": "vilmire",
48
48
  "license": "AGPL-3.0-or-later",
49
49
  "dependencies": {
50
- "@adhdev/mesh-shared": "1.0.18-rc.16",
51
- "@adhdev/session-host-core": "1.0.18-rc.16",
50
+ "@adhdev/mesh-shared": "1.0.18-rc.18",
51
+ "@adhdev/session-host-core": "1.0.18-rc.18",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -34,6 +34,7 @@ import type { SessionRegistry } from '../sessions/registry.js';
34
34
  import type { ProviderInstance } from '../providers/provider-instance.js';
35
35
  import { LOG } from '../logging/logger.js';
36
36
  import { shouldRestoreHostedRuntime } from './hosted-runtime-restore.js';
37
+ import { findProviderAutoApproveMode, resolveProviderAutoApproveMode } from '../providers/auto-approve-modes.js';
37
38
 
38
39
  // ─── external dependency interface ──────────────────────────
39
40
 
@@ -452,6 +453,40 @@ export function expandThinkingLaunchArgs(
452
453
  return template.map((part) => part.includes('{{level}}') ? part.replace('{{level}}', mapped) : part);
453
454
  }
454
455
 
456
+ function matchesRemovedLaunchArg(arg: string, removeArg: string): boolean {
457
+ return arg === removeArg || (removeArg.startsWith('--') && arg.startsWith(`${removeArg}=`));
458
+ }
459
+
460
+ /**
461
+ * Apply a selected launch-args auto-approve mode without mutating provider metadata.
462
+ * removeArgs only targets provider-owned base spawn.args; launchArgs are prepended to
463
+ * per-launch args beside model/thinking args, making conflict removal order-independent.
464
+ */
465
+ export function applyAutoApproveModeLaunchArgs(
466
+ provider: ProviderModule | undefined,
467
+ cliArgs: string[] | undefined,
468
+ settings: Record<string, unknown> | undefined,
469
+ ): { provider: ProviderModule | undefined; cliArgs: string[] | undefined } {
470
+ if (!provider) return { provider, cliArgs };
471
+ const resolved = resolveProviderAutoApproveMode(provider, settings);
472
+ if (!resolved.active || resolved.strategy !== 'launch-args') return { provider, cliArgs };
473
+ const mode = findProviderAutoApproveMode(provider, resolved.modeId);
474
+ if (!mode || !Array.isArray(mode.launchArgs) || mode.launchArgs.length === 0) return { provider, cliArgs };
475
+
476
+ const removeArgs = Array.isArray(mode.removeArgs) ? mode.removeArgs : [];
477
+ const baseArgs = provider.spawn?.args;
478
+ const filteredBaseArgs = Array.isArray(baseArgs) && removeArgs.length > 0
479
+ ? baseArgs.filter((arg) => !removeArgs.some((removeArg) => matchesRemovedLaunchArg(arg, removeArg)))
480
+ : baseArgs;
481
+ const launchProvider = filteredBaseArgs === baseArgs
482
+ ? provider
483
+ : { ...provider, spawn: { ...provider.spawn!, args: filteredBaseArgs } };
484
+ return {
485
+ provider: launchProvider,
486
+ cliArgs: [...mode.launchArgs, ...(cliArgs || [])],
487
+ };
488
+ }
489
+
455
490
  function readSubcommandSessionId(args: string[], subcommands: string[]): string | undefined {
456
491
  const resumeIndex = args.findIndex((arg) => subcommands.includes(arg));
457
492
  if (resumeIndex < 0) return undefined;
@@ -1048,6 +1083,17 @@ export class DaemonCliManager {
1048
1083
  console.log(colorize('cyan', ` 📦 Using provider: ${provider.name} (${provider.type})`));
1049
1084
  }
1050
1085
 
1086
+ const launchSettings = {
1087
+ ...this.providerLoader.getSettings(normalizedType),
1088
+ ...(options?.settingsOverride || {}),
1089
+ };
1090
+ const versionResolvedProvider = provider
1091
+ ? (this.providerLoader.resolve(cliType, { version: cliInfo.version }) || provider)
1092
+ : undefined;
1093
+ const autoApproveLaunch = applyAutoApproveModeLaunchArgs(versionResolvedProvider, cliArgs, launchSettings);
1094
+ const launchProvider = autoApproveLaunch.provider || provider;
1095
+ const cliArgsWithAutoApprove = autoApproveLaunch.cliArgs;
1096
+
1051
1097
  // ─── Model axis (MAGI kind-panel): expand initialModel → launch args ───
1052
1098
  // For a plain CLI provider the model is selected at spawn time via the manifest's
1053
1099
  // modelLaunchArgs template ('{{model}}' → the requested model). ACP providers took
@@ -1055,10 +1101,10 @@ export class DaemonCliManager {
1055
1101
  // or no requested model, is a no-op — model selection is best-effort and must never
1056
1102
  // fail a launch. The model args are prepended so a caller's explicit cliArgs (e.g. a
1057
1103
  // resume flag) still win positionally where order matters.
1058
- const modelLaunchArgs = expandModelLaunchArgs(provider?.modelLaunchArgs, initialModel);
1104
+ const modelLaunchArgs = expandModelLaunchArgs(launchProvider?.modelLaunchArgs, initialModel);
1059
1105
  const cliArgsWithModel = modelLaunchArgs
1060
- ? [...modelLaunchArgs, ...(cliArgs || [])]
1061
- : cliArgs;
1106
+ ? [...modelLaunchArgs, ...(cliArgsWithAutoApprove || [])]
1107
+ : cliArgsWithAutoApprove;
1062
1108
  if (initialModel && !modelLaunchArgs) {
1063
1109
  LOG.warn('CLI', `[${normalizedType}] initialModel='${initialModel}' requested but provider declares no modelLaunchArgs template — launching without model selection.`);
1064
1110
  }
@@ -1069,7 +1115,7 @@ export class DaemonCliManager {
1069
1115
  // Best-effort; a provider with no template (or no requested level) is a no-op. ACP
1070
1116
  // providers route thinking through setConfigOption('thought_level') above.
1071
1117
  const initialThinkingLevel = options?.initialThinkingLevel;
1072
- const thinkingLaunchArgs = expandThinkingLaunchArgs(provider?.thinkingLaunchArgs, initialThinkingLevel, provider?.thinkingLevelMap);
1118
+ const thinkingLaunchArgs = expandThinkingLaunchArgs(launchProvider?.thinkingLaunchArgs, initialThinkingLevel, launchProvider?.thinkingLevelMap);
1073
1119
  const cliArgsWithBrain = thinkingLaunchArgs
1074
1120
  ? [...thinkingLaunchArgs, ...(cliArgsWithModel || [])]
1075
1121
  : cliArgsWithModel;
@@ -1078,13 +1124,13 @@ export class DaemonCliManager {
1078
1124
  }
1079
1125
 
1080
1126
  // ─── Resolve launch options → provider session binding ───
1081
- const sessionBinding = resolveCliSessionBinding(provider, normalizedType, cliArgsWithBrain, options?.resumeSessionId);
1127
+ const sessionBinding = resolveCliSessionBinding(launchProvider, normalizedType, cliArgsWithBrain, options?.resumeSessionId);
1082
1128
  const resolvedCliArgs = sessionBinding.cliArgs;
1083
1129
 
1084
1130
  // If InstanceManager exists, manage as CliProviderInstance unified
1085
1131
  const instanceManager = this.deps.getInstanceManager();
1086
- if (provider && instanceManager) {
1087
- const resolvedProvider = this.providerLoader.resolve(cliType, { version: cliInfo.version }) || provider;
1132
+ if (launchProvider && instanceManager) {
1133
+ const resolvedProvider = launchProvider;
1088
1134
  await this.registerCliInstance(
1089
1135
  key,
1090
1136
  normalizedType,
@@ -1092,7 +1138,7 @@ export class DaemonCliManager {
1092
1138
  resolvedDir,
1093
1139
  resolvedCliArgs,
1094
1140
  resolvedProvider,
1095
- { ...this.providerLoader.getSettings(normalizedType), ...(options?.settingsOverride || {}) },
1141
+ launchSettings,
1096
1142
  false,
1097
1143
  {
1098
1144
  providerSessionId: sessionBinding.providerSessionId,
@@ -399,6 +399,161 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
399
399
  }
400
400
  },
401
401
 
402
+ // READ path for `.adhdev/mesh.json` — returns the currently-committed repo
403
+ // config (parsed + normalized) for a workspace so a UI can render/edit the
404
+ // existing declarative zones (notably `providerDefaults.autoApproveModes`)
405
+ // WITHOUT re-deriving them from the machine-local scaffold. Never writes.
406
+ // `config` is undefined when no repo file exists (sourceType 'unavailable') or
407
+ // it is unparseable (sourceType 'invalid', with the parse error surfaced).
408
+ read_mesh_json_config: async (_ctx: MedFamilyContext, args: any) => {
409
+ const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
410
+ try {
411
+ const { loadRepoMeshJsonConfig } = await import('../../config/mesh-json-config.js');
412
+ const loaded = loadRepoMeshJsonConfig(workspace);
413
+ return {
414
+ success: true,
415
+ workspace,
416
+ sourceType: loaded.sourceType,
417
+ source: loaded.source,
418
+ ...(loaded.path ? { path: loaded.path } : {}),
419
+ ...(loaded.error ? { error: loaded.error } : {}),
420
+ config: loaded.config,
421
+ // Convenience projection so the UI does not have to reach into config.
422
+ providerDefaults: loaded.config?.providerDefaults,
423
+ };
424
+ } catch (e: any) {
425
+ return { success: false, error: e.message };
426
+ }
427
+ },
428
+
429
+ // Partial-edit WRITE path for `.adhdev/mesh.json` `providerDefaults` — a
430
+ // READ-MODIFY-WRITE that preserves operator hand-edits. Unlike
431
+ // write_mesh_json_config (which rebuilds the WHOLE file from the machine-local
432
+ // scaffold and can silently drop hand-edited zones), this parses the existing
433
+ // repo file, merges ONLY the providerDefaults.autoApproveModes zone, and
434
+ // re-serializes — coordinator prompt, operating notes and limits authored in
435
+ // the repo are carried through untouched. Defaults to dry-run.
436
+ //
437
+ // args: { workspace?, autoApproveModes: Record<providerType,modeId>, write?, merge? }
438
+ // merge=true (default): per-provider merge into the existing map; a modeId of
439
+ // '' | null removes that provider's entry. merge=false: REPLACE the whole
440
+ // autoApproveModes map with the supplied one.
441
+ set_mesh_provider_defaults: async (_ctx: MedFamilyContext, args: any) => {
442
+ const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
443
+ const write = args?.write === true;
444
+ const merge = args?.merge !== false; // default true
445
+ const inputModes = args?.autoApproveModes;
446
+ if (inputModes !== undefined && (typeof inputModes !== 'object' || inputModes === null || Array.isArray(inputModes))) {
447
+ return { success: false, error: 'autoApproveModes must be an object (providerType → modeId) when provided' };
448
+ }
449
+ try {
450
+ const {
451
+ loadRepoMeshJsonConfig,
452
+ normalizeRepoMeshDeclarativeConfig,
453
+ MESH_JSON_CONFIG_LOCATIONS,
454
+ } = await import('../../config/mesh-json-config.js');
455
+ const { existsSync, readFileSync, mkdirSync, writeFileSync } = await import('fs');
456
+ const { dirname, join } = await import('path');
457
+ const yaml = await import('js-yaml');
458
+
459
+ const relativePath = MESH_JSON_CONFIG_LOCATIONS[0];
460
+
461
+ // Read-modify-write: parse the EXISTING on-disk document (preferring the
462
+ // first existing json/yaml variant) so unrelated zones survive verbatim.
463
+ let baseDoc: Record<string, any> = { version: 1 };
464
+ let existingPath = join(workspace, relativePath);
465
+ let existedAsYaml = false;
466
+ for (const relative of MESH_JSON_CONFIG_LOCATIONS) {
467
+ const candidate = join(workspace, relative);
468
+ if (!existsSync(candidate)) continue;
469
+ try {
470
+ const text = readFileSync(candidate, 'utf-8');
471
+ const parsed = /\.json$/i.test(candidate) ? JSON.parse(text) : yaml.load(text);
472
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
473
+ baseDoc = parsed as Record<string, any>;
474
+ existingPath = candidate;
475
+ existedAsYaml = !/\.json$/i.test(candidate);
476
+ }
477
+ } catch (e: any) {
478
+ return { success: false, error: `existing ${relative} is unparseable, refusing to overwrite: ${e?.message || e}` };
479
+ }
480
+ break;
481
+ }
482
+
483
+ // Merge ONLY the providerDefaults.autoApproveModes zone.
484
+ const existingPd = baseDoc.providerDefaults && typeof baseDoc.providerDefaults === 'object' && !Array.isArray(baseDoc.providerDefaults)
485
+ ? baseDoc.providerDefaults as Record<string, any>
486
+ : {};
487
+ const existingModes = existingPd.autoApproveModes && typeof existingPd.autoApproveModes === 'object' && !Array.isArray(existingPd.autoApproveModes)
488
+ ? { ...existingPd.autoApproveModes as Record<string, string> }
489
+ : {};
490
+
491
+ const nextModes: Record<string, string> = merge ? existingModes : {};
492
+ if (inputModes) {
493
+ for (const [providerType, modeId] of Object.entries(inputModes as Record<string, unknown>)) {
494
+ const type = typeof providerType === 'string' ? providerType.trim() : '';
495
+ if (!type) continue;
496
+ const id = typeof modeId === 'string' ? modeId.trim() : '';
497
+ if (id) nextModes[type] = id;
498
+ else delete nextModes[type]; // '' / null → remove this provider's entry
499
+ }
500
+ }
501
+
502
+ const nextDoc: Record<string, any> = { ...baseDoc, version: 1 };
503
+ if (Object.keys(nextModes).length) {
504
+ nextDoc.providerDefaults = { ...existingPd, autoApproveModes: nextModes };
505
+ } else {
506
+ // No entries left → drop the zone entirely so we don't leave an empty stub.
507
+ if (nextDoc.providerDefaults) {
508
+ const { autoApproveModes, ...restPd } = nextDoc.providerDefaults;
509
+ if (Object.keys(restPd).length) nextDoc.providerDefaults = restPd;
510
+ else delete nextDoc.providerDefaults;
511
+ }
512
+ }
513
+
514
+ // Validate the merged document before it ever touches disk.
515
+ const validation = normalizeRepoMeshDeclarativeConfig(nextDoc);
516
+ if (!validation.valid) {
517
+ return { success: false, error: `merged mesh.json is invalid: ${validation.errors.join('; ')}` };
518
+ }
519
+
520
+ // Serialize in the on-disk format (JSON unless the existing file was YAML).
521
+ const absolutePath = existingPath;
522
+ const serialized = existedAsYaml
523
+ ? yaml.dump(nextDoc, { indent: 2 })
524
+ : `${JSON.stringify(nextDoc, null, 2)}\n`;
525
+
526
+ if (!write) {
527
+ return {
528
+ success: true,
529
+ written: false,
530
+ dryRun: true,
531
+ path: absolutePath,
532
+ relativePath,
533
+ merge,
534
+ providerDefaults: nextDoc.providerDefaults,
535
+ preview: serialized,
536
+ note: 'Dry-run: nothing written. Re-run with write=true to persist. Only the providerDefaults zone is merged; other repo zones are preserved.',
537
+ };
538
+ }
539
+
540
+ mkdirSync(dirname(absolutePath), { recursive: true });
541
+ writeFileSync(absolutePath, serialized, 'utf-8');
542
+ return {
543
+ success: true,
544
+ written: true,
545
+ dryRun: false,
546
+ path: absolutePath,
547
+ relativePath,
548
+ merge,
549
+ providerDefaults: nextDoc.providerDefaults,
550
+ note: 'Wrote providerDefaults into .adhdev/mesh.json (read-modify-write; other zones preserved). Commit it to the repo.',
551
+ };
552
+ } catch (e: any) {
553
+ return { success: false, error: e.message };
554
+ }
555
+ },
556
+
402
557
  delete_mesh: async (_ctx: MedFamilyContext, args: any) => {
403
558
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
404
559
  if (!meshId) return { success: false, error: 'meshId required' };
@@ -19,6 +19,18 @@
19
19
  * machine-local policy directly. The repo file shapes the coordinator prompt and
20
20
  * operating notes, nothing else.
21
21
  *
22
+ * `providerDefaults` IS NOT POLICY EITHER. It is a repo-shared DECLARATIVE
23
+ * "requested auto-approve mode" per providerType — the answer to "WHEN a delegated
24
+ * worker of type X is auto-approved, WHICH mode should it use by default". It has
25
+ * NO say over WHETHER auto-approve is enabled: the ENABLE decision (and the
26
+ * dangerous-mode opt-in) remains 100% machine-local (meshes.json /
27
+ * RepoMeshPolicy). A repo can declare `providerDefaults` freely; a machine that
28
+ * has delegatedWorkerAutoApprove=false still gets no auto-approve, and a dangerous
29
+ * requested mode is still downgraded to PTY parsing unless the machine opts in.
30
+ * The requested mode ID is validated at runtime against the provider spec — an
31
+ * unknown mode ID is IGNORED (fall back to the provider's own default), never
32
+ * silently coerced into a dangerous mode.
33
+ *
22
34
  * The coordinator/operatingNotes merge is **in-memory only**: the on-disk
23
35
  * machine-local `meshes.json` is never mutated by this module.
24
36
  *
@@ -60,6 +72,19 @@ export interface RepoMeshDeclarativeLimits {
60
72
  maxNotes?: number;
61
73
  }
62
74
 
75
+ /**
76
+ * Repo-shared declarative per-provider defaults. NOT policy — see the file header.
77
+ * `autoApproveModes` maps a providerType (e.g. "claude-cli") to the auto-approve
78
+ * mode ID that a delegated worker of that type should REQUEST when it is
79
+ * auto-approved. The map only influences WHICH mode is used, never WHETHER
80
+ * auto-approve is enabled (that stays machine-local). Mode IDs are validated
81
+ * against the live provider spec at resolve time; an unknown ID is ignored.
82
+ */
83
+ export interface RepoMeshDeclarativeProviderDefaults {
84
+ /** providerType → requested auto-approve mode ID. */
85
+ autoApproveModes?: Record<string, string>;
86
+ }
87
+
63
88
  /**
64
89
  * Parsed + normalized `.adhdev/mesh.json` shape. Every field is optional except
65
90
  * version so a repo can declare only the zone(s) it cares about. Policy is NOT a
@@ -70,6 +95,8 @@ export interface RepoMeshDeclarativeConfig {
70
95
  coordinator?: RepoMeshDeclarativeCoordinatorConfig;
71
96
  operatingNotes?: CoordinatorOperatingNote[];
72
97
  limits?: RepoMeshDeclarativeLimits;
98
+ /** Repo-shared per-provider defaults (requested auto-approve mode). NOT policy. */
99
+ providerDefaults?: RepoMeshDeclarativeProviderDefaults;
73
100
  }
74
101
 
75
102
  export interface RepoMeshJsonConfigLoadResult {
@@ -132,6 +159,20 @@ export const MESH_JSON_CONFIG_SCHEMA = {
132
159
  maxNotes: { type: 'number', minimum: 1 },
133
160
  },
134
161
  },
162
+ providerDefaults: {
163
+ type: 'object',
164
+ additionalProperties: false,
165
+ properties: {
166
+ // providerType → requested auto-approve mode ID. Mode IDs are
167
+ // validated against the live provider spec at resolve time; an
168
+ // unknown ID is ignored (provider default is used), so the schema
169
+ // only constrains the shape (string→string), not the ID values.
170
+ autoApproveModes: {
171
+ type: 'object',
172
+ additionalProperties: { type: 'string' },
173
+ },
174
+ },
175
+ },
135
176
  },
136
177
  } as const;
137
178
 
@@ -226,6 +267,38 @@ export function normalizeRepoMeshDeclarativeConfig(parsed: unknown): {
226
267
  }
227
268
  }
228
269
 
270
+ // providerDefaults: shape-only normalization. We keep every string→string
271
+ // (providerType → requested mode ID) entry after trimming; we deliberately do
272
+ // NOT validate the mode IDs against any provider spec here — the normalizer has
273
+ // no provider registry, and (crucially) validity depends on the LIVE provider
274
+ // at resolve time. The runtime resolver (resolveDelegatedWorkerAutoApprove)
275
+ // checks the requested ID against provider.autoApproveModes.modes and falls
276
+ // back to the provider's own default when the ID is unknown. This keeps the
277
+ // config fail-closed: a stale/typo'd mode ID never coerces into a dangerous
278
+ // mode, it just means "no repo override → provider default".
279
+ if (parsed.providerDefaults !== undefined) {
280
+ if (isRecord(parsed.providerDefaults)) {
281
+ const pd: RepoMeshDeclarativeProviderDefaults = {};
282
+ const rawModes = (parsed.providerDefaults as Record<string, unknown>).autoApproveModes;
283
+ if (rawModes !== undefined) {
284
+ if (isRecord(rawModes)) {
285
+ const modes: Record<string, string> = {};
286
+ for (const [providerType, modeId] of Object.entries(rawModes)) {
287
+ const type = typeof providerType === 'string' ? providerType.trim() : '';
288
+ const id = typeof modeId === 'string' ? modeId.trim() : '';
289
+ if (type && id) modes[type] = id;
290
+ }
291
+ if (Object.keys(modes).length) pd.autoApproveModes = modes;
292
+ } else {
293
+ errors.push('providerDefaults.autoApproveModes must be an object when provided');
294
+ }
295
+ }
296
+ if (Object.keys(pd).length) config.providerDefaults = pd;
297
+ } else {
298
+ errors.push('providerDefaults must be an object when provided');
299
+ }
300
+ }
301
+
229
302
  return { valid: true, config, errors };
230
303
  }
231
304
 
@@ -364,6 +437,21 @@ export function applyRepoMeshConfig<T extends Pick<LocalMeshEntry, 'coordinator'
364
437
  * exported — it is machine-local only and has no place in mesh.json. Operating
365
438
  * notes are intentionally NOT exported either: those are runtime ledger lessons,
366
439
  * and a repo should declare baseline notes deliberately.
440
+ *
441
+ * `providerDefaults` is NOT auto-exported from the machine-local mesh (there is no
442
+ * machine-local source for it — it is a repo-authored declaration). To help an
443
+ * operator hand-author it, the scaffold carries a commented example shape:
444
+ *
445
+ * "providerDefaults": {
446
+ * "autoApproveModes": {
447
+ * "claude-cli": "accept-edits", // requested mode WHEN auto-approve is on
448
+ * "codex-cli": "auto" // (enable/dangerous opt-in stays machine-local)
449
+ * }
450
+ * }
451
+ *
452
+ * The example is surfaced via the scaffold's `_providerDefaultsExample` hint (JSON
453
+ * has no comments) rather than a live `providerDefaults` value, so serializing the
454
+ * scaffold never writes an unwanted requested-mode map into the repo file.
367
455
  */
368
456
  export function buildMeshJsonConfigScaffold(
369
457
  mesh: Pick<LocalMeshEntry, 'coordinator'>,
@@ -378,6 +466,21 @@ export function buildMeshJsonConfigScaffold(
378
466
  return scaffold;
379
467
  }
380
468
 
469
+ /**
470
+ * A copy-paste example of the `providerDefaults` zone for operators hand-authoring
471
+ * a repo `.adhdev/mesh.json`. Returned by the export/write commands as a separate
472
+ * hint field (never merged into the serialized scaffold) so the repo file stays
473
+ * clean. The mode IDs shown are illustrative — a repo should use IDs that exist in
474
+ * its own providers' specs; an unknown ID is ignored at resolve time and the
475
+ * provider's own default is used.
476
+ */
477
+ export const MESH_JSON_PROVIDER_DEFAULTS_EXAMPLE: RepoMeshDeclarativeProviderDefaults = {
478
+ autoApproveModes: {
479
+ 'claude-cli': 'accept-edits',
480
+ 'codex-cli': 'auto',
481
+ },
482
+ };
483
+
381
484
  /** Serialize a scaffold to the canonical 2-space JSON draft text. */
382
485
  export function serializeMeshJsonConfigScaffold(config: RepoMeshDeclarativeConfig): string {
383
486
  return JSON.stringify(config, null, 2);
package/src/index.ts CHANGED
@@ -143,6 +143,8 @@ export type {
143
143
  export {
144
144
  DEFAULT_MESH_POLICY,
145
145
  resolveDelegatedWorkerAutoApprove,
146
+ delegatedWorkerAutoApproveSettings,
147
+ resolveDelegatedWorkerDangerousModeAllow,
146
148
  resolveAllowSendKeysDestructive,
147
149
  resolveMagiSessionCleanupMode,
148
150
  magiAutoLaunchedSessionCleanupDecision,
@@ -161,6 +163,24 @@ export {
161
163
  resolveAutoConvergeCodeChange,
162
164
  } from './repo-mesh-types.js';
163
165
 
166
+ // ── Repo-shared declarative mesh config (.adhdev/mesh.json) ──
167
+ export {
168
+ loadRepoMeshJsonConfig,
169
+ normalizeRepoMeshDeclarativeConfig,
170
+ buildMeshJsonConfigScaffold,
171
+ serializeMeshJsonConfigScaffold,
172
+ MESH_JSON_CONFIG_LOCATIONS,
173
+ MESH_JSON_CONFIG_SCHEMA,
174
+ MESH_JSON_PROVIDER_DEFAULTS_EXAMPLE,
175
+ } from './config/mesh-json-config.js';
176
+ export type {
177
+ RepoMeshDeclarativeConfig,
178
+ RepoMeshDeclarativeCoordinatorConfig,
179
+ RepoMeshDeclarativeLimits,
180
+ RepoMeshDeclarativeProviderDefaults,
181
+ RepoMeshJsonConfigLoadResult,
182
+ } from './config/mesh-json-config.js';
183
+
164
184
  // ── Git Surface ──
165
185
  export * from './git/index.js';
166
186
 
@@ -489,7 +509,7 @@ export { ProviderInstanceManager } from './providers/provider-instance-manager.j
489
509
  export { IdeProviderInstance } from './providers/ide-provider-instance.js';
490
510
  export { CliProviderInstance } from './providers/cli-provider-instance.js';
491
511
  export { AcpProviderInstance } from './providers/acp-provider-instance.js';
492
- export type { ProviderModule, CdpTargetFilter, ProviderResumeCapability, InputEnvelope, InputPart, MessagePart, ReadChatTurnStatus, ControlListResult, ControlSetResult, ControlInvokeResult } from './providers/contracts.js';
512
+ export type { ProviderModule, AutoApproveMode, AutoApproveModesConfig, AutoApproveModeRisk, AutoApproveModeStrategy, CdpTargetFilter, ProviderResumeCapability, InputEnvelope, InputPart, MessagePart, ReadChatTurnStatus, ControlListResult, ControlSetResult, ControlInvokeResult } from './providers/contracts.js';
493
513
  export type { ProviderSourceConfigSnapshot, ProviderSourceConfigUpdate } from './config/provider-source-config.js';
494
514
  export { parseProviderSourceConfigUpdate } from './config/provider-source-config.js';
495
515
  export { normalizeInputEnvelope, normalizeMessageParts, flattenMessageParts } from './providers/io-contracts.js';