35ade14630
SessionAwareHandle wrapped the base's WorkerHandle but never overrode charterReceipt(), so it silently inherited the interface default (null) while the real receipt sat on its delegate. sol/terra never got a charterSource/charterSha256 roster row. Deletes the default so every PeerHandle must answer explicitly; the compiler now catches this class of gap instead of a roster field quietly going missing.
466 lines
23 KiB
Java
466 lines
23 KiB
Java
package dev.ltms.bridged.member;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
import dev.ltms.bridged.config.BridgedConfig;
|
|
import dev.ltms.bridged.herdr.Agent;
|
|
import dev.ltms.bridged.herdr.AgentControl;
|
|
import dev.ltms.bridged.herdr.WorkspaceControl;
|
|
import dev.ltms.bridged.peer.Capability;
|
|
import dev.ltms.bridged.peer.CharterReceipt;
|
|
import dev.ltms.bridged.peer.PeerHandle;
|
|
import dev.ltms.bridged.peer.SpawnRequest;
|
|
|
|
import java.io.IOException;
|
|
import java.io.UncheckedIOException;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.EnumSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.function.Function;
|
|
import java.util.function.LongSupplier;
|
|
import java.util.function.Supplier;
|
|
|
|
/**
|
|
* The {@link HerdrPeerLauncher} adapter for <strong>opencode</strong> — an open-source,
|
|
* provider-agnostic terminal coding agent. Its whole reason for existing is to prove the
|
|
* {@code PeerLauncher} SPI is genuinely provider-neutral: opencode shares none of Claude Code's
|
|
* private launch seams, yet reuses every line of shared transport in the base (tab/pane placement,
|
|
* the CB-306 readiness gate, unique naming + CB-117 reap, teardown, listing, cwd).
|
|
*
|
|
* <p>The divergences from {@link ClaudeCodeLauncher}, all confined to {@link #buildLaunch}:
|
|
* <ul>
|
|
* <li><strong>No subscription boundary.</strong> opencode carries no {@code ANTHROPIC_BASE_URL}
|
|
* and there is no {@link dev.ltms.bridged.guard.SubscriptionGuard} — the guard is a
|
|
* Claude-private concern, not part of the SPI. opencode reads the operator's own provider
|
|
* credentials from its global {@code auth.json}; the bridge injects none.</li>
|
|
* <li><strong>File-based MCP mount + instructions.</strong> opencode has no inline
|
|
* {@code --mcp-config}/{@code --append-system-prompt}. Instead the bridge writes an ephemeral
|
|
* {@code opencode.json} that declares the bridge as a {@code remote} MCP server and lists a
|
|
* member-charter file under {@code instructions}, then points the worker at it with
|
|
* {@code OPENCODE_CONFIG}. This is the one place the launcher touches disk — Claude never did.</li>
|
|
* <li><strong>Model as a flag.</strong> the {@code provider/model} selector is passed as
|
|
* {@code -m}, not an env var.</li>
|
|
* <li><strong>{@code opencode} name prefix</strong> so reap matches {@code opencode-*} panes and
|
|
* never another adapter's.</li>
|
|
* </ul>
|
|
*/
|
|
public final class OpenCodeLauncher extends HerdrPeerLauncher {
|
|
|
|
/** Label prefix for this adapter's herdr agent names (drives naming + orphan reap). */
|
|
private static final String NAME_PREFIX = "opencode";
|
|
|
|
/** Writer for the generated {@code opencode.json}. */
|
|
private static final ObjectMapper JSON = new ObjectMapper();
|
|
|
|
/** Root under which per-spawn opencode config dirs are created (injectable for tests). */
|
|
private final Path configRoot;
|
|
|
|
/**
|
|
* Session discovery against opencode's on-disk storage ({@link OpenCodeSessionDiscovery}) —
|
|
* the one seam that knows opencode's private session-file layout. Its root is injectable for
|
|
* tests so they never touch the operator's real {@code ~/.local/share/opencode}.
|
|
*/
|
|
private final OpenCodeSessionDiscovery discovery;
|
|
|
|
/**
|
|
* Production constructor — disables the spawn-ready gate ({@code spawnReadyTimeoutMs == 0}) so it
|
|
* matches the legacy non-blocking spawn semantics. Config dirs are created under the JVM temp dir.
|
|
*/
|
|
public OpenCodeLauncher(AgentControl agents, WorkspaceControl spaces,
|
|
Map<String, BridgedConfig.Profile> profiles, String defaultProfile,
|
|
Function<String, String> env) {
|
|
this(agents, spaces, profiles, defaultProfile, env, 0,
|
|
System::currentTimeMillis, () -> sleepUninterruptibly(300),
|
|
defaultConfigRoot(), defaultDiscoveryRoot());
|
|
}
|
|
|
|
/**
|
|
* Production constructor with the spawn-ready gate enabled. Polls {@code agents.status()} until
|
|
* the pane reports an injectable state or {@code spawnReadyTimeoutMs} elapses.
|
|
*/
|
|
public OpenCodeLauncher(AgentControl agents, WorkspaceControl spaces,
|
|
Map<String, BridgedConfig.Profile> profiles, String defaultProfile,
|
|
Function<String, String> env,
|
|
long spawnReadyTimeoutMs, long spawnReadyPollMs) {
|
|
this(agents, spaces, profiles, defaultProfile, env,
|
|
spawnReadyTimeoutMs, spawnReadyPollMs, null);
|
|
}
|
|
|
|
/**
|
|
* Production constructor carrying the fleet-wide tab-label template (CB-557). The template comes
|
|
* from {@code fleet.tabLabel}, which a profile cannot know because it names the member's
|
|
* <em>role</em>; a profile may still override it with its own {@code tabLabel}.
|
|
*/
|
|
public OpenCodeLauncher(AgentControl agents, WorkspaceControl spaces,
|
|
Map<String, BridgedConfig.Profile> profiles, String defaultProfile,
|
|
Function<String, String> env,
|
|
long spawnReadyTimeoutMs, long spawnReadyPollMs,
|
|
Supplier<BridgedConfig.Fleet> fleet) {
|
|
this(agents, spaces, profiles, defaultProfile, env, spawnReadyTimeoutMs,
|
|
System::currentTimeMillis, () -> sleepUninterruptibly(spawnReadyPollMs),
|
|
defaultConfigRoot(), defaultDiscoveryRoot(), fleet);
|
|
}
|
|
|
|
/**
|
|
* Full testability constructor. Every injectable collaborator is explicit so unit tests supply a
|
|
* fake clock ({@code nowMillis}), poll-loop wait ({@code sleeper}), and a temp {@code configRoot}
|
|
* they can inspect the generated {@code opencode.json}/charter under.
|
|
*
|
|
* @param agents herdr agent control (start, status, close)
|
|
* @param spaces workspace / tab control (ensure, create, close)
|
|
* @param profiles configured worker profiles
|
|
* @param defaultProfile profile a no-argument spawn uses (nullable)
|
|
* @param env host env lookup (injectable for tests)
|
|
* @param spawnReadyTimeoutMs max ms to wait for injectable state (0 disables the gate)
|
|
* @param nowMillis monotonic clock source (e.g. {@code System::currentTimeMillis})
|
|
* @param sleeper sleep/wait hook (encodes the poll interval; never called when the
|
|
* gate is disabled)
|
|
* @param configRoot existing directory under which per-spawn config dirs are created
|
|
* @param discoveryRoot opencode's on-disk storage root to scan for session records
|
|
* (injectable for tests; opencode's layout is matched at
|
|
* {@link OpenCodeSessionDiscovery})
|
|
*/
|
|
public OpenCodeLauncher(AgentControl agents, WorkspaceControl spaces,
|
|
Map<String, BridgedConfig.Profile> profiles, String defaultProfile,
|
|
Function<String, String> env,
|
|
long spawnReadyTimeoutMs,
|
|
LongSupplier nowMillis, Runnable sleeper,
|
|
Path configRoot, Path discoveryRoot) {
|
|
this(agents, spaces, profiles, defaultProfile, env, spawnReadyTimeoutMs,
|
|
nowMillis, sleeper, configRoot, discoveryRoot, null);
|
|
}
|
|
|
|
/**
|
|
* Full testability constructor, plus the fleet-wide tab-label template (CB-557).
|
|
*
|
|
* @param fleet live fleet config, read once for each spawn
|
|
*/
|
|
public OpenCodeLauncher(AgentControl agents, WorkspaceControl spaces,
|
|
Map<String, BridgedConfig.Profile> profiles, String defaultProfile,
|
|
Function<String, String> env,
|
|
long spawnReadyTimeoutMs,
|
|
LongSupplier nowMillis, Runnable sleeper,
|
|
Path configRoot, Path discoveryRoot,
|
|
Supplier<BridgedConfig.Fleet> fleet) {
|
|
super(NAME_PREFIX, agents, spaces, profiles, defaultProfile, env,
|
|
spawnReadyTimeoutMs, nowMillis, sleeper, fleet);
|
|
this.configRoot = configRoot;
|
|
this.discovery = new OpenCodeSessionDiscovery(discoveryRoot);
|
|
}
|
|
|
|
private static Path defaultConfigRoot() {
|
|
return Path.of(System.getProperty("java.io.tmpdir"));
|
|
}
|
|
|
|
/** The default opencode storage root: {@code ~/.local/share/opencode} (the XDG data dir). */
|
|
private static Path defaultDiscoveryRoot() {
|
|
return Path.of(System.getProperty("user.home"), ".local", "share", "opencode");
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*
|
|
* <p>Builds the opencode launch: no {@code ANTHROPIC_*} and no guard (opencode reads its own
|
|
* provider credentials); when the profile mounts the bridge MCP or has a member charter,
|
|
* generate an ephemeral {@code opencode.json} (remote MCP server + member-charter instructions)
|
|
* and point the worker at it via {@code OPENCODE_CONFIG}; carry the parity-neutral git-forge
|
|
* grant; and select the model with {@code -m}.
|
|
*/
|
|
@Override
|
|
protected Launch buildLaunch(BridgedConfig.Profile cfg, LaunchSpec spec) {
|
|
Map<String, String> workerEnv = baseEnv(cfg);
|
|
// A config file is needed for the bridge MCP mount, a member charter, or a pinned endpoint (CB-508).
|
|
if (cfg.hasMcp() || spec.charter() != null || hasCustomProvider(cfg)) {
|
|
workerEnv.put("OPENCODE_CONFIG", writeConfig(cfg, spec.charter()).toString());
|
|
}
|
|
applyGitToken(workerEnv, cfg);
|
|
return new Launch(workerEnv,
|
|
argvWithResume(argvWithModel(argvWithAuto(cfg), cfg), spec.resumeSessionId()));
|
|
}
|
|
|
|
/**
|
|
* True when this profile pins its own OpenAI-compatible endpoint (CB-508) rather than using
|
|
* whatever provider opencode resolves by default.
|
|
*
|
|
* <p>Note this reuses {@code baseUrl}, the same field the Claude adapter injects as
|
|
* {@code ANTHROPIC_BASE_URL} — but it does <em>not</em> go through {@code SubscriptionGuard}.
|
|
* That asymmetry is deliberate and safe: the guard exists to stop a worker borrowing the
|
|
* primary's Anthropic subscription, and an opencode process has no Anthropic credential path
|
|
* at all. Pointing it at a local vLLM cannot leak the subscription.
|
|
*/
|
|
private static boolean hasCustomProvider(BridgedConfig.Profile cfg) {
|
|
return cfg.baseUrl() != null && !cfg.baseUrl().isBlank();
|
|
}
|
|
|
|
/**
|
|
* The launch argv plus the unconditional {@code --auto} flag, which auto-approves the
|
|
* permissions opencode does not explicitly deny. It is unconditional, not a preference: a
|
|
* spawned peer has no human at its pane — the bridge spawned it — so one that stops at an
|
|
* approval prompt is a wedged agent, indistinguishable from a legitimate mid-turn wait and
|
|
* unable to end its turn with {@code bridge_reply}. opencode's own help calls this
|
|
* "dangerous!", but the blast radius here is already bounded by design: a worker runs in its
|
|
* own git worktree on its own branch, is off-subscription, and cannot merge — the lead is the
|
|
* gate.
|
|
*/
|
|
private List<String> argvWithAuto(BridgedConfig.Profile cfg) {
|
|
List<String> argv = mutableArgv(cfg.argv());
|
|
argv.add("--auto");
|
|
return argv;
|
|
}
|
|
|
|
/**
|
|
* The launch argv plus, on a resumed spawn, opencode's {@code -s <id>} flag to continue a prior
|
|
* conversation by its session id. {@code -s, --session <id>} resumes an existing session; on a
|
|
* fresh spawn (no resume target) no flag is added, letting opencode start a brand-new session.
|
|
* The id comes from the base launch spec.
|
|
*/
|
|
private List<String> argvWithResume(List<String> argv, String id) {
|
|
if (id == null || id.isBlank()) {
|
|
return argv;
|
|
}
|
|
List<String> withResume = mutableArgv(argv);
|
|
withResume.add("-s");
|
|
withResume.add(id);
|
|
return withResume;
|
|
}
|
|
|
|
/** The launch argv plus, when a model is configured, the opencode {@code -m provider/model} flag. */
|
|
private List<String> argvWithModel(List<String> argv, BridgedConfig.Profile cfg) {
|
|
if (cfg.model() != null && !cfg.model().isBlank()) {
|
|
argv.add("-m");
|
|
argv.add(cfg.model());
|
|
}
|
|
return argv;
|
|
}
|
|
|
|
/**
|
|
* Write an ephemeral {@code opencode.json} (and the member-charter file it references) into a
|
|
* fresh per-spawn directory under {@link #configRoot}, and return the config file's path for
|
|
* {@code OPENCODE_CONFIG}. The dir is unique per spawn so concurrent workers never race on it;
|
|
* it is best-effort cleaned on JVM exit (worker config is disposable — regenerated every spawn).
|
|
*/
|
|
private Path writeConfig(BridgedConfig.Profile cfg, String charterText) {
|
|
try {
|
|
Path dir = Files.createTempDirectory(configRoot, "bridged-opencode-");
|
|
dir.toFile().deleteOnExit();
|
|
|
|
ObjectNode root = JSON.createObjectNode();
|
|
root.put("$schema", "https://opencode.ai/config.json");
|
|
// CB-523, opencode side: a worker that runs out of context dies mid-turn, and its reply
|
|
// — the entire point of the turn — is lost with it. Auto-compaction is therefore not an
|
|
// operator preference for a bridged worker, it is a condition of the turn contract.
|
|
//
|
|
// Stated deliberately even though it is redundant today: OPENCODE_CONFIG is MERGED over
|
|
// ~/.config/opencode/config.json rather than replacing it, so a worker already inherits
|
|
// an `auto: true` set at home. We do not want that inheritance to be what the guarantee
|
|
// rests on — the home file is outside this repo, differs per machine, and is not ours.
|
|
//
|
|
// Know the cost before removing it: this key WINS over the home config (verified — an
|
|
// OPENCODE_CONFIG value overrides the home value, it does not defer to it), so an
|
|
// operator who sets `compaction.auto: false` at home cannot turn it off for bridged
|
|
// workers. That is the intended trade for peers we spawn and whose turns we must land;
|
|
// if per-profile control is ever wanted, add a profile knob rather than dropping this.
|
|
root.putObject("compaction").put("auto", true);
|
|
|
|
if (charterText != null) {
|
|
Path charter = dir.resolve("member-charter.md");
|
|
Files.writeString(charter, charterText);
|
|
charter.toFile().deleteOnExit();
|
|
|
|
root.putArray("instructions").add(charter.toAbsolutePath().toString());
|
|
}
|
|
|
|
if (cfg.hasMcp()) {
|
|
ObjectNode bridge = root.putObject("mcp").putObject("bridge");
|
|
bridge.put("type", "remote");
|
|
bridge.put("url", cfg.mcpUrl());
|
|
bridge.put("enabled", true);
|
|
}
|
|
if (hasCustomProvider(cfg)) {
|
|
addCustomProvider(root, cfg);
|
|
}
|
|
|
|
Path cfgFile = dir.resolve("opencode.json");
|
|
// Built with Jackson rather than string concatenation: the provider block is nested and
|
|
// carries operator-supplied values (URL, model id, api key), so escaping must be real.
|
|
Files.writeString(cfgFile, JSON.writerWithDefaultPrettyPrinter().writeValueAsString(root));
|
|
cfgFile.toFile().deleteOnExit();
|
|
return cfgFile;
|
|
} catch (IOException e) {
|
|
throw new UncheckedIOException(
|
|
"cannot write opencode config for profile " + cfg.profile(), e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Declare a custom OpenAI-compatible provider so the worker talks to a pinned endpoint (a local
|
|
* vLLM, say) instead of opencode's default gateway (CB-508).
|
|
*
|
|
* <p>The provider id comes from the {@code provider/model} selector in {@code model:}, so one
|
|
* field drives both the declaration and the {@code -m} flag and they cannot drift apart.
|
|
*/
|
|
private void addCustomProvider(ObjectNode root, BridgedConfig.Profile cfg) {
|
|
String[] parts = splitModelSelector(cfg);
|
|
String providerId = parts[0];
|
|
String modelId = parts[1];
|
|
|
|
ObjectNode provider = root.putObject("provider").putObject(providerId);
|
|
provider.put("npm", "@ai-sdk/openai-compatible");
|
|
provider.put("name", providerId + " (bridged)");
|
|
|
|
ObjectNode options = provider.putObject("options");
|
|
options.put("baseURL", openAiBaseUrl(cfg.baseUrl()));
|
|
// vLLM and friends usually ignore the key, but the AI SDK still requires a non-empty one.
|
|
String token = resolveEnv(cfg.tokenEnv());
|
|
options.put("apiKey", (token == null || token.isBlank()) ? "bridged-local-noauth" : token);
|
|
|
|
provider.putObject("models").putObject(modelId).put("name", modelId);
|
|
}
|
|
|
|
/**
|
|
* Split {@code model:} into its {@code provider} and {@code model} halves. A pinned endpoint
|
|
* needs both, so a bare model name is rejected loudly rather than silently falling back to the
|
|
* default gateway — a worker quietly talking to the wrong endpoint is the failure this avoids.
|
|
*/
|
|
private static String[] splitModelSelector(BridgedConfig.Profile cfg) {
|
|
String model = cfg.model();
|
|
int slash = model == null ? -1 : model.indexOf('/');
|
|
if (model == null || model.isBlank() || slash <= 0 || slash == model.length() - 1) {
|
|
throw new IllegalArgumentException(
|
|
"profile " + cfg.profile() + " sets baseUrl (a pinned opencode endpoint) so"
|
|
+ " model: must be \"<provider>/<model>\", e.g."
|
|
+ " \"local-vllm/deepseek-v4-flash\"; got "
|
|
+ (model == null ? "null" : '"' + model + '"'));
|
|
}
|
|
return new String[]{model.substring(0, slash), model.substring(slash + 1)};
|
|
}
|
|
|
|
/**
|
|
* The OpenAI-compatible base URL for {@code baseUrl}. A bare {@code host:port} gets {@code /v1}
|
|
* appended (where these servers put the API); a URL that already carries a path is taken as-is,
|
|
* so an endpoint mounted somewhere unusual is still reachable.
|
|
*/
|
|
private static String openAiBaseUrl(String baseUrl) {
|
|
String trimmed = baseUrl.trim();
|
|
while (trimmed.endsWith("/")) {
|
|
trimmed = trimmed.substring(0, trimmed.length() - 1);
|
|
}
|
|
int schemeEnd = trimmed.indexOf("://");
|
|
String afterScheme = schemeEnd < 0 ? trimmed : trimmed.substring(schemeEnd + 3);
|
|
return afterScheme.contains("/") ? trimmed : trimmed + "/v1";
|
|
}
|
|
|
|
/** Add lazy on-disk session discovery to the base handle. */
|
|
@Override
|
|
public PeerHandle spawn(SpawnRequest req) {
|
|
PeerHandle inner = super.spawn(req);
|
|
return new SessionAwareHandle(inner, discovery, effectiveCwd(req));
|
|
}
|
|
|
|
/**
|
|
* A {@link PeerHandle} that delegates everything to the base's worker handle but resolves
|
|
* {@link #agentSessionId()} lazily through opencode session discovery. Delegate-only, so the
|
|
* base's id/terminalId/profile semantics (CB-519's host-unique routing key, herdr coordinates)
|
|
* are untouched — only the opencode-specific identity answer is added. {@code sessionName()}
|
|
* stays null: opencode has no display-name seam, so the logical name lives only in the bridge's
|
|
* roster (see the SESSION_NAME capability).
|
|
*/
|
|
private static final class SessionAwareHandle implements PeerHandle {
|
|
private final PeerHandle delegate;
|
|
private final OpenCodeSessionDiscovery discovery;
|
|
private final String cwd;
|
|
|
|
SessionAwareHandle(PeerHandle delegate, OpenCodeSessionDiscovery discovery, String cwd) {
|
|
this.delegate = delegate;
|
|
this.discovery = discovery;
|
|
this.cwd = cwd;
|
|
}
|
|
|
|
@Override
|
|
public String id() {
|
|
return delegate.id();
|
|
}
|
|
|
|
@Override
|
|
public String terminalId() {
|
|
return delegate.terminalId();
|
|
}
|
|
|
|
@Override
|
|
public String profile() {
|
|
return delegate.profile();
|
|
}
|
|
|
|
@Override
|
|
public String sessionName() {
|
|
return delegate.sessionName();
|
|
}
|
|
|
|
@Override
|
|
public String agentSessionId() {
|
|
// Lazy + retried, never a spawn-time blocker: opencode writes the session record only
|
|
// when the session is first persisted, so null here is the correct interim answer and
|
|
// the caller re-calls later (each call re-scans, picking up a record that has since
|
|
// appeared).
|
|
return discovery.sessionIdForDirectory(cwd);
|
|
}
|
|
|
|
@Override
|
|
public CharterReceipt charterReceipt() {
|
|
return delegate.charterReceipt();
|
|
}
|
|
}
|
|
|
|
// --- Agent-returning convenience spawns (used by callers/tests that want the herdr Agent) ---
|
|
|
|
/** Spawn a worker for the default profile in the resolved default cwd. */
|
|
public Agent spawn() {
|
|
return spawnInternal(null, null, null);
|
|
}
|
|
|
|
/** Spawn a worker for a named profile (null → default) in the resolved default cwd. */
|
|
public Agent spawn(String profileName) {
|
|
return spawnInternal(profileName, null, null);
|
|
}
|
|
|
|
/** Spawn a worker for a named profile with an explicit requested/caller cwd (CB-112). */
|
|
public Agent spawn(String profileName, String requestedCwd, String callerCwd) {
|
|
return spawnInternal(profileName, requestedCwd, callerCwd);
|
|
}
|
|
|
|
// --- capabilities --------------------------------------------------------------------------
|
|
|
|
@Override
|
|
public Set<Capability> capabilities() {
|
|
Set<Capability> caps = EnumSet.of(Capability.MID_TURN_ASK, Capability.WORKTREE,
|
|
Capability.ORPHAN_REAP, Capability.SESSION_RESUME);
|
|
if (hasGitTokenProfile()) {
|
|
caps.add(Capability.SELF_PR);
|
|
}
|
|
// Deliberately NOT SESSION_NAME: opencode has no display-name flag, so the bridge's logical
|
|
// name can't surface in the peer's own UI — declaring the capability would hide that
|
|
// asymmetry rather than make it honest. For opencode the name lives only in the bridge's
|
|
// roster (see PeerHandle.sessionName() returning null).
|
|
return Set.copyOf(caps);
|
|
}
|
|
|
|
/** Whether any configured profile opts into a git-forge token (required for {@link Capability#SELF_PR}). */
|
|
private boolean hasGitTokenProfile() {
|
|
return profileConfigs().stream().anyMatch(BridgedConfig.Profile::hasGitToken);
|
|
}
|
|
|
|
// --- CB-117 reap predicate (opencode prefix), kept for direct unit testing -----------------
|
|
|
|
/**
|
|
* Whether {@code name} is an opencode bridge worker started by a <em>different</em> process than
|
|
* {@code currentNonce}. A thin {@code opencode}-prefix binding of
|
|
* {@link HerdrPeerLauncher#isForeignWorker(String, String, String)}.
|
|
*/
|
|
static boolean isForeignWorker(String name, String currentNonce) {
|
|
return HerdrPeerLauncher.isForeignWorker(NAME_PREFIX, name, currentNonce);
|
|
}
|
|
}
|