File size: 7,602 Bytes
9299241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// Bootstraps approval handlers from channel plugin capabilities.
import { resolveChannelApprovalCapability } from "../channels/plugins/approvals.js";
import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js";
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { withGatewayNativeApprovalRuntime } from "./approval-gateway-runtime-context.js";
import type { GatewayNativeApprovalRuntime } from "./approval-gateway-runtime.types.js";
import {
  CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY,
  createChannelApprovalHandlerFromCapability,
  type ChannelApprovalHandler,
} from "./approval-handler-runtime.js";
import {
  getChannelRuntimeContext,
  watchChannelRuntimeContexts,
} from "./channel-runtime-context.js";
import { isExecApprovalChannelRuntimeTerminalStartError } from "./exec-approval-channel-runtime.js";

type ApprovalBootstrapHandler = ChannelApprovalHandler;
const APPROVAL_HANDLER_BOOTSTRAP_RETRY_MS = 1_000;

function isRetryableApprovalBootstrapStartError(error: unknown): boolean {
  const message = String(error);
  return (
    message.includes("gateway readiness unavailable before approval client start") ||
    message.includes("gateway approval client start aborted before readiness") ||
    message.includes("gateway readiness unavailable before exec approval runtime start") ||
    message.includes("gateway approval runtime start aborted before readiness") ||
    message.includes("gateway event loop readiness timeout") ||
    message.includes("gateway starting") ||
    message.includes("code=1013") ||
    message.includes("close code 1013")
  );
}

function formatRetryableApprovalBootstrapStartError(error: unknown): string {
  const message = String(error);
  if (message.includes("gateway event loop readiness timeout")) {
    return "gateway readiness unavailable before approval handler start";
  }
  return message;
}

/** Starts the native approval handler for a channel runtime context and returns its cleanup hook. */
export async function startChannelApprovalHandlerBootstrap(params: {
  plugin: Pick<ChannelPlugin, "id" | "meta" | "approvalCapability">;
  cfg: OpenClawConfig;
  accountId: string;
  channelRuntime?: ChannelRuntimeSurface;
  gatewayRuntime?: GatewayNativeApprovalRuntime;
  logger?: ReturnType<typeof createSubsystemLogger>;
}): Promise<() => Promise<void>> {
  const capability = resolveChannelApprovalCapability(params.plugin);
  if (!capability?.nativeRuntime || !params.channelRuntime) {
    return async () => {};
  }

  const channelLabel = params.plugin.meta.label || params.plugin.id;
  const logger = params.logger ?? createSubsystemLogger(`${params.plugin.id}/approval-bootstrap`);
  let activeGeneration = 0;
  let activeHandler: ApprovalBootstrapHandler | null = null;
  let retryTimer: NodeJS.Timeout | null = null;
  const invalidateActiveHandler = () => {
    activeGeneration += 1;
  };
  const clearRetryTimer = () => {
    if (!retryTimer) {
      return;
    }
    clearTimeout(retryTimer);
    retryTimer = null;
  };

  const stopHandler = async () => {
    const handler = activeHandler;
    activeHandler = null;
    if (!handler) {
      return;
    }
    await handler.stop();
  };

  const startHandlerForContext = async (context: unknown, generation: number) => {
    if (generation !== activeGeneration) {
      return;
    }
    await stopHandler();
    if (generation !== activeGeneration) {
      return;
    }
    const handler = await withGatewayNativeApprovalRuntime(params.gatewayRuntime, () =>
      createChannelApprovalHandlerFromCapability({
        capability,
        label: `${params.plugin.id}/native-approvals`,
        clientDisplayName: `${channelLabel} Native Approvals (${params.accountId})`,
        channel: params.plugin.id,
        channelLabel,
        cfg: params.cfg,
        accountId: params.accountId,
        context,
      }),
    );
    if (!handler) {
      return;
    }
    if (generation !== activeGeneration) {
      // Runtime contexts can unregister while the handler factory awaits; stop stale handlers.
      await handler.stop().catch(() => {});
      return;
    }
    activeHandler = handler as ApprovalBootstrapHandler;
    try {
      await withGatewayNativeApprovalRuntime(params.gatewayRuntime, () => handler.start());
    } catch (error) {
      if (activeHandler === handler) {
        activeHandler = null;
      }
      await handler.stop().catch(() => {});
      throw error;
    }
  };

  const spawn = (label: string, promise: Promise<void>) => {
    void promise.catch((error: unknown) => {
      logger.error(`${label}: ${String(error)}`);
    });
  };
  const scheduleRetryForContext = (context: unknown, generation: number) => {
    if (generation !== activeGeneration) {
      return;
    }
    clearRetryTimer();
    retryTimer = setTimeout(() => {
      retryTimer = null;
      if (generation !== activeGeneration) {
        return;
      }
      spawn(
        "failed to retry native approval handler",
        startHandlerForRegisteredContext(context, generation),
      );
    }, APPROVAL_HANDLER_BOOTSTRAP_RETRY_MS);
    retryTimer.unref?.();
  };
  const startHandlerForRegisteredContext = async (context: unknown, generation: number) => {
    try {
      await startHandlerForContext(context, generation);
    } catch (error) {
      if (generation === activeGeneration) {
        if (isExecApprovalChannelRuntimeTerminalStartError(error)) {
          logger.error(`native approval handler disabled: ${String(error)}`);
          return;
        }
        if (isRetryableApprovalBootstrapStartError(error)) {
          logger.warn(
            `native approval handler deferred until gateway readiness recovers: ${formatRetryableApprovalBootstrapStartError(error)}`,
          );
          scheduleRetryForContext(context, generation);
          return;
        }
        logger.error(`failed to start native approval handler: ${String(error)}`);
        scheduleRetryForContext(context, generation);
      }
    }
  };

  const unsubscribe =
    watchChannelRuntimeContexts({
      channelRuntime: params.channelRuntime,
      channelId: params.plugin.id,
      accountId: params.accountId,
      capability: CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY,
      onEvent: (event) => {
        if (event.type === "registered") {
          clearRetryTimer();
          invalidateActiveHandler();
          const generation = activeGeneration;
          spawn(
            "failed to start native approval handler",
            startHandlerForRegisteredContext(event.context, generation),
          );
          return;
        }
        clearRetryTimer();
        invalidateActiveHandler();
        spawn("failed to stop native approval handler", stopHandler());
      },
    }) ?? (() => {});

  const existingContext = getChannelRuntimeContext({
    channelRuntime: params.channelRuntime,
    channelId: params.plugin.id,
    accountId: params.accountId,
    capability: CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY,
  });
  if (existingContext !== undefined) {
    clearRetryTimer();
    invalidateActiveHandler();
    const generation = activeGeneration;
    spawn(
      "failed to start native approval handler",
      startHandlerForRegisteredContext(existingContext, generation),
    );
  }

  return async () => {
    unsubscribe();
    clearRetryTimer();
    invalidateActiveHandler();
    await stopHandler();
  };
}