agent-relay-runner 0.91.4 → 0.92.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/package.json +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/adapters/claude.ts +19 -23
- package/src/adapters/codex.ts +23 -1
- package/src/claude-prompt-gates.ts +268 -0
- package/src/launch-assembly.ts +5 -10
- package/src/profile-home.ts +2 -14
- package/src/rate-limit.ts +0 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.0",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"directory": "runner"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"agent-relay-sdk": "0.2.
|
|
23
|
+
"agent-relay-sdk": "0.2.72"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/adapters/claude.ts
CHANGED
|
@@ -12,7 +12,8 @@ import type { SessionEvent } from "../session-insights";
|
|
|
12
12
|
import { prepareClaudeProfileHome, profileUsesClaudeHostProviderGlobals } from "../profile-home";
|
|
13
13
|
import { assembleLaunch, materializeLaunchAssembly, sessionStatusLineSettingsArgs } from "../launch-assembly";
|
|
14
14
|
import { claudeProviderMessageText } from "./claude-delivery";
|
|
15
|
-
import { buildRateLimitProviderState,
|
|
15
|
+
import { buildRateLimitProviderState, parseClaudeRateLimitPane } from "../rate-limit";
|
|
16
|
+
import { evaluateClaudePromptGatePane, initialClaudePromptGatePaneState, type ClaudePromptGatePaneState } from "../claude-prompt-gates";
|
|
16
17
|
|
|
17
18
|
export class ClaudeAdapter implements ProviderAdapter {
|
|
18
19
|
readonly provider = "claude";
|
|
@@ -24,10 +25,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
24
25
|
private tmuxWatcher?: Timer;
|
|
25
26
|
private turnWatcher?: Timer;
|
|
26
27
|
private modelUnavailableReported = false;
|
|
27
|
-
|
|
28
|
-
// it + emits the hold once (not every 2s tick). Cleared when the modal leaves the pane.
|
|
29
|
-
private rateLimitReported = false;
|
|
30
|
-
private rateLimitPaneDetections = 0;
|
|
28
|
+
private promptGatePaneState: ClaudePromptGatePaneState = initialClaudePromptGatePaneState();
|
|
31
29
|
|
|
32
30
|
onStatusChange(cb: (status: ProviderStatusUpdate) => void): void {
|
|
33
31
|
this.statusCb = cb;
|
|
@@ -309,6 +307,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
309
307
|
private async spawnHeadless(config: RunnerSpawnConfig, spawnArgs: SpawnArgs): Promise<ManagedProcess> {
|
|
310
308
|
const { sessionName, socketName, args: tmuxArgs } = this.buildTmuxArgs(config, spawnArgs);
|
|
311
309
|
this.modelUnavailableReported = false;
|
|
310
|
+
this.promptGatePaneState = initialClaudePromptGatePaneState();
|
|
312
311
|
|
|
313
312
|
Bun.spawnSync(tmuxCommand(socketName, "kill-session", "-t", sessionName), {
|
|
314
313
|
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
@@ -370,30 +369,27 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
370
369
|
this.statusCb(status);
|
|
371
370
|
return;
|
|
372
371
|
}
|
|
373
|
-
// #
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
this.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
this.
|
|
384
|
-
this.statusCb(rateLimit.status);
|
|
372
|
+
// #663: prompt-gate fallback. Known gates should be prevented before launch;
|
|
373
|
+
// this catches missed/new blocking panes with the same #655 debounce + busy
|
|
374
|
+
// guard used for rate-limit holds.
|
|
375
|
+
const promptGate = evaluateClaudePromptGatePane(pane, this.promptGatePaneState, {
|
|
376
|
+
sessionName,
|
|
377
|
+
busy: claudePaneIsBusy(pane),
|
|
378
|
+
});
|
|
379
|
+
this.promptGatePaneState = promptGate.state;
|
|
380
|
+
if (promptGate.action) {
|
|
381
|
+
this.sendPromptGateKeystroke(sessionName, promptGate.action.keystroke, socketName);
|
|
382
|
+
if (promptGate.status) this.statusCb(promptGate.status);
|
|
385
383
|
}
|
|
386
384
|
}, 2000);
|
|
387
385
|
}
|
|
388
386
|
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
// a failed send-keys must never wedge the pane watcher.
|
|
392
|
-
private dismissRateLimitModal(sessionName: string, socketName?: string): void {
|
|
387
|
+
// Best-effort: a failed auto-dismiss must not wedge the pane watcher.
|
|
388
|
+
private sendPromptGateKeystroke(sessionName: string, keystroke: string, socketName?: string): void {
|
|
393
389
|
try {
|
|
394
|
-
Bun.spawnSync(tmuxCommand(socketName, "send-keys", "-t", sessionName,
|
|
390
|
+
Bun.spawnSync(tmuxCommand(socketName, "send-keys", "-t", sessionName, keystroke), { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
395
391
|
} catch {
|
|
396
|
-
// ignore
|
|
392
|
+
// ignore; the timeline/status still publishes, and the next watcher tick can observe whether the pane moved.
|
|
397
393
|
}
|
|
398
394
|
}
|
|
399
395
|
|
package/src/adapters/codex.ts
CHANGED
|
@@ -101,7 +101,29 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
101
101
|
const client = process.meta?.client as CodexAppClient | undefined;
|
|
102
102
|
if (!client?.isConnected()) return "unknown";
|
|
103
103
|
const threadId = typeof process.meta?.threadId === "string" ? process.meta.threadId : "";
|
|
104
|
-
|
|
104
|
+
// #651: when no local activeTurnId, don't short-circuit to idle — query the app-server.
|
|
105
|
+
// turn/started can arrive after processing begins (or be missed entirely); a stale-idle
|
|
106
|
+
// probe would make a busy agent appear idle to the coordinator. Same fallback as #653's
|
|
107
|
+
// ensureMainTurnActive path, but for the pull (probe) direction, not the push (event) one.
|
|
108
|
+
if (!this.activeTurnId) {
|
|
109
|
+
if (!threadId) return "idle";
|
|
110
|
+
try {
|
|
111
|
+
const read = await client.threadRead(threadId, true);
|
|
112
|
+
const thread = isRecord(read.thread) ? read.thread : undefined;
|
|
113
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
114
|
+
const inProgressTurn = turns.find((t) => isRecord(t) && stringValue(t.status) === "inProgress");
|
|
115
|
+
if (inProgressTurn) {
|
|
116
|
+
// Synthesize the active turn (same path as ensureMainTurnActive on item events).
|
|
117
|
+
this.ensureMainTurnActive(stringValue(inProgressTurn.id));
|
|
118
|
+
return "busy";
|
|
119
|
+
}
|
|
120
|
+
const threadStatus = statusType(thread?.status);
|
|
121
|
+
if (threadStatus === "active") return "busy";
|
|
122
|
+
return "idle";
|
|
123
|
+
} catch {
|
|
124
|
+
return "unknown";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
105
127
|
if (!threadId) return "busy";
|
|
106
128
|
try {
|
|
107
129
|
const read = await client.threadRead(threadId, true);
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { isRecord } from "agent-relay-sdk";
|
|
3
|
+
import type { ProviderStatusUpdate, RunnerSpawnConfig } from "./adapter";
|
|
4
|
+
import { buildRateLimitProviderState, parseClaudeRateLimitPane } from "./rate-limit";
|
|
5
|
+
|
|
6
|
+
export interface ClaudeConfigPromptGatePrevention {
|
|
7
|
+
id: string;
|
|
8
|
+
description: string;
|
|
9
|
+
apply(seed: Record<string, unknown>, context: { host?: Record<string, unknown>; config: RunnerSpawnConfig }): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ClaudeLaunchPromptGatePrevention {
|
|
13
|
+
id: string;
|
|
14
|
+
description: string;
|
|
15
|
+
apply(config: RunnerSpawnConfig): { strictMcpConfig?: boolean };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PaneGateMatch = { message?: string; resetAt?: number; kind?: "usage_limit" | "transient_overload"; errorType?: string };
|
|
19
|
+
|
|
20
|
+
interface ClaudePanePromptGate {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
policy: string;
|
|
24
|
+
keystroke: string;
|
|
25
|
+
minDetections: number;
|
|
26
|
+
match(text: string, nowMs?: number): PaneGateMatch | null;
|
|
27
|
+
providerState?(match: PaneGateMatch, context: ClaudePromptGatePaneContext): Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ClaudePromptGatePaneState {
|
|
31
|
+
currentGateId?: string;
|
|
32
|
+
consecutiveDetections: number;
|
|
33
|
+
handledGateId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ClaudePromptGatePaneContext {
|
|
37
|
+
sessionName?: string;
|
|
38
|
+
busy?: boolean;
|
|
39
|
+
nowMs?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ClaudePromptGateAction {
|
|
43
|
+
gateId: string;
|
|
44
|
+
label: string;
|
|
45
|
+
policy: string;
|
|
46
|
+
keystroke: string;
|
|
47
|
+
source: "claude-pane";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ClaudePromptGatePaneResult {
|
|
51
|
+
action?: ClaudePromptGateAction;
|
|
52
|
+
status?: ProviderStatusUpdate;
|
|
53
|
+
state: ClaudePromptGatePaneState;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const claudeConfigPromptGatePreventions: ClaudeConfigPromptGatePrevention[] = [
|
|
57
|
+
{
|
|
58
|
+
id: "first-run-onboarding",
|
|
59
|
+
description: "Pre-accept Claude Code's first-run onboarding gate.",
|
|
60
|
+
apply(seed, { host }) {
|
|
61
|
+
seed.hasCompletedOnboarding = true;
|
|
62
|
+
if (typeof host?.lastOnboardingVersion === "string") seed.lastOnboardingVersion = host.lastOnboardingVersion;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "theme-selection",
|
|
67
|
+
description: "Pre-seed a theme so the first-run theme picker does not render.",
|
|
68
|
+
apply(seed, { host }) {
|
|
69
|
+
seed.theme = typeof host?.theme === "string" ? host.theme : "dark";
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "bypass-permissions-mode",
|
|
74
|
+
description: "Pre-accept Claude's open-approval bypass-permissions modal.",
|
|
75
|
+
apply(seed, { config }) {
|
|
76
|
+
if (config.approvalMode === "open") seed.bypassPermissionsModeAccepted = true;
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "workspace-trust",
|
|
81
|
+
description: "Trust the managed spawn cwd and clear project onboarding.",
|
|
82
|
+
apply(seed, { config }) {
|
|
83
|
+
if (!config.cwd) return;
|
|
84
|
+
seed.projects = {
|
|
85
|
+
...(isRecord(seed.projects) ? seed.projects : {}),
|
|
86
|
+
[resolve(config.cwd)]: { hasTrustDialogAccepted: true, hasCompletedProjectOnboarding: true },
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
export const claudeLaunchPromptGatePreventions: ClaudeLaunchPromptGatePrevention[] = [
|
|
93
|
+
{
|
|
94
|
+
id: "project-mcp-discovery",
|
|
95
|
+
description: "Use strict controlled MCP config so Claude never prompts for ambient project .mcp.json servers.",
|
|
96
|
+
apply() {
|
|
97
|
+
return { strictMcpConfig: true };
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const panePromptGates: ClaudePanePromptGate[] = [
|
|
103
|
+
{
|
|
104
|
+
id: "usage-limit",
|
|
105
|
+
label: "Usage limit",
|
|
106
|
+
policy: "wait-for-reset",
|
|
107
|
+
keystroke: "Escape",
|
|
108
|
+
minDetections: 2,
|
|
109
|
+
match: (text, nowMs) => parseClaudeRateLimitPane(text, nowMs),
|
|
110
|
+
providerState(match, context) {
|
|
111
|
+
return buildRateLimitProviderState({
|
|
112
|
+
errorType: match.errorType ?? "session_limit",
|
|
113
|
+
kind: match.kind ?? "usage_limit",
|
|
114
|
+
...(match.resetAt ? { resetAt: match.resetAt } : {}),
|
|
115
|
+
message: match.message,
|
|
116
|
+
source: context.sessionName ? `claude-pane:${context.sessionName}` : "claude-pane",
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "workspace-trust",
|
|
122
|
+
label: "Workspace trust",
|
|
123
|
+
policy: "accept-trusted-workspace",
|
|
124
|
+
keystroke: "Enter",
|
|
125
|
+
minDetections: 2,
|
|
126
|
+
match: (text) => /do you trust (?:the files in )?(?:this folder|this project|this directory)/i.test(text)
|
|
127
|
+
&& /\b(?:yes|proceed|trust|continue)\b/i.test(text)
|
|
128
|
+
? { message: lineMatching(text, /do you trust/i) }
|
|
129
|
+
: null,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "bypass-permissions-mode",
|
|
133
|
+
label: "Bypass permissions mode",
|
|
134
|
+
policy: "accept-configured-open-approval",
|
|
135
|
+
keystroke: "Enter",
|
|
136
|
+
minDetections: 2,
|
|
137
|
+
match: (text) => /bypass permissions mode/i.test(text) && /\b(?:accept|proceed|continue|yes)\b/i.test(text)
|
|
138
|
+
? { message: lineMatching(text, /bypass permissions mode/i) }
|
|
139
|
+
: null,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "project-mcp-discovery",
|
|
143
|
+
label: "Project MCP discovery",
|
|
144
|
+
policy: "reject-ambient-project-mcp",
|
|
145
|
+
keystroke: "Escape",
|
|
146
|
+
minDetections: 2,
|
|
147
|
+
match: (text) => /\bnew MCP servers? found\b/i.test(text) && /\b(?:select|enable|servers?)\b/i.test(text)
|
|
148
|
+
? { message: lineMatching(text, /new MCP servers? found/i) }
|
|
149
|
+
: null,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "theme-selection",
|
|
153
|
+
label: "Theme selection",
|
|
154
|
+
policy: "accept-default-theme",
|
|
155
|
+
keystroke: "Enter",
|
|
156
|
+
minDetections: 2,
|
|
157
|
+
match: (text) => /choose (?:the )?(?:text style|theme).*Claude Code/i.test(text) && /\b(?:dark|light) mode\b/i.test(text)
|
|
158
|
+
? { message: lineMatching(text, /choose/i) }
|
|
159
|
+
: null,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "first-run-onboarding",
|
|
163
|
+
label: "First-run onboarding",
|
|
164
|
+
policy: "accept-onboarding",
|
|
165
|
+
keystroke: "Enter",
|
|
166
|
+
minDetections: 2,
|
|
167
|
+
match: (text) => /welcome to Claude Code/i.test(text) && /\b(?:let'?s get started|continue|get started)\b/i.test(text)
|
|
168
|
+
? { message: lineMatching(text, /welcome to Claude Code/i) }
|
|
169
|
+
: null,
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
export function applyClaudeConfigPromptGatePreventions(
|
|
174
|
+
seed: Record<string, unknown>,
|
|
175
|
+
host: Record<string, unknown> | undefined,
|
|
176
|
+
config: RunnerSpawnConfig,
|
|
177
|
+
): Record<string, unknown> {
|
|
178
|
+
const next = { ...seed };
|
|
179
|
+
for (const prevention of claudeConfigPromptGatePreventions) prevention.apply(next, { host, config });
|
|
180
|
+
return next;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function claudeLaunchPromptGateSettings(config: RunnerSpawnConfig): { strictMcpConfig: boolean } {
|
|
184
|
+
return {
|
|
185
|
+
strictMcpConfig: claudeLaunchPromptGatePreventions.some((prevention) => prevention.apply(config).strictMcpConfig === true),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function initialClaudePromptGatePaneState(): ClaudePromptGatePaneState {
|
|
190
|
+
return { consecutiveDetections: 0 };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function evaluateClaudePromptGatePane(
|
|
194
|
+
text: string,
|
|
195
|
+
state: ClaudePromptGatePaneState,
|
|
196
|
+
context: ClaudePromptGatePaneContext = {},
|
|
197
|
+
): ClaudePromptGatePaneResult {
|
|
198
|
+
const detected = firstDetectedPaneGate(text, context.nowMs);
|
|
199
|
+
if (!detected) return { state: initialClaudePromptGatePaneState() };
|
|
200
|
+
if (context.busy) {
|
|
201
|
+
return { state: { ...state, currentGateId: undefined, consecutiveDetections: 0 } };
|
|
202
|
+
}
|
|
203
|
+
if (state.handledGateId === detected.entry.id) {
|
|
204
|
+
return { state };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const consecutiveDetections = state.currentGateId === detected.entry.id ? state.consecutiveDetections + 1 : 1;
|
|
208
|
+
const nextState: ClaudePromptGatePaneState = {
|
|
209
|
+
currentGateId: detected.entry.id,
|
|
210
|
+
consecutiveDetections,
|
|
211
|
+
...(state.handledGateId ? { handledGateId: state.handledGateId } : {}),
|
|
212
|
+
};
|
|
213
|
+
if (consecutiveDetections < detected.entry.minDetections) return { state: nextState };
|
|
214
|
+
|
|
215
|
+
const action: ClaudePromptGateAction = {
|
|
216
|
+
gateId: detected.entry.id,
|
|
217
|
+
label: detected.entry.label,
|
|
218
|
+
policy: detected.entry.policy,
|
|
219
|
+
keystroke: detected.entry.keystroke,
|
|
220
|
+
source: "claude-pane",
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
action,
|
|
224
|
+
status: promptGateStatus(detected.entry, detected.match, action, context),
|
|
225
|
+
state: { ...nextState, handledGateId: detected.entry.id },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function firstDetectedPaneGate(text: string, nowMs?: number): { entry: ClaudePanePromptGate; match: PaneGateMatch } | null {
|
|
230
|
+
for (const entry of panePromptGates) {
|
|
231
|
+
const match = entry.match(text, nowMs);
|
|
232
|
+
if (match) return { entry, match };
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function promptGateStatus(
|
|
238
|
+
entry: ClaudePanePromptGate,
|
|
239
|
+
match: PaneGateMatch,
|
|
240
|
+
action: ClaudePromptGateAction,
|
|
241
|
+
context: ClaudePromptGatePaneContext,
|
|
242
|
+
): ProviderStatusUpdate {
|
|
243
|
+
const metadata = {
|
|
244
|
+
eventType: "claude.prompt_gate.auto_handled",
|
|
245
|
+
gateId: action.gateId,
|
|
246
|
+
policy: action.policy,
|
|
247
|
+
keystroke: action.keystroke,
|
|
248
|
+
source: action.source,
|
|
249
|
+
...(context.sessionName ? { sessionName: context.sessionName } : {}),
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
status: "idle",
|
|
253
|
+
clear: ["subagent"],
|
|
254
|
+
...(entry.providerState ? { providerState: entry.providerState(match, context) } : {}),
|
|
255
|
+
timeline: {
|
|
256
|
+
status: "claude.prompt_gate.auto_handled",
|
|
257
|
+
id: `claude-prompt-gate-${entry.id}-${Date.now()}`,
|
|
258
|
+
timestamp: Date.now(),
|
|
259
|
+
title: "Claude prompt gate auto-handled",
|
|
260
|
+
body: match.message ?? `${entry.label} dismissed with ${entry.keystroke}`,
|
|
261
|
+
metadata,
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function lineMatching(text: string, pattern: RegExp): string | undefined {
|
|
267
|
+
return text.split(/\r?\n/).map((line) => line.trim()).find((line) => pattern.test(line));
|
|
268
|
+
}
|
package/src/launch-assembly.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
profileUsesClaudeHostProviderGlobals,
|
|
14
14
|
providerHomePathFor,
|
|
15
15
|
} from "./profile-home";
|
|
16
|
+
import { claudeLaunchPromptGateSettings } from "./claude-prompt-gates";
|
|
16
17
|
import {
|
|
17
18
|
materializeResolvedAssets,
|
|
18
19
|
resolveClaudeProjectSkills,
|
|
@@ -280,16 +281,10 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
280
281
|
: {};
|
|
281
282
|
const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
|
|
282
283
|
|
|
283
|
-
// #662 —
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
// decides should load is injected via --mcp-config, which strict PRESERVES; strict only
|
|
288
|
-
// blocks what relay DIDN'T inject — above all the cwd project `.mcp.json` (the modal's
|
|
289
|
-
// source), which stays excluded. The relay plugin (monitor + turn hooks) loads via
|
|
290
|
-
// --plugin-dir and ships no MCP server, so it is untouched. Was `vanilla`-only, which left
|
|
291
|
-
// host/minimal/isolated bases exposed to the modal wedge.
|
|
292
|
-
const strictMcp = true;
|
|
284
|
+
// #662/#663 — strict controlled MCP is a Claude prompt-gate prevention entry, not a
|
|
285
|
+
// launch-assembly special case. It prevents the blocking "N new MCP servers found in
|
|
286
|
+
// this project" modal while preserving every relay-declared server in --mcp-config.
|
|
287
|
+
const strictMcp = claudeLaunchPromptGateSettings(config).strictMcpConfig;
|
|
293
288
|
|
|
294
289
|
// #662 follow-up — strict drops ALL on-disk config, including the host user-scope
|
|
295
290
|
// `~/.claude.json` MCP that a host-base agent loaded ambiently before strict. That
|
package/src/profile-home.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ProvisioningMcpServer } from "agent-relay-sdk";
|
|
|
6
6
|
import { profileAllowsRelayFeature, type RunnerSpawnConfig } from "./adapter";
|
|
7
7
|
import { CLAUDE_RELAY_MANUAL } from "./relay-instructions";
|
|
8
8
|
import { providerHomeRootFromEnv } from "./config";
|
|
9
|
+
import { applyClaudeConfigPromptGatePreventions } from "./claude-prompt-gates";
|
|
9
10
|
|
|
10
11
|
type ProviderHome = {
|
|
11
12
|
path: string;
|
|
@@ -101,20 +102,7 @@ function seedClaudeConfigIfMissing(claudeHome: string, config: RunnerSpawnConfig
|
|
|
101
102
|
const path = join(claudeHome, ".claude.json");
|
|
102
103
|
if (existsSync(path)) return;
|
|
103
104
|
const host = readHostClaudeConfig();
|
|
104
|
-
const seed
|
|
105
|
-
hasCompletedOnboarding: true,
|
|
106
|
-
theme: typeof host?.theme === "string" ? host.theme : "dark",
|
|
107
|
-
};
|
|
108
|
-
if (typeof host?.lastOnboardingVersion === "string") seed.lastOnboardingVersion = host.lastOnboardingVersion;
|
|
109
|
-
// Open approval = headless launch adds --dangerously-skip-permissions, which has
|
|
110
|
-
// its OWN first-run gate ("Bypass Permissions mode" accept/exit). Pre-accept it,
|
|
111
|
-
// or the agent stalls there exactly like the onboarding/trust gates.
|
|
112
|
-
if (config.approvalMode === "open") seed.bypassPermissionsModeAccepted = true;
|
|
113
|
-
if (config.cwd) {
|
|
114
|
-
seed.projects = {
|
|
115
|
-
[resolve(config.cwd)]: { hasTrustDialogAccepted: true, hasCompletedProjectOnboarding: true },
|
|
116
|
-
};
|
|
117
|
-
}
|
|
105
|
+
const seed = applyClaudeConfigPromptGatePreventions({}, host, config);
|
|
118
106
|
writeFileSync(path, JSON.stringify(seed, null, 2), { mode: 0o600 });
|
|
119
107
|
}
|
|
120
108
|
|
package/src/rate-limit.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// #286 — shared rate-limit hold construction + Claude pane detection.
|
|
2
|
-
import type { ProviderStatusUpdate } from "./adapter";
|
|
3
2
|
//
|
|
4
3
|
// Two detection arms feed the SAME provider-neutral `blocked` hold:
|
|
5
4
|
// 1. The StopFailure hook (clean API-error turn-end: API-429/auth/billing) → control-server.
|
|
@@ -126,38 +125,6 @@ function parseClaudeTransientOverloadPane(text: string): PaneRateLimit | null {
|
|
|
126
125
|
};
|
|
127
126
|
}
|
|
128
127
|
|
|
129
|
-
export interface ClaudeRateLimitPaneGateState {
|
|
130
|
-
consecutiveDetections: number;
|
|
131
|
-
reported: boolean;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function gatedClaudeRateLimitStatus(
|
|
135
|
-
text: string,
|
|
136
|
-
state: ClaudeRateLimitPaneGateState,
|
|
137
|
-
options: { sessionName?: string; busy?: boolean } = {},
|
|
138
|
-
): { status: ProviderStatusUpdate | null; state: ClaudeRateLimitPaneGateState } {
|
|
139
|
-
const parsed = parseClaudeRateLimitPane(text);
|
|
140
|
-
if (!parsed) return { status: null, state: { consecutiveDetections: 0, reported: false } };
|
|
141
|
-
if (options.busy) return { status: null, state: { consecutiveDetections: 0, reported: state.reported } };
|
|
142
|
-
const nextState = { consecutiveDetections: state.consecutiveDetections + 1, reported: state.reported };
|
|
143
|
-
if (nextState.reported || nextState.consecutiveDetections < 2) return { status: null, state: nextState };
|
|
144
|
-
nextState.reported = true;
|
|
145
|
-
return {
|
|
146
|
-
status: {
|
|
147
|
-
status: "idle",
|
|
148
|
-
clear: ["subagent"],
|
|
149
|
-
providerState: buildRateLimitProviderState({
|
|
150
|
-
errorType: parsed.errorType ?? "session_limit",
|
|
151
|
-
kind: parsed.kind ?? "usage_limit",
|
|
152
|
-
...(parsed.resetAt ? { resetAt: parsed.resetAt } : {}),
|
|
153
|
-
message: parsed.message,
|
|
154
|
-
source: options.sessionName ? `claude-pane:${options.sessionName}` : "claude-pane",
|
|
155
|
-
}),
|
|
156
|
-
},
|
|
157
|
-
state: nextState,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
128
|
// The clean limit line containing the match offset, stripped of box-drawing chrome.
|
|
162
129
|
function limitLineAt(text: string, index: number): string {
|
|
163
130
|
const start = text.lastIndexOf("\n", index) + 1;
|