@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.
- package/dist/commands/cli-manager.d.ts +9 -0
- package/dist/config/mesh-json-config.d.ts +62 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +919 -421
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +880 -391
- package/dist/index.mjs.map +1 -1
- package/dist/providers/auto-approve-modes.d.ts +14 -0
- package/dist/providers/cli-provider-instance.d.ts +4 -0
- package/dist/providers/contracts.d.ts +17 -0
- package/dist/providers/provider-schema.d.ts +1 -0
- package/dist/repo-mesh-types.d.ts +38 -6
- package/dist/shared-types.d.ts +3 -1
- package/package.json +3 -3
- package/src/commands/cli-manager.ts +54 -8
- package/src/commands/med-family/mesh-crud.ts +155 -0
- package/src/config/mesh-json-config.ts +103 -0
- package/src/index.ts +21 -1
- package/src/mesh/mesh-event-forwarding.ts +18 -2
- package/src/mesh/mesh-queue-assignment.ts +66 -9
- package/src/providers/auto-approve-modes.ts +97 -0
- package/src/providers/cli-provider-instance.ts +45 -13
- package/src/providers/contracts.ts +25 -1
- package/src/providers/provider-schema.ts +83 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +44 -0
- package/src/repo-mesh-types.ts +112 -12
- package/src/shared-types.ts +3 -1
- package/src/status/snapshot.ts +2 -0
|
@@ -16,7 +16,8 @@ import { resolveMeshHostStatus } from './mesh-host-ownership.js';
|
|
|
16
16
|
import { enqueueUnresolvedDelegateForward, nudgeUnresolvedForwardRetry } from './mesh-unresolved-forward-outbox.js';
|
|
17
17
|
import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
|
|
18
18
|
import { getLastDisplayMessage } from '../status/snapshot.js';
|
|
19
|
-
import {
|
|
19
|
+
import { delegatedWorkerAutoApproveSettings } from '../repo-mesh-types.js';
|
|
20
|
+
import { loadRepoMeshJsonConfig } from '../config/mesh-json-config.js';
|
|
20
21
|
import { meshNodeIdMatches, daemonIdsEquivalent, expandDaemonIdForms, sessionIdsEquivalent, withStatusProbeMarker, type MeshNodeIdentified } from '@adhdev/mesh-shared';
|
|
21
22
|
import {
|
|
22
23
|
findRecentTerminalLedgerEvidence,
|
|
@@ -1614,7 +1615,22 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1614
1615
|
spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || 'hidden',
|
|
1615
1616
|
// Coordinator-dispatched recovery relaunch: same auto-approve
|
|
1616
1617
|
// policy as the primary worker launch path.
|
|
1617
|
-
|
|
1618
|
+
...delegatedWorkerAutoApproveSettings(
|
|
1619
|
+
mesh?.policy,
|
|
1620
|
+
node?.policy,
|
|
1621
|
+
components.providerLoader?.getMeta(recoveryContext.failedProviderType),
|
|
1622
|
+
// Recovery relaunch: same repo-declared requested mode as the
|
|
1623
|
+
// primary path. node.workspace missing → null → provider default.
|
|
1624
|
+
(() => {
|
|
1625
|
+
const ws = typeof node?.workspace === 'string' && node.workspace.trim() ? node.workspace.trim() : '';
|
|
1626
|
+
if (!ws) return null;
|
|
1627
|
+
try {
|
|
1628
|
+
const r = loadRepoMeshJsonConfig(ws);
|
|
1629
|
+
return r.sourceType === 'repo_file' && r.config ? r.config : null;
|
|
1630
|
+
} catch { return null; }
|
|
1631
|
+
})(),
|
|
1632
|
+
recoveryContext.failedProviderType,
|
|
1633
|
+
),
|
|
1618
1634
|
launchedByCoordinator: true,
|
|
1619
1635
|
}
|
|
1620
1636
|
}).catch((e: any) => LOG.error('MeshRecovery', `Failed to auto-relaunch session for ${node.id}: ${e?.message}`));
|
|
@@ -13,7 +13,9 @@ import { createSessionDelivery, updateSessionDeliveryStatus } from './mesh-deliv
|
|
|
13
13
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
14
14
|
import { traceMeshEventDrop } from './mesh-event-trace.js';
|
|
15
15
|
import { awaitWithWarmupDeadline, resolveWarmupDeadlineOpts } from './mesh-warmup-deadline.js';
|
|
16
|
-
import {
|
|
16
|
+
import { delegatedWorkerAutoApproveSettings, resolveProviderMaxParallel, resolveNodeSchedulingPriority, normalizeMeshSchedulingStrategy, resolveMaxParallelTasks, resolveMaxReadonlyParallelTasks, resolveCoordinatorIdlePushPolicy } from '../repo-mesh-types.js';
|
|
17
|
+
import { loadRepoMeshJsonConfig } from '../config/mesh-json-config.js';
|
|
18
|
+
import type { RepoMeshDeclarativeConfig } from '../config/mesh-json-config.js';
|
|
17
19
|
import type { RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
|
|
18
20
|
import { normalizeMeshNodeId, meshNodeIdMatches, daemonIdsEquivalent, canonicalDaemonId, expandDaemonIdForms, normalizeMeshWorkspaceForCompare, meshWorkspacesEquivalent, sessionIdsEquivalent, normalizeNodeCapabilitySlots, isMeshTaskDifficulty, withStatusProbeMarker, type MeshNodeIdentified, type NodeCapabilitySlot, type MeshTaskDifficulty } from '@adhdev/mesh-shared';
|
|
19
21
|
import { resolveNodeCapabilitySlots } from './mesh-node-slots.js';
|
|
@@ -90,6 +92,25 @@ export function __resetIdleAutoFastForwardForTests(): void {
|
|
|
90
92
|
autoFastForwardWorkspaceLease.clear();
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Load the repo-shared `.adhdev/mesh.json` for a node's workspace, tolerating a
|
|
97
|
+
* missing/invalid file (returns null → resolver falls back to provider-spec
|
|
98
|
+
* defaults, i.e. exactly the pre-providerDefaults behavior). Only the
|
|
99
|
+
* `providerDefaults` zone influences the delegated-worker MODE selection; it never
|
|
100
|
+
* touches the ENABLE decision. When a node carries no workspace path (should not
|
|
101
|
+
* happen for a launchable node, but be defensive), we skip the read entirely.
|
|
102
|
+
*/
|
|
103
|
+
function loadRepoConfigForNode(node: any): RepoMeshDeclarativeConfig | null {
|
|
104
|
+
const workspace = typeof node?.workspace === 'string' && node.workspace.trim() ? node.workspace.trim() : '';
|
|
105
|
+
if (!workspace) return null;
|
|
106
|
+
try {
|
|
107
|
+
const result = loadRepoMeshJsonConfig(workspace);
|
|
108
|
+
return result.sourceType === 'repo_file' && result.config ? result.config : null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
93
114
|
export function getMeshWithCache(components: DaemonComponents, meshId: string): any | undefined {
|
|
94
115
|
const localMesh = getMesh(meshId);
|
|
95
116
|
const cachedMesh = components.router?.getCachedInlineMesh(meshId);
|
|
@@ -690,7 +711,7 @@ export function tryAssignQueueTask(
|
|
|
690
711
|
if (inst && typeof inst.updateSettings === 'function') {
|
|
691
712
|
// Adopting a (possibly manually-opened) local session as a worker: apply the
|
|
692
713
|
// delegated-worker auto-approve policy here too, so a session that was launched
|
|
693
|
-
// without
|
|
714
|
+
// without an auto-approve boolean/mode still resolves the delegated policy once dispatched
|
|
694
715
|
// to it (the "approval notification fires only for certain delegated sessions"
|
|
695
716
|
// case). updateSettings preserves runtime mesh keys; passing autoApprove keeps it.
|
|
696
717
|
//
|
|
@@ -704,7 +725,13 @@ export function tryAssignQueueTask(
|
|
|
704
725
|
meshNodeFor: meshId,
|
|
705
726
|
meshNodeId: nodeId,
|
|
706
727
|
launchedByCoordinator: true,
|
|
707
|
-
|
|
728
|
+
...delegatedWorkerAutoApproveSettings(
|
|
729
|
+
mesh?.policy,
|
|
730
|
+
node?.policy,
|
|
731
|
+
components.providerLoader?.getMeta(providerType),
|
|
732
|
+
loadRepoConfigForNode(node),
|
|
733
|
+
providerType,
|
|
734
|
+
),
|
|
708
735
|
...(localDaemonId ? { meshCoordinatorDaemonId: localDaemonId } : {}),
|
|
709
736
|
// COMPLETION-PROPAGATION F5: (re)stamp the coordinator SESSION anchor from THIS
|
|
710
737
|
// task's sourceCoordinatorSessionId with PRIORITY — a manually-launched (or reused)
|
|
@@ -1694,8 +1721,20 @@ function liveSessionCountForNode(components: DaemonComponents, meshId: string, n
|
|
|
1694
1721
|
* free claimer, and is excluded — so a read-only auto-launch onto a busy-but-no-idle node
|
|
1695
1722
|
* is still allowed. A node with NO live mesh session at all (dead, or never launched) does
|
|
1696
1723
|
* not match, preserving the legitimate first-session spawn.
|
|
1724
|
+
*
|
|
1725
|
+
* PROVIDER-MATCH gate (DISPATCH-DEADLOCK-PROVIDER-MISMATCH): a live session only suppresses
|
|
1726
|
+
* this launch if it could ACTUALLY claim THIS task. A session whose providerType does not
|
|
1727
|
+
* satisfy the task's requiredTags (e.g. a claude-cli coordinator/worker session on a node,
|
|
1728
|
+
* while the pending task is required_tags: [provider=codex-cli]) is NOT a pending claimer for
|
|
1729
|
+
* this task — claimNextQueueTask's nodeSatisfiesRequiredTags gate would reject its claim. Left
|
|
1730
|
+
* unchecked, such a mismatched session made this gate return true for a task it can never claim,
|
|
1731
|
+
* so the required-provider worker never auto-launched AND no session could claim → nobody made
|
|
1732
|
+
* progress → permanent silent deadlock. Mirror the claim path: build the session's own
|
|
1733
|
+
* capability tags (its providerType pinned onto the node) and only count it as a pending claimer
|
|
1734
|
+
* when those tags satisfy task.requiredTags. `node` is passed in so the tag set reflects this
|
|
1735
|
+
* node's os/arch/worktree/converge context, matching claimNextQueueTask exactly.
|
|
1697
1736
|
*/
|
|
1698
|
-
function nodeHasLiveSessionPendingClaim(components: DaemonComponents, meshId: string, nodeId: string): boolean {
|
|
1737
|
+
function nodeHasLiveSessionPendingClaim(components: DaemonComponents, meshId: string, nodeId: string, task: MeshWorkQueueEntry, node: any): boolean {
|
|
1699
1738
|
// (A) AUTOLAUNCH-CLAIM-CHURN remote-awareness: a task whose auto-launch record targets this
|
|
1700
1739
|
// node and is still inside its await-claim window (base or backoff) already has a session on
|
|
1701
1740
|
// its way to claim — even when that session is REMOTE and thus invisible to the local
|
|
@@ -1734,7 +1773,20 @@ function nodeHasLiveSessionPendingClaim(components: DaemonComponents, meshId: st
|
|
|
1734
1773
|
if (isTerminalSessionStatus(status)) return false; // dead → no claimer here, allow launch
|
|
1735
1774
|
const sessionId = readNonEmptyString(state.instanceId);
|
|
1736
1775
|
if (sessionId && busySessionIds.has(sessionId)) return false; // busy with its own assigned task
|
|
1737
|
-
|
|
1776
|
+
// PROVIDER-MATCH gate (DISPATCH-DEADLOCK-PROVIDER-MISMATCH): only count this session as a
|
|
1777
|
+
// pending claimer if its own provider could satisfy THIS task's requiredTags. A session
|
|
1778
|
+
// whose providerType does not match (e.g. a claude-cli session while task requires
|
|
1779
|
+
// provider=codex-cli) can never claim this task via claimNextQueueTask's
|
|
1780
|
+
// nodeSatisfiesRequiredTags gate, so it must not suppress the required-provider launch —
|
|
1781
|
+
// otherwise the task deadlocks (mismatched session blocks launch, yet cannot claim). Mirror
|
|
1782
|
+
// the claim path: pin the session's providerType onto this node and check the tags.
|
|
1783
|
+
if (task.requiredTags?.length) {
|
|
1784
|
+
const sessionProviderType = state.type || readNonEmptyString(settings.providerType);
|
|
1785
|
+
if (sessionProviderType && !nodeSatisfiesRequiredTags(task.requiredTags, buildMeshNodeCapabilityTags(node, sessionProviderType))) {
|
|
1786
|
+
return false; // provider mismatch → this session can't claim this task; not a pending claimer
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return true; // live + unassigned + provider-capable → will claim the pending task itself
|
|
1738
1790
|
});
|
|
1739
1791
|
}
|
|
1740
1792
|
|
|
@@ -2246,7 +2298,7 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2246
2298
|
// busy-but-no-idle node is still allowed. Skip with a transient (non-actionable)
|
|
2247
2299
|
// reason so the coordinator is not paged; the 4s reconcile retries, and once the
|
|
2248
2300
|
// existing session goes terminal this gate clears and a legitimate launch proceeds.
|
|
2249
|
-
if (nodeHasLiveSessionPendingClaim(components, meshId, nodeId)) {
|
|
2301
|
+
if (nodeHasLiveSessionPendingClaim(components, meshId, nodeId, task, node)) {
|
|
2250
2302
|
markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'node_has_live_session_pending_claim', nodeId });
|
|
2251
2303
|
continue;
|
|
2252
2304
|
}
|
|
@@ -2318,8 +2370,14 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2318
2370
|
spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || 'hidden',
|
|
2319
2371
|
// Coordinator-dispatched worker: auto-approve unless mesh/node policy
|
|
2320
2372
|
// opts out (default true). Lands in settingsOverride and beats the
|
|
2321
|
-
// global per-provider-type
|
|
2322
|
-
|
|
2373
|
+
// global per-provider-type boolean/mode through explicit opposite-key clearing.
|
|
2374
|
+
...delegatedWorkerAutoApproveSettings(
|
|
2375
|
+
mesh?.policy,
|
|
2376
|
+
node?.policy,
|
|
2377
|
+
components.providerLoader?.getMeta(resolved.providerType),
|
|
2378
|
+
loadRepoConfigForNode(node),
|
|
2379
|
+
resolved.providerType,
|
|
2380
|
+
),
|
|
2323
2381
|
launchedByCoordinator: true,
|
|
2324
2382
|
autoLaunchedForQueueTaskId: task.id,
|
|
2325
2383
|
};
|
|
@@ -2984,4 +3042,3 @@ export function runIdleMaintenanceThenAssignQueue(components: DaemonComponents,
|
|
|
2984
3042
|
});
|
|
2985
3043
|
});
|
|
2986
3044
|
}
|
|
2987
|
-
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AutoApproveMode,
|
|
3
|
+
AutoApproveModeRisk,
|
|
4
|
+
AutoApproveModeStrategy,
|
|
5
|
+
ProviderModule,
|
|
6
|
+
} from './contracts.js';
|
|
7
|
+
|
|
8
|
+
const KNOWN_DANGEROUS_LAUNCH_ARGS = new Set([
|
|
9
|
+
'--dangerously-skip-permissions',
|
|
10
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
11
|
+
'bypassPermissions',
|
|
12
|
+
'sandbox_mode=danger-full-access',
|
|
13
|
+
'approval_policy=never',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export interface ResolvedAutoApproveMode {
|
|
17
|
+
active: boolean;
|
|
18
|
+
strategy: AutoApproveModeStrategy;
|
|
19
|
+
modeId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isKnownDangerousLaunchArg(arg: string): boolean {
|
|
23
|
+
const normalized = arg.trim();
|
|
24
|
+
if (KNOWN_DANGEROUS_LAUNCH_ARGS.has(normalized)) return true;
|
|
25
|
+
return normalized.startsWith('--dangerously-skip-permissions=')
|
|
26
|
+
|| normalized.startsWith('--dangerously-bypass-approvals-and-sandbox=');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Runtime defense-in-depth for provider definitions that bypass schema validation. */
|
|
30
|
+
export function deriveAutoApproveModeRisk(mode: Pick<AutoApproveMode, 'risk' | 'launchArgs'>): AutoApproveModeRisk {
|
|
31
|
+
return Array.isArray(mode.launchArgs) && mode.launchArgs.some(isKnownDangerousLaunchArg)
|
|
32
|
+
? 'dangerous'
|
|
33
|
+
: mode.risk;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function inactiveMode(modeId = ''): ResolvedAutoApproveMode {
|
|
37
|
+
return { active: false, strategy: 'pty-parse-default', modeId };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveConfiguredMode(
|
|
41
|
+
provider: ProviderModule,
|
|
42
|
+
mode: AutoApproveMode,
|
|
43
|
+
settings: Record<string, unknown> | undefined,
|
|
44
|
+
): ResolvedAutoApproveMode {
|
|
45
|
+
if (mode.strategy === 'post-boot-command') return inactiveMode(mode.id);
|
|
46
|
+
if (settings?.launchedByCoordinator === true
|
|
47
|
+
&& settings.delegatedWorkerDangerousModeAllow !== true
|
|
48
|
+
&& deriveAutoApproveModeRisk(mode) === 'dangerous') {
|
|
49
|
+
const fallback = provider.autoApproveModes?.modes.find((candidate) =>
|
|
50
|
+
candidate.strategy === 'pty-parse-default'
|
|
51
|
+
&& deriveAutoApproveModeRisk(candidate) !== 'dangerous');
|
|
52
|
+
return fallback
|
|
53
|
+
? { active: true, strategy: fallback.strategy, modeId: fallback.id }
|
|
54
|
+
: inactiveMode(mode.id);
|
|
55
|
+
}
|
|
56
|
+
return { active: true, strategy: mode.strategy, modeId: mode.id };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve new mode settings before the legacy boolean. A stale/unknown explicit
|
|
61
|
+
* mode id fails closed instead of falling through to an enabled legacy setting.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveProviderAutoApproveMode(
|
|
64
|
+
provider: ProviderModule,
|
|
65
|
+
settings: Record<string, unknown> | undefined,
|
|
66
|
+
): ResolvedAutoApproveMode {
|
|
67
|
+
const config = provider.autoApproveModes;
|
|
68
|
+
const explicitModeId = settings?.autoApproveMode;
|
|
69
|
+
if (typeof explicitModeId === 'string') {
|
|
70
|
+
const mode = config?.modes.find((candidate) => candidate.id === explicitModeId);
|
|
71
|
+
if (!mode) return inactiveMode(explicitModeId);
|
|
72
|
+
return resolveConfiguredMode(provider, mode, settings);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const explicitLegacy = settings?.autoApprove;
|
|
76
|
+
const providerLegacyDefault = provider.settings?.autoApprove?.default;
|
|
77
|
+
const legacyActive = typeof explicitLegacy === 'boolean'
|
|
78
|
+
? explicitLegacy
|
|
79
|
+
: typeof providerLegacyDefault === 'boolean'
|
|
80
|
+
? providerLegacyDefault
|
|
81
|
+
: false;
|
|
82
|
+
if (!legacyActive) return inactiveMode();
|
|
83
|
+
|
|
84
|
+
if (!config) {
|
|
85
|
+
return { active: true, strategy: 'pty-parse-default', modeId: 'legacy' };
|
|
86
|
+
}
|
|
87
|
+
const defaultMode = config.modes.find((mode) => mode.id === config.default);
|
|
88
|
+
if (!defaultMode) return inactiveMode(config.default);
|
|
89
|
+
return resolveConfiguredMode(provider, defaultMode, settings);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function findProviderAutoApproveMode(
|
|
93
|
+
provider: ProviderModule | undefined,
|
|
94
|
+
modeId: string,
|
|
95
|
+
): AutoApproveMode | undefined {
|
|
96
|
+
return provider?.autoApproveModes?.modes.find((mode) => mode.id === modeId);
|
|
97
|
+
}
|
|
@@ -75,6 +75,7 @@ import type {
|
|
|
75
75
|
} from './cli-provider-instance-types.js';
|
|
76
76
|
import { mergeConversationMessages, buildExternalTranscriptProbe } from './cli-provider-transcript-merge.js';
|
|
77
77
|
import { getEffectDedupKey, formatApprovalRequestMessage, formatMarkerTimestamp } from './cli-provider-effect-format.js';
|
|
78
|
+
import { resolveProviderAutoApproveMode, type ResolvedAutoApproveMode } from './auto-approve-modes.js';
|
|
78
79
|
|
|
79
80
|
// Re-export moved public symbols so existing importers (index.ts, tests) keep
|
|
80
81
|
// their `./cli-provider-instance.js` path. Pure move — no behavior change.
|
|
@@ -2780,7 +2781,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2780
2781
|
* resolveModal, so a plain turn that never saw an approval always returns false.
|
|
2781
2782
|
*/
|
|
2782
2783
|
private inApprovalResumeGrace(now = Date.now()): boolean {
|
|
2783
|
-
if (!this.isAutonomousMeshSession() || !this.
|
|
2784
|
+
if (!this.isAutonomousMeshSession() || !this.shouldUsePtyAutoApprove()) return false;
|
|
2784
2785
|
const resolvedAt = typeof (this.adapter as any)?.lastApprovalResolvedAt === 'number'
|
|
2785
2786
|
? (this.adapter as any).lastApprovalResolvedAt as number
|
|
2786
2787
|
: 0;
|
|
@@ -2897,7 +2898,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2897
2898
|
}
|
|
2898
2899
|
|
|
2899
2900
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
2900
|
-
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.
|
|
2901
|
+
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldUsePtyAutoApprove();
|
|
2901
2902
|
const latestVisibleStatus = latestAutoApproveActive || this.autoApproveBusy ? 'generating' : latestStatus.status;
|
|
2902
2903
|
LOG.debug('CLI', `[${this.type}] flush attempt: adapterStatus=${latestStatus.status} latestVisible=${latestVisibleStatus} generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse} hasPartial=${!!this.adapter.getPartialResponse?.()}`);
|
|
2903
2904
|
if (latestVisibleStatus !== 'idle') {
|
|
@@ -3300,7 +3301,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3300
3301
|
private stabilizeFlappingApprovalStatus(adapterStatus: any, now = Date.now()): any {
|
|
3301
3302
|
// Only autonomous auto-approving mesh sessions are subject to the delegated flap;
|
|
3302
3303
|
// never overlay for attended/foreground/non-mesh sessions.
|
|
3303
|
-
if (!this.isAutonomousMeshSession() || !this.
|
|
3304
|
+
if (!this.isAutonomousMeshSession() || !this.shouldUsePtyAutoApprove()) return adapterStatus;
|
|
3304
3305
|
|
|
3305
3306
|
const rawStatus = adapterStatus?.status;
|
|
3306
3307
|
const resolvedAt = typeof (this.adapter as any)?.lastApprovalResolvedAt === 'number'
|
|
@@ -3347,6 +3348,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3347
3348
|
}
|
|
3348
3349
|
|
|
3349
3350
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
3351
|
+
// launch-args modes grant approval in the CLI process itself and therefore
|
|
3352
|
+
// must never enter the PTY modal parser/fire/settle/mask/nudge subsystem.
|
|
3353
|
+
// post-boot-command is reserved and resolves inactive in v1.
|
|
3354
|
+
if (!this.shouldUsePtyAutoApprove()) {
|
|
3355
|
+
this.resetPtyAutoApproveState();
|
|
3356
|
+
return false;
|
|
3357
|
+
}
|
|
3350
3358
|
// Manual-attendance suppression (provider-common): when a human is
|
|
3351
3359
|
// Manual-attendance suppression (provider-common): when a human is
|
|
3352
3360
|
// actively driving this session from the dashboard, hold auto-approve so
|
|
@@ -3358,7 +3366,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3358
3366
|
// would otherwise never re-drive this decision. Background mesh workers
|
|
3359
3367
|
// are never attended, so their delegated auto-approve is untouched.
|
|
3360
3368
|
if (adapterStatus?.status === 'waiting_approval'
|
|
3361
|
-
&& this.
|
|
3369
|
+
&& this.shouldUsePtyAutoApprove()
|
|
3362
3370
|
&& this.manualAttendance.isAttended(now)) {
|
|
3363
3371
|
this.lastAutoApprovalSignature = '';
|
|
3364
3372
|
this.pendingAutoApprovalSignature = '';
|
|
@@ -3376,7 +3384,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3376
3384
|
}, this.manualAttendance.remainingMs(now) + 20);
|
|
3377
3385
|
return false;
|
|
3378
3386
|
}
|
|
3379
|
-
const autoApproveActive = adapterStatus?.status === 'waiting_approval' && this.
|
|
3387
|
+
const autoApproveActive = adapterStatus?.status === 'waiting_approval' && this.shouldUsePtyAutoApprove();
|
|
3380
3388
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
3381
3389
|
// times while the PTY absorbs the approval key. Without this flag, repeated
|
|
3382
3390
|
// snapshots would write stray keys into the input once the modal dismisses.
|
|
@@ -4652,15 +4660,37 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
4652
4660
|
get cliType(): string { return this.type; }
|
|
4653
4661
|
get cliName(): string { return this.provider.name; }
|
|
4654
4662
|
|
|
4663
|
+
private resolveAutoApproveMode(): ResolvedAutoApproveMode {
|
|
4664
|
+
return resolveProviderAutoApproveMode(this.provider, this.settings);
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
/** Legacy boolean view retained for internal/test compatibility. */
|
|
4655
4668
|
private shouldAutoApprove(): boolean {
|
|
4656
|
-
|
|
4657
|
-
|
|
4669
|
+
return this.resolveAutoApproveMode().active;
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
private shouldUsePtyAutoApprove(): boolean {
|
|
4673
|
+
const resolved = this.resolveAutoApproveMode();
|
|
4674
|
+
return this.shouldAutoApprove() && resolved.strategy === 'pty-parse-default';
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
private resetPtyAutoApproveState(): void {
|
|
4678
|
+
this.lastAutoApprovalSignature = '';
|
|
4679
|
+
this.pendingAutoApprovalSignature = '';
|
|
4680
|
+
this.pendingAutoApprovalSince = 0;
|
|
4681
|
+
this.autoApproveInactiveSince = 0;
|
|
4682
|
+
this.autoApproveMaskSince = 0;
|
|
4683
|
+
this.stalledApprovalNudgeEpisode = 0;
|
|
4684
|
+
this.autoApproveLastModalSeenAt = 0;
|
|
4685
|
+
this.autoApproveBusy = false;
|
|
4686
|
+
if (this.autoApproveSettleTimer) {
|
|
4687
|
+
clearTimeout(this.autoApproveSettleTimer);
|
|
4688
|
+
this.autoApproveSettleTimer = null;
|
|
4658
4689
|
}
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4690
|
+
if (this.autoApproveBusyTimer) {
|
|
4691
|
+
clearTimeout(this.autoApproveBusyTimer);
|
|
4692
|
+
this.autoApproveBusyTimer = null;
|
|
4662
4693
|
}
|
|
4663
|
-
return false;
|
|
4664
4694
|
}
|
|
4665
4695
|
|
|
4666
4696
|
/** @see ProviderInstance.noteManualInteraction */
|
|
@@ -4687,7 +4717,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
4687
4717
|
*/
|
|
4688
4718
|
private autoApproveEffectivelyActive(status: string | undefined, now = Date.now()): boolean {
|
|
4689
4719
|
return status === 'waiting_approval'
|
|
4690
|
-
&& this.
|
|
4720
|
+
&& this.shouldUsePtyAutoApprove()
|
|
4691
4721
|
&& !this.manualAttendance.isAttended(now);
|
|
4692
4722
|
}
|
|
4693
4723
|
|
|
@@ -4699,7 +4729,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
4699
4729
|
// maybeAutoApproveStatus (driven by getState + the recheck timer during a waiting episode);
|
|
4700
4730
|
// this read is side-effect-free so getStatusMetadata can consult it too.
|
|
4701
4731
|
private autoApproveMaskStalled(now = Date.now()): boolean {
|
|
4702
|
-
return this.
|
|
4732
|
+
return this.shouldUsePtyAutoApprove()
|
|
4733
|
+
&& this.autoApproveMaskSince > 0
|
|
4703
4734
|
&& now - this.autoApproveMaskSince > CliProviderInstance.AUTO_APPROVE_MASK_STALL_MS;
|
|
4704
4735
|
}
|
|
4705
4736
|
|
|
@@ -4725,6 +4756,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
4725
4756
|
* mask-clock value) so a modal that flaps between parsed/unparsed states is announced once.
|
|
4726
4757
|
*/
|
|
4727
4758
|
private maybeEmitStalledApprovalNudge(adapterStatus: any, now: number): void {
|
|
4759
|
+
if (!this.shouldUsePtyAutoApprove()) return;
|
|
4728
4760
|
if (!this.isMeshWorkerSession()) return;
|
|
4729
4761
|
if (adapterStatus?.status !== 'waiting_approval') return;
|
|
4730
4762
|
if (!this.autoApproveMaskStalled(now)) return;
|
|
@@ -488,6 +488,28 @@ export interface ProviderCompatibilityEntry {
|
|
|
488
488
|
scriptDir: string;
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
+
export type AutoApproveModeStrategy =
|
|
492
|
+
| 'pty-parse-default'
|
|
493
|
+
| 'launch-args'
|
|
494
|
+
| 'post-boot-command';
|
|
495
|
+
|
|
496
|
+
export type AutoApproveModeRisk = 'safe' | 'caution' | 'dangerous';
|
|
497
|
+
|
|
498
|
+
export interface AutoApproveMode {
|
|
499
|
+
id: string;
|
|
500
|
+
label: string;
|
|
501
|
+
strategy: AutoApproveModeStrategy;
|
|
502
|
+
risk: AutoApproveModeRisk;
|
|
503
|
+
warning?: string;
|
|
504
|
+
launchArgs?: string[];
|
|
505
|
+
removeArgs?: string[];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export interface AutoApproveModesConfig {
|
|
509
|
+
default: string;
|
|
510
|
+
modes: AutoApproveMode[];
|
|
511
|
+
}
|
|
512
|
+
|
|
491
513
|
export interface ProviderModule {
|
|
492
514
|
/** Unique identifier (e.g. 'cline', 'cursor', 'gemini-cli') */
|
|
493
515
|
type: string;
|
|
@@ -519,7 +541,9 @@ export interface ProviderModule {
|
|
|
519
541
|
status?: string;
|
|
520
542
|
/** Inventory/support detail string maintained in adhdev-providers */
|
|
521
543
|
details?: string;
|
|
522
|
-
|
|
544
|
+
/** Provider-specific auto-approve choices and their launch/runtime strategy. */
|
|
545
|
+
autoApproveModes?: AutoApproveModesConfig;
|
|
546
|
+
/** Install instructions (shown when command is missing) */
|
|
523
547
|
install?: string;
|
|
524
548
|
/** Custom version detection command (e.g. 'cursor --version', 'claude -v') */
|
|
525
549
|
versionCommand?: ProviderVersionCommand;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProviderControlDef, ProviderControlType, ProviderModule } from './contracts.js'
|
|
2
|
+
import { deriveAutoApproveModeRisk } from './auto-approve-modes.js'
|
|
2
3
|
import { providerHasOpenPanelSupport } from './open-panel-support.js'
|
|
3
4
|
|
|
4
5
|
const VALID_CAPABILITY_MEDIA_TYPES = new Set(['text', 'image', 'audio', 'video', 'resource'])
|
|
@@ -66,6 +67,7 @@ const KNOWN_PROVIDER_FIELDS = new Set<string>([
|
|
|
66
67
|
'providerVersion',
|
|
67
68
|
'status',
|
|
68
69
|
'details',
|
|
70
|
+
'autoApproveModes',
|
|
69
71
|
'modelLaunchArgs',
|
|
70
72
|
'modelOptions',
|
|
71
73
|
'thinkingLaunchArgs',
|
|
@@ -144,6 +146,7 @@ export function validateProviderDefinition(raw: unknown): ProviderValidationResu
|
|
|
144
146
|
validateCapabilities(provider as unknown as ProviderModule, controls, errors)
|
|
145
147
|
validateNativeHistory(provider.nativeHistory, errors)
|
|
146
148
|
validateMeshCoordinator(provider.meshCoordinator, errors)
|
|
149
|
+
validateAutoApproveModes(provider.autoApproveModes, errors)
|
|
147
150
|
|
|
148
151
|
for (const control of controls) {
|
|
149
152
|
validateControl(control as ProviderControlDef, errors)
|
|
@@ -160,6 +163,86 @@ export function validateProviderDefinition(raw: unknown): ProviderValidationResu
|
|
|
160
163
|
return { errors, warnings }
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
export function validateAutoApproveModes(raw: unknown, errors: string[]): void {
|
|
167
|
+
if (raw === undefined) return
|
|
168
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
169
|
+
errors.push('autoApproveModes must be an object')
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const config = raw as Record<string, unknown>
|
|
174
|
+
const defaultModeId = typeof config.default === 'string' ? config.default.trim() : ''
|
|
175
|
+
if (!defaultModeId) {
|
|
176
|
+
errors.push('autoApproveModes.default must be a non-empty string')
|
|
177
|
+
} else {
|
|
178
|
+
config.default = defaultModeId
|
|
179
|
+
}
|
|
180
|
+
if (!Array.isArray(config.modes) || config.modes.length === 0) {
|
|
181
|
+
errors.push('autoApproveModes.modes must be a non-empty array')
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ids = new Set<string>()
|
|
186
|
+
for (const [index, rawMode] of config.modes.entries()) {
|
|
187
|
+
const prefix = `autoApproveModes.modes[${index}]`
|
|
188
|
+
if (!rawMode || typeof rawMode !== 'object' || Array.isArray(rawMode)) {
|
|
189
|
+
errors.push(`${prefix} must be an object`)
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
const mode = rawMode as Record<string, unknown>
|
|
193
|
+
const id = typeof mode.id === 'string' ? mode.id.trim() : ''
|
|
194
|
+
if (!id) {
|
|
195
|
+
errors.push(`${prefix}.id must be a non-empty string`)
|
|
196
|
+
} else if (ids.has(id)) {
|
|
197
|
+
errors.push(`${prefix}.id must be unique (duplicate: ${id})`)
|
|
198
|
+
} else {
|
|
199
|
+
ids.add(id)
|
|
200
|
+
mode.id = id
|
|
201
|
+
}
|
|
202
|
+
if (typeof mode.label !== 'string' || !mode.label.trim()) {
|
|
203
|
+
errors.push(`${prefix}.label must be a non-empty string`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const strategy = mode.strategy
|
|
207
|
+
if (!['pty-parse-default', 'launch-args', 'post-boot-command'].includes(String(strategy))) {
|
|
208
|
+
errors.push(`${prefix}.strategy must be one of: pty-parse-default, launch-args, post-boot-command`)
|
|
209
|
+
} else if (strategy === 'post-boot-command') {
|
|
210
|
+
errors.push(`${prefix}.strategy post-boot-command is reserved and unsupported in v1`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const risk = mode.risk
|
|
214
|
+
if (!['safe', 'caution', 'dangerous'].includes(String(risk))) {
|
|
215
|
+
errors.push(`${prefix}.risk must be one of: safe, caution, dangerous`)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const field of ['launchArgs', 'removeArgs'] as const) {
|
|
219
|
+
const value = mode[field]
|
|
220
|
+
if (value !== undefined && (!Array.isArray(value) || value.some((arg) => typeof arg !== 'string' || !arg.trim()))) {
|
|
221
|
+
errors.push(`${prefix}.${field} must be an array of non-empty strings when provided`)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (strategy === 'launch-args' && (!Array.isArray(mode.launchArgs) || mode.launchArgs.length === 0)) {
|
|
225
|
+
errors.push(`${prefix}.launchArgs must be a non-empty array for launch-args strategy`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Mutate the validated provider object so downstream runtime/UI consumers see
|
|
229
|
+
// the effective risk. The runtime resolver repeats this derivation as defense-in-depth.
|
|
230
|
+
if (['safe', 'caution', 'dangerous'].includes(String(risk))
|
|
231
|
+
&& deriveAutoApproveModeRisk(mode as any) === 'dangerous') {
|
|
232
|
+
mode.risk = 'dangerous'
|
|
233
|
+
}
|
|
234
|
+
if (mode.risk === 'dangerous' && (typeof mode.warning !== 'string' || !mode.warning.trim())) {
|
|
235
|
+
errors.push(`${prefix}.warning is required for dangerous modes`)
|
|
236
|
+
} else if (mode.warning !== undefined && (typeof mode.warning !== 'string' || !mode.warning.trim())) {
|
|
237
|
+
errors.push(`${prefix}.warning must be a non-empty string when provided`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (defaultModeId && !ids.has(defaultModeId)) {
|
|
242
|
+
errors.push(`autoApproveModes.default must reference an existing mode id: ${defaultModeId}`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
163
246
|
function validateCapabilities(provider: ProviderModule, controls: ProviderControlDef[], errors: string[]): void {
|
|
164
247
|
const capabilities = provider.capabilities
|
|
165
248
|
if (provider.contractVersion === 2) {
|
|
@@ -119,6 +119,20 @@
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
},
|
|
122
|
+
"autoApproveModes": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"description": "Provider-specific auto-approve choices and their launch/runtime strategy.",
|
|
125
|
+
"required": ["default", "modes"],
|
|
126
|
+
"additionalProperties": false,
|
|
127
|
+
"properties": {
|
|
128
|
+
"default": { "type": "string", "minLength": 1 },
|
|
129
|
+
"modes": {
|
|
130
|
+
"type": "array",
|
|
131
|
+
"minItems": 1,
|
|
132
|
+
"items": { "$ref": "#/$defs/autoApproveMode" }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
122
136
|
"sendDelayMs": {
|
|
123
137
|
"type": "number",
|
|
124
138
|
"minimum": 0,
|
|
@@ -476,6 +490,36 @@
|
|
|
476
490
|
}
|
|
477
491
|
},
|
|
478
492
|
"$defs": {
|
|
493
|
+
"autoApproveMode": {
|
|
494
|
+
"type": "object",
|
|
495
|
+
"required": ["id", "label", "strategy", "risk"],
|
|
496
|
+
"additionalProperties": false,
|
|
497
|
+
"properties": {
|
|
498
|
+
"id": { "type": "string", "minLength": 1 },
|
|
499
|
+
"label": { "type": "string", "minLength": 1 },
|
|
500
|
+
"strategy": { "enum": ["pty-parse-default", "launch-args", "post-boot-command"] },
|
|
501
|
+
"risk": { "enum": ["safe", "caution", "dangerous"] },
|
|
502
|
+
"warning": { "type": "string", "minLength": 1 },
|
|
503
|
+
"launchArgs": {
|
|
504
|
+
"type": "array",
|
|
505
|
+
"items": { "type": "string", "minLength": 1 }
|
|
506
|
+
},
|
|
507
|
+
"removeArgs": {
|
|
508
|
+
"type": "array",
|
|
509
|
+
"items": { "type": "string", "minLength": 1 }
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
"allOf": [
|
|
513
|
+
{
|
|
514
|
+
"if": { "properties": { "strategy": { "const": "launch-args" } }, "required": ["strategy"] },
|
|
515
|
+
"then": { "required": ["launchArgs"], "properties": { "launchArgs": { "minItems": 1 } } }
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
"if": { "properties": { "risk": { "const": "dangerous" } }, "required": ["risk"] },
|
|
519
|
+
"then": { "required": ["warning"] }
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
},
|
|
479
523
|
"patternList": {
|
|
480
524
|
"type": "array",
|
|
481
525
|
"items": {
|