@bridge_gpt/mcp-server 0.2.36 → 0.2.38

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.
Files changed (92) hide show
  1. package/README.md +48 -8
  2. package/build/base-url.js +79 -0
  3. package/build/bridge-api-urls.js +9 -0
  4. package/build/chain-orchestrator.js +93 -15
  5. package/build/claude-user-config-doctor.js +317 -0
  6. package/build/commands.generated.js +2 -1
  7. package/build/conductor/bridge-api-client.js +178 -4
  8. package/build/conductor-bin.js +1 -1
  9. package/build/conductor-bundle-artifacts.js +7 -6
  10. package/build/credential-store.js +205 -4
  11. package/build/direct-ticket-tools.js +70 -0
  12. package/build/doctor.js +239 -80
  13. package/build/executor/cli.js +51 -1
  14. package/build/executor/credentials.js +1 -7
  15. package/build/executor/deps.js +18 -1
  16. package/build/executor/env.js +51 -25
  17. package/build/executor/heartbeat.js +138 -17
  18. package/build/executor/http-client.js +49 -8
  19. package/build/executor/job-errors.js +4 -0
  20. package/build/executor/job-runner.js +422 -22
  21. package/build/executor/observation.js +130 -0
  22. package/build/executor/permissions.js +104 -8
  23. package/build/executor/preflight.js +32 -0
  24. package/build/executor/runner.js +8 -0
  25. package/build/executor/test-clock.js +67 -3
  26. package/build/executor/types.js +4 -1
  27. package/build/executor/worker-command.js +11 -3
  28. package/build/executor/worker-config-isolation.js +287 -0
  29. package/build/executor/worker-finalization.js +68 -14
  30. package/build/executor/worktree.js +46 -4
  31. package/build/index.js +614 -244
  32. package/build/init.js +363 -73
  33. package/build/install-bridge.js +568 -80
  34. package/build/launcher-config-inspection.js +351 -0
  35. package/build/mcp-invoke.js +49 -6
  36. package/build/mcp-provisioning.js +30 -7
  37. package/build/mcp-registration-doctor.js +14 -5
  38. package/build/notifications.js +553 -0
  39. package/build/pipeline-orchestrator.js +146 -4
  40. package/build/pipeline-utils.js +3 -0
  41. package/build/pipelines.generated.js +22 -9
  42. package/build/plan-execution-ledger.js +550 -0
  43. package/build/plan-phase-routing.js +272 -0
  44. package/build/plane/alembic-head.js +110 -0
  45. package/build/plane/build-freshness.js +167 -0
  46. package/build/plane/cli.js +480 -0
  47. package/build/plane/defaults.js +266 -0
  48. package/build/plane/manifest.js +377 -0
  49. package/build/plane/member-logs.js +147 -0
  50. package/build/plane/member-roster.js +147 -0
  51. package/build/plane/preflight.js +289 -0
  52. package/build/plane/shutdown.js +195 -0
  53. package/build/plane/status.js +125 -0
  54. package/build/plane/supervisor.js +569 -0
  55. package/build/plane/test-fakes.js +156 -0
  56. package/build/plane/types.js +75 -0
  57. package/build/readme.generated.js +1 -1
  58. package/build/run-unit-tests-launcher.js +2 -0
  59. package/build/setup-epic.js +662 -27
  60. package/build/sfcc/log-gate.js +38 -11
  61. package/build/sfcc/log-query.js +55 -15
  62. package/build/sfcc/ocapi-shape.js +70 -14
  63. package/build/sfcc/output.js +41 -11
  64. package/build/sfcc/permissions.js +24 -2
  65. package/build/sfcc/read-body.js +92 -0
  66. package/build/sfcc/read-projection.js +185 -0
  67. package/build/sfcc/read-result.js +158 -0
  68. package/build/sfcc/reads-custom-object-def.js +57 -34
  69. package/build/sfcc/reads-site-preference.js +86 -33
  70. package/build/sfcc/reads-system-object.js +50 -38
  71. package/build/sfcc/sfcc-result.js +106 -0
  72. package/build/sfcc/tool-wrapper.js +56 -13
  73. package/build/sfcc/write-grants.js +45 -22
  74. package/build/sfcc/write-guard.js +21 -13
  75. package/build/sfcc/write-result.js +71 -15
  76. package/build/sfcc/write-tool-common.js +126 -32
  77. package/build/sfcc/writes-custom-object-def.js +6 -2
  78. package/build/sfcc/writes-system-object.js +11 -50
  79. package/build/start-tickets-prereqs.js +129 -0
  80. package/build/start-tickets.js +17 -13
  81. package/build/ticket-backend-metadata.js +59 -0
  82. package/build/ticket-key-utils.js +92 -0
  83. package/build/tool-error-envelope.js +71 -0
  84. package/build/tool-surface-gating.js +72 -0
  85. package/build/update-status.js +102 -0
  86. package/build/upgrade-advice.js +47 -0
  87. package/build/upgrade-cli.js +417 -101
  88. package/build/version.generated.js +1 -1
  89. package/build/worktree-core.js +73 -0
  90. package/docs/CONDUCTOR.md +23 -8
  91. package/package.json +3 -3
  92. package/pipelines/implement-ticket.json +15 -5
@@ -13,9 +13,10 @@
13
13
  * FRESH object from a strict allowlist of non-secret operational keys — it
14
14
  * never copies arbitrary `process.env`, so credentials/tokens/headers cannot
15
15
  * leak into the spawned `claude` process. The `mcp-invoke` shim (a separate
16
- * process) resolves `BAPI_API_KEY` itself at call time from
17
- * `~/.config/bridge/credentials.json` — this worker environment never carries
18
- * it.
16
+ * process) resolves `BAPI_API_KEY` itself at call time from the credential
17
+ * store under the worker's redirected `XDG_CONFIG_HOME`the executor seeds a
18
+ * repo-scoped store into the per-job isolation directory (see
19
+ * `worker-config-isolation.ts`) — so this worker environment never carries it.
19
20
  */
20
21
  import { PR_BASE_BRANCH_ENV_VAR } from "../pr-base-contract.js";
21
22
  /** Non-secret operational keys forwarded to the worker when present. */
@@ -31,12 +32,19 @@ const ALLOWED_ENV_KEYS = [
31
32
  "LANG",
32
33
  "LC_ALL",
33
34
  "TERM",
34
- "XDG_CONFIG_HOME",
35
35
  "NO_COLOR",
36
36
  ];
37
37
  /** Substrings that mark a key as secret-bearing (case-insensitive). */
38
38
  const SECRET_NAME_FRAGMENTS = ["TOKEN", "SECRET", "PASSWORD", "API_KEY"];
39
- /** Explicit deny-list of known secret / MCP / conductor keys. */
39
+ /**
40
+ * Explicit deny-list of known secret / MCP / conductor keys.
41
+ *
42
+ * BAPI-731 adds the two Claude configuration paths. They are denied on the
43
+ * INHERITED-COPY path and then set explicitly from executor-computed values
44
+ * below, following the `BAPI_BASE_BRANCH` precedent — an operator's
45
+ * `CLAUDE_CONFIG_DIR` or `XDG_CONFIG_HOME` must never be able to redirect a
46
+ * worker back at the operator's own configuration and undo isolation.
47
+ */
40
48
  const EXPLICIT_DENY_KEYS = [
41
49
  "BRIDGE_MCP_PROFILE",
42
50
  "CONDUCTOR_NODE_PATH",
@@ -45,6 +53,8 @@ const EXPLICIT_DENY_KEYS = [
45
53
  "OPENAI_API_KEY",
46
54
  "GITHUB_TOKEN",
47
55
  "GH_TOKEN",
56
+ "CLAUDE_CONFIG_DIR",
57
+ "XDG_CONFIG_HOME",
48
58
  ];
49
59
  /**
50
60
  * True only when `key` is a safe, allowlisted operational key. Exported so the
@@ -72,24 +82,7 @@ export function isExecutorEnvKeyAllowed(key) {
72
82
  return false;
73
83
  return true;
74
84
  }
75
- /**
76
- * Build the secret-free worker environment: a fresh object containing only the
77
- * allowlisted keys that are present (and non-undefined) in `parentEnv`, plus the
78
- * literal `BRIDGE_SKIP_PREPUSH=\"1\"` (BAPI-551). This is a constant, never read
79
- * from `parentEnv` — the worker's advisory local pre-push suite (~70s) is
80
- * redundant for a conductor worker, since its PR is CI-gated by the merge gate
81
- * anyway, and the delay was observed causing a worker to background the push and
82
- * exit before it landed. `BAPI_CONDUCTOR_*` identity keys remain intentionally
83
- * absent from the worker env (TDD §7) — this literal does not change that.
84
- *
85
- * BAPI-586: when `effectiveBaseBranch` is provided (the validated run base for
86
- * this job), it is injected as `BAPI_BASE_BRANCH` so the worker can run
87
- * `gh pr create --base "$BAPI_BASE_BRANCH"` deterministically. It is set from the
88
- * EXPLICIT job value, NOT copied from `parentEnv` (`BAPI_BASE_BRANCH` is not in
89
- * the allowlist), so any inherited value cannot override the run base and no
90
- * unrelated `BAPI_*` / secret value can leak in via this key.
91
- */
92
- export function buildExecutorWorkerEnv(parentEnv, effectiveBaseBranch) {
85
+ export function buildExecutorWorkerEnv(parentEnv, options = {}) {
93
86
  const env = {};
94
87
  for (const key of ALLOWED_ENV_KEYS) {
95
88
  if (!isExecutorEnvKeyAllowed(key))
@@ -100,8 +93,41 @@ export function buildExecutorWorkerEnv(parentEnv, effectiveBaseBranch) {
100
93
  }
101
94
  }
102
95
  env.BRIDGE_SKIP_PREPUSH = "1";
103
- if (typeof effectiveBaseBranch === "string" && effectiveBaseBranch.length > 0) {
104
- env[PR_BASE_BRANCH_ENV_VAR] = effectiveBaseBranch;
96
+ if (typeof options.effectiveBaseBranch === "string" &&
97
+ options.effectiveBaseBranch.length > 0) {
98
+ env[PR_BASE_BRANCH_ENV_VAR] = options.effectiveBaseBranch;
99
+ }
100
+ // --- Worker config isolation (BAPI-731) --------------------------------
101
+ // Both paths are assigned from EXPLICIT executor-computed values and are on
102
+ // `EXPLICIT_DENY_KEYS`, so the inherited-copy loop above can never supply them.
103
+ // That ordering is the isolation guarantee: an operator `CLAUDE_CONFIG_DIR` or
104
+ // `XDG_CONFIG_HOME` cannot redirect the worker back at the operator's own
105
+ // configuration (and its `projects[...].mcpServers` map).
106
+ if (typeof options.claudeConfigDir === "string" && options.claudeConfigDir.length > 0) {
107
+ env.CLAUDE_CONFIG_DIR = options.claudeConfigDir;
108
+ }
109
+ if (typeof options.xdgConfigHome === "string" && options.xdgConfigHome.length > 0) {
110
+ env.XDG_CONFIG_HOME = options.xdgConfigHome;
111
+ }
112
+ // The ONE deliberately-injected credential, and a genuine exception to this
113
+ // module's otherwise absolute secret-free rule. It is injected because
114
+ // isolation REMOVES the worker's previous authentication path: a worker used to
115
+ // authenticate as a side effect of inheriting the operator's `HOME` and
116
+ // `~/.claude.json`, and that inheritance is exactly the defect being closed.
117
+ // Per the measured inventory (`docs/claude/claude-cli-config-isolation-inventory.md`,
118
+ // finding 4) an operator OAuth/subscription login does NOT follow an isolated
119
+ // `CLAUDE_CONFIG_DIR`, while `ANTHROPIC_API_KEY` does — so this is the only
120
+ // supported way an isolated worker can authenticate at all.
121
+ //
122
+ // It is passed from an EXPLICIT executor-resolved value rather than copied
123
+ // (`ANTHROPIC_API_KEY` remains on `EXPLICIT_DENY_KEYS` and still matches the
124
+ // `API_KEY` secret fragment), so it travels only when the isolation strategy
125
+ // deliberately supplies it, never as ambient leakage. Nothing writes it to
126
+ // disk: the only file seeded into the isolated directory is the repo-scoped
127
+ // Bridge credential store (see `worker-config-isolation.ts`), which never
128
+ // carries the Anthropic key.
129
+ if (typeof options.anthropicApiKey === "string" && options.anthropicApiKey.length > 0) {
130
+ env.ANTHROPIC_API_KEY = options.anthropicApiKey;
105
131
  }
106
132
  return env;
107
133
  }
@@ -12,40 +12,83 @@
12
12
  *
13
13
  * After abandonment the loop stops all further heartbeat / telemetry / mutation
14
14
  * work so `runClaimedJob` never reports success after losing ownership.
15
+ *
16
+ * BAPI-731 adds two behaviors, neither of which may weaken the above:
17
+ *
18
+ * - PUSH-TRIGGERED PROMPTNESS (WS-H). The loop samples the LOCAL
19
+ * remote-tracking marker for the job's branch on a short sub-interval tick.
20
+ * When the marker first appears or changes, the worker has pushed, so the
21
+ * loop flushes one immediate heartbeat instead of waiting up to a full
22
+ * interval. This is TELEMETRY ONLY (R14): the marker is never persisted, never
23
+ * reported as `last_commit_sha`, and never drives an executor decision — the
24
+ * reconciler remains the sole consumer and the sole transition authority
25
+ * (`docs/claude/conductor-standing-rules.md`).
26
+ *
27
+ * - SERVER STOP (WS-B executor half). A heartbeat response carrying the
28
+ * server's stop request kills the owned worker like `stale_claim` does, but
29
+ * reports a DISTINCT outcome so the runner can send a server-cancelled
30
+ * terminal report rather than classifying a deliberate cancellation as a
31
+ * worker crash. Inert until the reconciler ticket ships the field.
32
+ *
33
+ * Every heartbeat — initial, periodic, and push-triggered — goes through the ONE
34
+ * `sendHeartbeat` path below, so requests can never overlap or arrive out of
35
+ * order.
15
36
  */
16
37
  import { killOwnedProcess } from "./process.js";
38
+ import { normalizeHeartbeatResult } from "./types.js";
17
39
  /**
18
- * Run the heartbeat loop until the worker is done or ownership is abandoned. The
19
- * FIRST heartbeat fires promptly (no initial interval wait).
40
+ * How often the push detector samples the local remote-tracking ref. Bounded
41
+ * well under the heartbeat interval so a push is noticed promptly, and it is a
42
+ * purely local `git rev-parse` (no network), so the cost is negligible.
43
+ */
44
+ export const PUSH_DETECT_INTERVAL_MS = 5_000;
45
+ /**
46
+ * Run the heartbeat loop until the worker is done, ownership is abandoned, or
47
+ * the server requests a stop. The FIRST heartbeat fires promptly (no initial
48
+ * interval wait).
20
49
  */
21
50
  export async function runHeartbeatLoop(params) {
22
51
  const { job, httpClient, options, deps, ownership, proc, observation } = params;
23
- let first = true;
24
- while (!ownership.abandoned && !params.isDone()) {
25
- if (!first) {
26
- await deps.sleep(options.heartbeatIntervalMs);
27
- if (ownership.abandoned || params.isDone())
28
- break;
29
- }
30
- first = false;
52
+ const pushDetectIntervalMs = params.pushDetectIntervalMs ?? PUSH_DETECT_INTERVAL_MS;
53
+ // Baseline for push detection. `undefined` means "not yet sampled": the FIRST
54
+ // successful sample only establishes the baseline and never itself triggers a
55
+ // flush, so a branch that was already pushed before this loop started does not
56
+ // manufacture a spurious immediate beat.
57
+ let remoteMarker;
58
+ let remoteBaselineEstablished = false;
59
+ /**
60
+ * The single heartbeat send path. Returns the loop outcome when the beat is
61
+ * terminal, or `null` to continue. Never called concurrently with itself:
62
+ * every caller awaits it before doing anything else.
63
+ */
64
+ const sendHeartbeat = async () => {
31
65
  const git = await params.collectTelemetry();
32
66
  observation.setGitTelemetry(git);
33
67
  const residue = observation.snapshot();
34
- const outcome = await httpClient.heartbeat(job, {
68
+ const result = normalizeHeartbeatResult(await httpClient.heartbeat(job, {
69
+ // Unconditionally the LOCAL head — never the remote marker. This is what
70
+ // preserves the BAPI-704 semantic that a zero-commit job reports the base
71
+ // SHA paired with `local_commit_count: 0`.
35
72
  local_commit_count: git.local_commit_count,
36
73
  last_commit_sha: git.last_commit_sha,
37
74
  telemetry: residue,
38
- });
39
- if (outcome === "updated") {
75
+ }));
76
+ // Checked BEFORE the outcome switch: a stop request is authoritative even
77
+ // when it rides along with a non-`updated` outcome.
78
+ if (result.stop_requested === true) {
79
+ killOwnedProcess(proc, "server_stop");
80
+ return { kind: "server_stop" };
81
+ }
82
+ if (result.outcome === "updated") {
40
83
  ownership.lastSuccessfulHeartbeatAt = deps.now();
41
- continue;
84
+ return null;
42
85
  }
43
- if (outcome === "stale_claim") {
86
+ if (result.outcome === "stale_claim") {
44
87
  // Definitive dead claim: kill the worker and stop touching the worktree.
45
88
  killOwnedProcess(proc, "stale_claim");
46
89
  ownership.abandoned = true;
47
90
  ownership.abandonReason = "stale_claim";
48
- break;
91
+ return { kind: "abandoned" };
49
92
  }
50
93
  // retry_later / fatal_http_error: keep the worker alive within the dead-man
51
94
  // window; SIGKILL + abandon once 2 minutes pass without a successful beat.
@@ -53,7 +96,85 @@ export async function runHeartbeatLoop(params) {
53
96
  killOwnedProcess(proc, "deadman");
54
97
  ownership.abandoned = true;
55
98
  ownership.abandonReason = "deadman";
56
- break;
99
+ return { kind: "abandoned" };
100
+ }
101
+ return null;
102
+ };
103
+ /**
104
+ * Wait out one heartbeat interval in short push-detector ticks, flushing an
105
+ * immediate beat if the worker pushes meanwhile. Returns an outcome when a
106
+ * flushed beat was terminal.
107
+ */
108
+ const waitIntervalWatchingForPush = async () => {
109
+ if (!params.collectRemoteMarker || pushDetectIntervalMs <= 0) {
110
+ await deps.sleep(options.heartbeatIntervalMs);
111
+ return null;
112
+ }
113
+ let waited = 0;
114
+ while (waited < options.heartbeatIntervalMs) {
115
+ const slice = Math.min(pushDetectIntervalMs, options.heartbeatIntervalMs - waited);
116
+ await deps.sleep(slice);
117
+ waited += slice;
118
+ if (ownership.abandoned || params.isDone())
119
+ return null;
120
+ let sample;
121
+ try {
122
+ sample = await params.collectRemoteMarker();
123
+ }
124
+ catch {
125
+ // Degrade-don't-throw: a failed local ref read is an observability gap,
126
+ // never a job failure.
127
+ sample = undefined;
128
+ }
129
+ if (sample === undefined)
130
+ continue;
131
+ if (!remoteBaselineEstablished) {
132
+ remoteBaselineEstablished = true;
133
+ remoteMarker = sample;
134
+ continue;
135
+ }
136
+ if (sample === remoteMarker)
137
+ continue;
138
+ // The marker moved: the worker pushed. A rewind/force-push moves it too and
139
+ // is treated identically — a flush trigger, never a gate or a decision.
140
+ remoteMarker = sample;
141
+ const outcome = await sendHeartbeat();
142
+ if (outcome)
143
+ return outcome;
144
+ if (ownership.abandoned || params.isDone())
145
+ return null;
146
+ }
147
+ return null;
148
+ };
149
+ let first = true;
150
+ while (!ownership.abandoned && !params.isDone()) {
151
+ if (!first) {
152
+ const waitOutcome = await waitIntervalWatchingForPush();
153
+ if (waitOutcome)
154
+ return waitOutcome;
155
+ if (ownership.abandoned || params.isDone())
156
+ break;
157
+ }
158
+ else {
159
+ // Establish the push baseline alongside the prompt first beat, so an
160
+ // already-pushed branch does not look like a brand-new push one tick later.
161
+ if (params.collectRemoteMarker) {
162
+ try {
163
+ const sample = await params.collectRemoteMarker();
164
+ if (sample !== undefined) {
165
+ remoteMarker = sample;
166
+ }
167
+ remoteBaselineEstablished = true;
168
+ }
169
+ catch {
170
+ remoteBaselineEstablished = true;
171
+ }
172
+ }
57
173
  }
174
+ first = false;
175
+ const outcome = await sendHeartbeat();
176
+ if (outcome)
177
+ return outcome;
58
178
  }
179
+ return ownership.abandoned ? { kind: "abandoned" } : { kind: "done" };
59
180
  }
@@ -12,6 +12,28 @@ function isOkBody(bodyText) {
12
12
  return false;
13
13
  }
14
14
  }
15
+ /**
16
+ * The server-published run-level stop field on a heartbeat response (BAPI-731,
17
+ * WS-B). Owned by the reconciler safety-plane ticket; until it ships the field is
18
+ * simply never present.
19
+ */
20
+ export const HEARTBEAT_STOP_FIELD = "stop_requested";
21
+ /**
22
+ * True ONLY for a literal `true` on the stop field. Anything else — absent,
23
+ * `false`, a string, a truthy non-boolean, or an unparseable body — means "no
24
+ * stop was requested", so an ambiguous body can never cancel a healthy run.
25
+ */
26
+ function parseStopRequested(bodyText) {
27
+ try {
28
+ const parsed = JSON.parse(bodyText);
29
+ if (!parsed || typeof parsed !== "object")
30
+ return false;
31
+ return parsed[HEARTBEAT_STOP_FIELD] === true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
15
37
  /**
16
38
  * Map a mutation (heartbeat/complete/fail) HTTP response onto the fencing
17
39
  * outcome. `allowInvalidResult` maps 422 to `invalid_job_result` (complete only).
@@ -92,14 +114,33 @@ export function createExecutorHttpClient(config) {
92
114
  // 5xx and any other unexpected status are retryable.
93
115
  return { kind: "retryable", error: `claim failed (HTTP ${status}): ${boundedDetail(text)}` };
94
116
  },
95
- heartbeat(job, payload) {
96
- return mutate(`/${job.id}/heartbeat`, {
97
- repo_name: job.repo_name,
98
- claim_token: job.claim_token,
99
- local_commit_count: payload.local_commit_count,
100
- last_commit_sha: payload.last_commit_sha,
101
- telemetry: payload.telemetry,
102
- }, false, job.repo_name);
117
+ // Heartbeat does NOT go through `mutate()`: it is the one mutation whose
118
+ // response BODY carries information beyond the outcome (the BAPI-731 stop
119
+ // request), so it maps the response itself instead of discarding the body.
120
+ async heartbeat(job, payload) {
121
+ let status;
122
+ let text;
123
+ try {
124
+ const res = await post(`/${job.id}/heartbeat`, {
125
+ repo_name: job.repo_name,
126
+ claim_token: job.claim_token,
127
+ local_commit_count: payload.local_commit_count,
128
+ last_commit_sha: payload.last_commit_sha,
129
+ telemetry: payload.telemetry,
130
+ }, job.repo_name);
131
+ status = res.status;
132
+ text = res.text;
133
+ }
134
+ catch {
135
+ // Transient network error — identical to the previous behavior; the
136
+ // caller's dead-man logic decides whether to keep the worker alive.
137
+ return { outcome: "fatal_http_error" };
138
+ }
139
+ const outcome = mapMutationResponse(status, text, false);
140
+ const result = { outcome };
141
+ if (parseStopRequested(text))
142
+ result.stop_requested = true;
143
+ return result;
103
144
  },
104
145
  complete(job, completion) {
105
146
  return mutate(`/${job.id}/complete`, {
@@ -4,6 +4,10 @@ export const WorktreeLostBeforePush = "WorktreeLostBeforePush";
4
4
  export const BranchMismatch = "BranchMismatch";
5
5
  export const WorkerFinalizationMissingRemoteBranchAndPr = "WorkerFinalizationMissingRemoteBranchAndPr";
6
6
  export const WorkerFinalizationPrBaseMismatch = "WorkerFinalizationPrBaseMismatch";
7
+ export const WorkerFinalizationSavedButUnfinalized = "WorkerFinalizationSavedButUnfinalized";
8
+ export const WorkerConfigIsolation = "ContractError.WorkerConfigIsolation";
9
+ export const PreSpawnVerification = "ContractError.PreSpawnVerification";
10
+ export const BridgeCredentialUnavailable = "ContractError.BridgeCredentialUnavailable";
7
11
  /** Bound a failure message so no unbounded/secret-bearing text is posted. */
8
12
  const ERROR_MESSAGE_MAX_CHARS = 300;
9
13
  /**