@browserstack/mcp-server 1.4.0-beta.2 → 1.5.0-beta.1

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 (44) hide show
  1. package/capability/loadtesting.capability-index.json +1754 -0
  2. package/capability/tm.capability-index.json +19793 -0
  3. package/dist/index.js +2 -5
  4. package/dist/server-factory.js +5 -5
  5. package/dist/tools/accessibility.js +2 -5
  6. package/dist/tools/capability-registry/bind.d.ts +29 -0
  7. package/dist/tools/capability-registry/bind.js +134 -0
  8. package/dist/tools/capability-registry/config.d.ts +62 -0
  9. package/dist/tools/capability-registry/config.js +218 -0
  10. package/dist/tools/capability-registry/discovery.d.ts +44 -0
  11. package/dist/tools/capability-registry/discovery.js +99 -0
  12. package/dist/tools/capability-registry/egress.d.ts +44 -0
  13. package/dist/tools/capability-registry/egress.js +128 -0
  14. package/dist/tools/capability-registry/index-loader.d.ts +119 -0
  15. package/dist/tools/capability-registry/index-loader.js +314 -0
  16. package/dist/tools/capability-registry/register.d.ts +34 -0
  17. package/dist/tools/capability-registry/register.js +354 -0
  18. package/dist/tools/capability-registry/resolve.d.ts +38 -0
  19. package/dist/tools/capability-registry/resolve.js +45 -0
  20. package/dist/tools/capability-registry/search.d.ts +65 -0
  21. package/dist/tools/capability-registry/search.js +342 -0
  22. package/dist/tools/capability-registry/types.d.ts +208 -0
  23. package/dist/tools/capability-registry/types.js +33 -0
  24. package/dist/tools/get-failure-logs.js +1 -3
  25. package/dist/tools/rca-agent.js +2 -5
  26. package/dist/tools/selfheal.js +2 -5
  27. package/dist/tools/testmanagement.js +15 -33
  28. package/package.json +3 -2
  29. package/dist/tools/ask-browserstack/central-oauth.d.ts +0 -114
  30. package/dist/tools/ask-browserstack/central-oauth.js +0 -271
  31. package/dist/tools/ask-browserstack/config.d.ts +0 -96
  32. package/dist/tools/ask-browserstack/config.js +0 -134
  33. package/dist/tools/ask-browserstack/egress.d.ts +0 -34
  34. package/dist/tools/ask-browserstack/egress.js +0 -31
  35. package/dist/tools/ask-browserstack/register.d.ts +0 -61
  36. package/dist/tools/ask-browserstack/register.js +0 -403
  37. package/dist/tools/ask-browserstack/relay.d.ts +0 -201
  38. package/dist/tools/ask-browserstack/relay.js +0 -577
  39. package/dist/tools/ask-browserstack/stream.d.ts +0 -116
  40. package/dist/tools/ask-browserstack/stream.js +0 -237
  41. package/dist/tools/ask-browserstack/types.d.ts +0 -196
  42. package/dist/tools/ask-browserstack/types.js +0 -10
  43. package/dist/tools/tool-handoff.d.ts +0 -37
  44. package/dist/tools/tool-handoff.js +0 -47
@@ -1,116 +0,0 @@
1
- /**
2
- * CONTRACT v2 (A1) — read the ask off the response, send the decision separately.
3
- *
4
- * WHY THIS REPLACES THE CALLBACK. `callback.ts` binds `127.0.0.1:<ephemeral>` and hands
5
- * Atlas the URL. That works only when Atlas is on the same loopback. For a real user it
6
- * cannot work at all: this server runs on their machine, and a laptop behind NAT is not
7
- * addressable from a pod in BrowserStack's cluster. There is no route, and no
8
- * configuration creates one. Every successful relay run to date used a locally-run
9
- * Atlas — the shape the v1 contract was written for, and not the shape a user is in.
10
- *
11
- * A1 inverts it. Both connections are outbound from here:
12
- *
13
- * 1. `POST /agent` — the response is an SSE stream carrying `run`, then any
14
- * `permission` asks, then exactly one `result`.
15
- * 2. `POST /agent/{run_id}/permission` — a fresh short request per decision.
16
- *
17
- * So NAT, firewalls and loopback stop mattering, because nothing ever dials in.
18
- *
19
- * WHAT THIS FILE DELIBERATELY DOES NOT DO: decide anything. Whether a human approved,
20
- * how an elicitation outcome maps to allow/deny, what the result looks like — all of
21
- * that stays in `relay.ts`, untouched, and is shared with the callback transport. This
22
- * is a pipe. Keeping the judgement out of the transport is why swapping A2 for A1 does
23
- * not risk the fail-closed behaviour.
24
- */
25
- import type { AgentRequest, PermissionAsk } from "./types.js";
26
- /** One SSE frame, already parsed. `data` is whatever JSON the frame carried. */
27
- export interface StreamEvent {
28
- event: string;
29
- data: unknown;
30
- /**
31
- * The HTTP status, carried ONLY on a `result` synthesised from a non-stream reply.
32
- *
33
- * Load-bearing, and it was a bug to omit it. `relay.ts` reads the status to tell a
34
- * rejected credential (401) from an account without the feature (403) from an
35
- * ordinary failure, and those produce three different sentences for the user. A
36
- * result event that dropped the status made every one of them read as a generic
37
- * error. On a real SSE stream the status is 200 by definition, so this is absent.
38
- */
39
- status?: number;
40
- }
41
- /**
42
- * CONTRACT v2 §4 — the whole-run guard, so a runaway run cannot hold a tool call open
43
- * forever. Deliberately generous: it is a backstop against a hung server, not a budget
44
- * for a human's attention, and the thing that actually bounds one approval is Atlas's
45
- * 300s gate.
46
- */
47
- export declare const WHOLE_RUN_TIMEOUT_MS = 1800000;
48
- export declare const EVENT_RUN = "run";
49
- export declare const EVENT_PERMISSION = "permission";
50
- export declare const EVENT_RESULT = "result";
51
- /** Atlas's `f"perm-{uuid.uuid4().hex}"`, and nothing else. */
52
- export declare const PERM_ID_PATTERN: RegExp;
53
- /**
54
- * Read an ask out of a `permission` frame's data, or return null.
55
- *
56
- * Came over from the transport this one replaced, and the reasons it existed did not
57
- * change with the transport — only the direction the ask arrives from did. It is not a
58
- * trust check on Atlas: it is what keeps a malformed frame from turning into a prompt
59
- * that cannot be honoured.
60
- *
61
- * A blank description is rejected rather than relayed: the description IS the whole of
62
- * what the human is shown, so an empty one is a prompt asking a person to approve
63
- * nothing. A `perm_id` off Atlas's shape is rejected because it is the only thing that
64
- * routes the answer back — the decision endpoint matches on it, so an id we could not
65
- * have received is an answer that can never be delivered.
66
- *
67
- * Only the four fields of CONTRACT §2 are carried forward. `op_key`, `method`, `path`
68
- * and `host` are Atlas-private (v1.1 §A) and it does not send them; if a future one
69
- * ever did, they would stop here rather than reach an elicitation prompt or a result.
70
- */
71
- export declare function parseAsk(data: unknown): PermissionAsk | null;
72
- /**
73
- * The transport seam: the shape `AgentTransport` had before A2 was removed, except that it
74
- * yields many events instead of returning one body. Injectable for the same reason that one
75
- * was: the tests must be able to drive a whole approval round trip without a socket.
76
- */
77
- export type AgentStreamTransport = (url: string, headers: Record<string, string>, body: AgentRequest) => AsyncIterable<StreamEvent>;
78
- /** Posts one decision. Separate seam because it is a separate connection. */
79
- export type DecisionTransport = (url: string, headers: Record<string, string>, body: {
80
- perm_id: string;
81
- decision: string;
82
- reason: string;
83
- }) => Promise<number>;
84
- /**
85
- * Split a buffer into complete SSE frames, returning the leftover.
86
- *
87
- * Exported for its own tests because chunk boundaries are where SSE parsers break: a
88
- * frame can arrive split across two reads, two frames can arrive in one read, and a
89
- * `data:` line can contain anything except a newline. Getting this wrong shows up as an
90
- * ask that is silently dropped — a write that never gets approved and never explains
91
- * why — so it is tested directly rather than only through the happy path.
92
- */
93
- export declare function splitFrames(buffer: string): {
94
- frames: string[];
95
- rest: string;
96
- };
97
- /**
98
- * Parse one frame. Returns null for anything that is not an event we can use —
99
- * including the heartbeat, which is a bare `:` comment and is SUPPOSED to be ignored
100
- * here: its only job is to be a read on the socket so the ingress does not time the
101
- * connection out while a human is thinking.
102
- */
103
- export declare function parseFrame(frame: string): StreamEvent | null;
104
- /**
105
- * A fetch-based streaming transport.
106
- *
107
- * `timeoutMs` bounds the WHOLE run, not one request — CONTRACT v2 §4 replaced the old
108
- * 330s outer rung because under A1 the stream lives for the run and may contain several
109
- * 300s approvals in series. The per-ask rung (270s elicitation inside Atlas's 300s gate)
110
- * is unchanged and still enforced where it belongs.
111
- */
112
- export declare function fetchAgentStreamTransport(timeoutMs?: number): AgentStreamTransport;
113
- /** The decision POST. 30s, because it is an ordinary short request. */
114
- export declare function fetchDecisionTransport(timeoutMs?: number): DecisionTransport;
115
- /** `POST /agent/{run_id}/permission`, built from the base URL the tool already resolved. */
116
- export declare function decisionUrl(agentUrl: string, runId: string): string;
@@ -1,237 +0,0 @@
1
- /**
2
- * CONTRACT v2 (A1) — read the ask off the response, send the decision separately.
3
- *
4
- * WHY THIS REPLACES THE CALLBACK. `callback.ts` binds `127.0.0.1:<ephemeral>` and hands
5
- * Atlas the URL. That works only when Atlas is on the same loopback. For a real user it
6
- * cannot work at all: this server runs on their machine, and a laptop behind NAT is not
7
- * addressable from a pod in BrowserStack's cluster. There is no route, and no
8
- * configuration creates one. Every successful relay run to date used a locally-run
9
- * Atlas — the shape the v1 contract was written for, and not the shape a user is in.
10
- *
11
- * A1 inverts it. Both connections are outbound from here:
12
- *
13
- * 1. `POST /agent` — the response is an SSE stream carrying `run`, then any
14
- * `permission` asks, then exactly one `result`.
15
- * 2. `POST /agent/{run_id}/permission` — a fresh short request per decision.
16
- *
17
- * So NAT, firewalls and loopback stop mattering, because nothing ever dials in.
18
- *
19
- * WHAT THIS FILE DELIBERATELY DOES NOT DO: decide anything. Whether a human approved,
20
- * how an elicitation outcome maps to allow/deny, what the result looks like — all of
21
- * that stays in `relay.ts`, untouched, and is shared with the callback transport. This
22
- * is a pipe. Keeping the judgement out of the transport is why swapping A2 for A1 does
23
- * not risk the fail-closed behaviour.
24
- */
25
- import logger from "../../logger.js";
26
- import { AskError } from "./config.js";
27
- /**
28
- * CONTRACT v2 §4 — the whole-run guard, so a runaway run cannot hold a tool call open
29
- * forever. Deliberately generous: it is a backstop against a hung server, not a budget
30
- * for a human's attention, and the thing that actually bounds one approval is Atlas's
31
- * 300s gate.
32
- */
33
- export const WHOLE_RUN_TIMEOUT_MS = 1_800_000;
34
- export const EVENT_RUN = "run";
35
- export const EVENT_PERMISSION = "permission";
36
- export const EVENT_RESULT = "result";
37
- /** Atlas's `f"perm-{uuid.uuid4().hex}"`, and nothing else. */
38
- export const PERM_ID_PATTERN = /^perm-[0-9a-f]{32}$/;
39
- /**
40
- * Read an ask out of a `permission` frame's data, or return null.
41
- *
42
- * Came over from the transport this one replaced, and the reasons it existed did not
43
- * change with the transport — only the direction the ask arrives from did. It is not a
44
- * trust check on Atlas: it is what keeps a malformed frame from turning into a prompt
45
- * that cannot be honoured.
46
- *
47
- * A blank description is rejected rather than relayed: the description IS the whole of
48
- * what the human is shown, so an empty one is a prompt asking a person to approve
49
- * nothing. A `perm_id` off Atlas's shape is rejected because it is the only thing that
50
- * routes the answer back — the decision endpoint matches on it, so an id we could not
51
- * have received is an answer that can never be delivered.
52
- *
53
- * Only the four fields of CONTRACT §2 are carried forward. `op_key`, `method`, `path`
54
- * and `host` are Atlas-private (v1.1 §A) and it does not send them; if a future one
55
- * ever did, they would stop here rather than reach an elicitation prompt or a result.
56
- */
57
- export function parseAsk(data) {
58
- if (typeof data !== "object" || data === null || Array.isArray(data))
59
- return null;
60
- const record = data;
61
- const permId = record.perm_id;
62
- const description = record.description;
63
- if (typeof permId !== "string" || !PERM_ID_PATTERN.test(permId))
64
- return null;
65
- if (typeof description !== "string" || !description.trim())
66
- return null;
67
- return {
68
- perm_id: permId,
69
- product: typeof record.product === "string" ? record.product : "",
70
- mode: typeof record.mode === "string" ? record.mode : "",
71
- description,
72
- };
73
- }
74
- /**
75
- * Split a buffer into complete SSE frames, returning the leftover.
76
- *
77
- * Exported for its own tests because chunk boundaries are where SSE parsers break: a
78
- * frame can arrive split across two reads, two frames can arrive in one read, and a
79
- * `data:` line can contain anything except a newline. Getting this wrong shows up as an
80
- * ask that is silently dropped — a write that never gets approved and never explains
81
- * why — so it is tested directly rather than only through the happy path.
82
- */
83
- export function splitFrames(buffer) {
84
- const frames = [];
85
- let rest = buffer;
86
- for (;;) {
87
- const idx = rest.indexOf("\n\n");
88
- if (idx === -1)
89
- break;
90
- frames.push(rest.slice(0, idx));
91
- rest = rest.slice(idx + 2);
92
- }
93
- return { frames, rest };
94
- }
95
- /**
96
- * Parse one frame. Returns null for anything that is not an event we can use —
97
- * including the heartbeat, which is a bare `:` comment and is SUPPOSED to be ignored
98
- * here: its only job is to be a read on the socket so the ingress does not time the
99
- * connection out while a human is thinking.
100
- */
101
- export function parseFrame(frame) {
102
- let event = "";
103
- const dataLines = [];
104
- for (const line of frame.split("\n")) {
105
- if (line.startsWith(":"))
106
- continue; // comment / heartbeat
107
- if (line.startsWith("event:"))
108
- event = line.slice(6).trim();
109
- else if (line.startsWith("data:"))
110
- dataLines.push(line.slice(5).trim());
111
- }
112
- if (!event)
113
- return null;
114
- if (dataLines.length === 0)
115
- return { event, data: null };
116
- try {
117
- return { event, data: JSON.parse(dataLines.join("\n")) };
118
- }
119
- catch {
120
- // A frame we cannot read is not a frame we may guess at. Dropping it is safe
121
- // because the only consequence is that an ask goes unanswered and the gate denies
122
- // on its own expiry — never that something is approved.
123
- logger.warn("askBrowserStackAI: unparseable stream frame, ignoring");
124
- return null;
125
- }
126
- }
127
- /**
128
- * A fetch-based streaming transport.
129
- *
130
- * `timeoutMs` bounds the WHOLE run, not one request — CONTRACT v2 §4 replaced the old
131
- * 330s outer rung because under A1 the stream lives for the run and may contain several
132
- * 300s approvals in series. The per-ask rung (270s elicitation inside Atlas's 300s gate)
133
- * is unchanged and still enforced where it belongs.
134
- */
135
- export function fetchAgentStreamTransport(timeoutMs = WHOLE_RUN_TIMEOUT_MS) {
136
- return function stream(url, headers, body) {
137
- return {
138
- async *[Symbol.asyncIterator]() {
139
- const controller = new AbortController();
140
- const timer = setTimeout(() => controller.abort(), timeoutMs);
141
- try {
142
- let response;
143
- try {
144
- response = await fetch(url, {
145
- method: "POST",
146
- headers: { ...headers, Accept: "text/event-stream" },
147
- body: JSON.stringify(body),
148
- // A redirect from an authenticated API is usually a login bounce, and
149
- // following it turns a clear 401 into a 200 carrying an HTML page.
150
- redirect: "manual",
151
- signal: controller.signal,
152
- });
153
- }
154
- catch {
155
- // The same sentence the request/response transport gives, and for the same
156
- // reason: the upstream detail ("connection reset", "fetch failed") names our
157
- // plumbing rather than anything the reader can act on, and letting it through
158
- // once already produced a result that read like the user had done something
159
- // wrong. What they need to know is that BrowserStack was not reachable.
160
- throw new AskError("BrowserStack AI could not be reached");
161
- }
162
- const contentType = response.headers.get("content-type") || "";
163
- // GRACEFUL DEGRADE, and the reason A1 is safe to ship before Atlas has it
164
- // everywhere: an Atlas that does not know `mode: "stream"` answers with an
165
- // ordinary JSON body (a read-only run, `permission_relay.reason: "disabled"`).
166
- // Yielding it as a single `result` means the caller needs no version
167
- // negotiation and no flag — it gets a correct read-only answer instead of a
168
- // parse failure against a body that was never SSE.
169
- if (contentType.includes("json")) {
170
- const parsed = await response.json().catch(() => null);
171
- yield {
172
- event: EVENT_RESULT,
173
- data: parsed,
174
- status: response.status,
175
- };
176
- return;
177
- }
178
- if (!response.ok || !response.body) {
179
- // Neither a stream nor a JSON result. Surface it as a first-class failure
180
- // rather than an empty iteration, which the caller could not tell apart
181
- // from "the run finished and said nothing".
182
- throw new AskError(`BrowserStack AI refused the stream (HTTP ${response.status}).`);
183
- }
184
- const decoder = new TextDecoder();
185
- let buffer = "";
186
- for await (const chunk of response.body) {
187
- buffer += decoder.decode(chunk, { stream: true });
188
- const { frames, rest } = splitFrames(buffer);
189
- buffer = rest;
190
- for (const frame of frames) {
191
- const parsed = parseFrame(frame);
192
- if (parsed)
193
- yield parsed;
194
- }
195
- }
196
- // A trailing frame with no terminating blank line still counts.
197
- const tail = parseFrame(buffer);
198
- if (tail)
199
- yield tail;
200
- }
201
- finally {
202
- clearTimeout(timer);
203
- }
204
- },
205
- };
206
- };
207
- }
208
- /** The decision POST. 30s, because it is an ordinary short request. */
209
- export function fetchDecisionTransport(timeoutMs = 30_000) {
210
- return async (url, headers, body) => {
211
- const controller = new AbortController();
212
- const timer = setTimeout(() => controller.abort(), timeoutMs);
213
- try {
214
- const response = await fetch(url, {
215
- method: "POST",
216
- headers,
217
- body: JSON.stringify(body),
218
- redirect: "manual",
219
- signal: controller.signal,
220
- });
221
- return response.status;
222
- }
223
- catch {
224
- // The gate on the far side is still waiting and will deny on its own expiry, so
225
- // a lost decision is safe — it is never an approval. 0 says "never delivered" so
226
- // the caller can say that rather than implying a human refused.
227
- return 0;
228
- }
229
- finally {
230
- clearTimeout(timer);
231
- }
232
- };
233
- }
234
- /** `POST /agent/{run_id}/permission`, built from the base URL the tool already resolved. */
235
- export function decisionUrl(agentUrl, runId) {
236
- return `${agentUrl.replace(/\/+$/, "")}/${encodeURIComponent(runId)}/permission`;
237
- }
@@ -1,196 +0,0 @@
1
- /**
2
- * CONTRACT v1, expressed as types.
3
- *
4
- * Everything in this file is one half of a wire format whose other half is being written
5
- * against `~/.claude/orchestration/shared/CONTRACT.md` by a different session, in a
6
- * different repo, at the same time. A field renamed here to read better is a field the
7
- * other half will never send. Nothing here changes without changing that document first.
8
- */
9
- export declare const PRODUCTS: readonly ["tm", "a11y", "tra"];
10
- export type Product = (typeof PRODUCTS)[number];
11
- /** CONTRACT §2 — what Atlas emits on the run's stream when its gate needs a human. */
12
- export interface PermissionAsk {
13
- /**
14
- * Atlas's own `perm-<32 hex>` uuid4, carried, never re-minted.
15
- *
16
- * Commit 737f6f57 replaced a per-Bridge sequential scheme with this one after ids
17
- * collided across pods and left a turn blocked until its 300s timeout. A second id
18
- * scheme on this side would reintroduce that bug on a new axis, so we echo theirs.
19
- */
20
- perm_id: string;
21
- product: string;
22
- /** "ask-always" | "ask-once" — Atlas's vocabulary, not re-interpreted here. */
23
- mode: string;
24
- /**
25
- * The model's `thought`: product language, route-free.
26
- *
27
- * The privacy boundary is that `op_key`, `method`, `path` and `host` stay inside Atlas's
28
- * private record. This field is the whole of what a human is shown.
29
- */
30
- description: string;
31
- }
32
- export type Decision = "allow" | "deny";
33
- /**
34
- * CONTRACT §2/§7. Advisory ONLY — `decision` alone decides whether the action proceeds.
35
- * It exists so a result can distinguish "a human said no" from "no human was there".
36
- */
37
- export type DecisionReason = "" | "declined" | "cancelled" | "no_human" | "timeout" | "error";
38
- /** CONTRACT §2 — the body we POST to `/agent/{run_id}/permission` to answer one ask. */
39
- export interface PermissionDecision {
40
- perm_id: string;
41
- decision: Decision;
42
- reason: DecisionReason;
43
- }
44
- /** CONTRACT §1 — the one new optional field on `POST /agent`. */
45
- /**
46
- * The `permission_relay` block — one shape, because there is one transport.
47
- *
48
- * `{ mode: "stream" }` is A1 / CONTRACT v2: nothing to address and nothing to
49
- * authenticate inbound, because nothing dials in. A2's `{ callback_url, token }` was
50
- * removed on both halves (v2 §7.4) — it could only ever reach a co-located caller, and
51
- * an Atlas that still sees that shape now reports the relay as `disabled` rather than
52
- * trying it.
53
- *
54
- * `mode` stays optional rather than required so this stays a superset of the block an
55
- * older Atlas will simply ignore: the field's ABSENCE is what selects the read-only
56
- * gate, and that has to remain expressible.
57
- */
58
- export interface PermissionRelay {
59
- mode?: string;
60
- }
61
- /**
62
- * CONTRACT §1 — the `POST /agent` body.
63
- *
64
- * `permission_relay` is OMITTED ENTIRELY, not sent empty or null, when the client cannot
65
- * elicit: its absence is what selects Atlas's read-only `HeadlessGate`, which is today's
66
- * byte-identical behaviour and the opencode/goose path.
67
- */
68
- export interface AgentRequest {
69
- task: string;
70
- product: string;
71
- /**
72
- * Who the run is for (CONTRACT v1.2 §3).
73
- *
74
- * The shared delegation token authenticates the caller but not the principal
75
- * (`principal_verified=false`), so Atlas reads the acting user from here. Omitted entirely
76
- * when no username is configured — never sent as `""`.
77
- *
78
- * Note the asymmetry and do not try to close it: on this path a caller can CLAIM any
79
- * `user_id`. That is Atlas's documented design for the shared-token route; a signed
80
- * principal requires the central-JWT path, which is out of scope.
81
- */
82
- user_id?: string;
83
- permission_relay?: PermissionRelay;
84
- }
85
- /** CONTRACT §5 — one entry in an approval trail, in the order the asks arrived. */
86
- export interface ApprovalRecord {
87
- description: string;
88
- decision: Decision;
89
- reason: string;
90
- /**
91
- * Did this approved step's request actually land? (Atlas task 3 §1.)
92
- *
93
- * `true` only when the entry was `allow` AND its request then returned 2xx. Everything
94
- * else — a dead port, a 4xx, a 5xx, an async-dispatch refusal — resolves to `false`:
95
- * unknown fails toward not-applied, never the other way.
96
- *
97
- * ONLY ATLAS CAN KNOW THIS. The gate returns before any request is sent, which was the
98
- * whole of D2; the fact is written by the tool layer into the same record object the gate
99
- * keeps by reference, so it correlates by object identity rather than by position.
100
- *
101
- * ABSENT means not reported (an Atlas that predates this, or our own elicitation trail,
102
- * which has no way to know). Never render an absent value as a measured `false`.
103
- */
104
- applied?: boolean;
105
- /**
106
- * A human-readable phrase for this entry.
107
- *
108
- * Exists because `allow` + `applied: false` — approved, then the request failed — is a
109
- * genuinely different thing to tell a person than a refusal, and the two must not read
110
- * alike. Derived, never sent by anyone.
111
- */
112
- outcome?: string;
113
- }
114
- /**
115
- * Whether the approval channel was offered to Atlas at all, and if not, why not.
116
- *
117
- * Three different facts about this deployment and this client, each needing a different
118
- * thing from whoever reads the result — so they are three values rather than one boolean.
119
- */
120
- export type RelayMode =
121
- /** `permission_relay` was sent and the run's stream carried the asks. */
122
- "offered"
123
- /** The client declares no `elicitation` capability, so nobody could be prompted. */
124
- | "no_human"
125
- /** This process is the hosted multi-tenant server, which cannot prompt a human. */
126
- | "remote_mode";
127
- export declare const ASK_STATUSES: readonly ["ok", "blocked", "error", "rate_limited"];
128
- export type AskStatus = (typeof ASK_STATUSES)[number];
129
- /** CONTRACT §5 — the single tool result. */
130
- export interface AskResult {
131
- ok: boolean;
132
- status: AskStatus;
133
- answer: unknown;
134
- /**
135
- * THE AUTHORITATIVE approval trail: Atlas's whenever it supplied one, ours otherwise.
136
- *
137
- * Atlas's wins because it is the only side that can populate `applied`, and because it
138
- * records what happened to the STEP — an ask answered without a prompt appearing (a
139
- * 401'd probe, a shape rejection) is a denial there and nothing at all here.
140
- */
141
- approvals: ApprovalRecord[];
142
- /** Which side produced `approvals`, so a reader never has to infer it. */
143
- approvals_source: "atlas" | "mcp";
144
- /**
145
- * OUR trail: one entry per prompt this server actually put in front of a human.
146
- *
147
- * Kept beside `approvals` rather than folded into it, because where the two disagree the
148
- * disagreement is the signal. An entry Atlas records as a denial with nothing here means a
149
- * ask was answered without any prompt appearing — which is what an attacker probing
150
- * the loopback port looks like.
151
- */
152
- elicitations: ApprovalRecord[];
153
- needs_approval: unknown[];
154
- /**
155
- * Atlas's own verdict: this run stopped on a refusal AND something had already changed.
156
- *
157
- * READ, NEVER DERIVED. Atlas computes it because only Atlas knows `applied`, and it sends
158
- * the field whenever a relay gate ran — including `false`, including with an empty trail.
159
- *
160
- * `null` means NOT REPORTED, which is not the same as `false`: either no gate ran (so
161
- * Atlas has nothing to say about applications) or this Atlas predates the field. Treating
162
- * it as `false` would assert something nobody measured.
163
- */
164
- applied_before_stop: boolean | null;
165
- /**
166
- * Why a write may have been refused.
167
- *
168
- * `reason` is ATLAS'S OWN when it reported one (CONTRACT v1.1 §D: `"" | disabled |
169
- * host_not_allowed | malformed`), and ours — `no_human` (§7's last row) — when the client
170
- * could not be prompted at all, which Atlas never learns about because we omit the block.
171
- * Typed as a plain string rather than a union because an Atlas newer than this build may
172
- * name a reason this one has never heard of, and an unrecognised reason must degrade to a
173
- * sentence, not throw.
174
- */
175
- permission_relay: {
176
- used: boolean;
177
- reason: string;
178
- detail: string;
179
- };
180
- /**
181
- * Atlas's public payload, verbatim.
182
- *
183
- * The mapped fields above are the contract; this is the belt to their braces. The two
184
- * halves are being built in parallel, so a field named slightly differently on the other
185
- * side would otherwise silently become `null` here rather than reaching the caller.
186
- */
187
- atlas_response: unknown;
188
- /**
189
- * Why the call failed, when it did.
190
- *
191
- * Ours when egress never completed; otherwise Atlas's own `error` string, which its
192
- * `public()` includes ONLY when non-empty (v1.1 §B). Lifted out of `atlas_response` so a
193
- * caller reading the top level is told why rather than having to go looking.
194
- */
195
- error?: string;
196
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * CONTRACT v1, expressed as types.
3
- *
4
- * Everything in this file is one half of a wire format whose other half is being written
5
- * against `~/.claude/orchestration/shared/CONTRACT.md` by a different session, in a
6
- * different repo, at the same time. A field renamed here to read better is a field the
7
- * other half will never send. Nothing here changes without changing that document first.
8
- */
9
- export const PRODUCTS = ["tm", "a11y", "tra"];
10
- export const ASK_STATUSES = ["ok", "blocked", "error", "rate_limited"];
@@ -1,37 +0,0 @@
1
- /**
2
- * Precondition sentences appended to a tool's description.
3
- *
4
- * WHY THESE EXIST. Nothing routes an MCP call: the client's model picks a tool from the
5
- * descriptions alone. A tool that needs an identifier the caller does not have is a dead
6
- * end — the model either asks the user for something they also do not know, or gives up.
7
- * These sentences turn that dead end into a HANDOFF by naming, in the description itself,
8
- * where the missing identifier comes from.
9
- *
10
- * Point at a sibling tool whenever one can produce the id — it is faster and more
11
- * predictable than an agent. Point at `askBrowserStackAI` only when NO tool here can.
12
- *
13
- * The one that matters most: 15 of the 17 Test Management tools require a project
14
- * identifier and NONE of them accepts its absence, yet no tool in this server lists
15
- * projects. "List my projects" is the first step of nearly every Test Management journey
16
- * and it was unserved, which is exactly why that request did not reach any tool unless a
17
- * user named one explicitly.
18
- *
19
- * Keep these as shared constants, not per-tool prose: the wording is a routing signal, and
20
- * twenty hand-written variants drift into twenty different signals.
21
- */
22
- /** No tool lists projects, so this genuinely has to go to the agent. */
23
- export declare const NEEDS_PROJECT_ID: string;
24
- /** A sibling tool can produce the id — prefer it over the agent. */
25
- export declare function needsIdFrom(idLabel: string, sourceTool: string): string;
26
- /** A test plan id (TP-*) comes from listTestPlans. */
27
- export declare const NEEDS_TEST_PLAN_ID: string;
28
- /** A build id comes from either build-lookup tool. */
29
- export declare const NEEDS_BUILD_ID: string;
30
- /** Session ids are not listable by any tool here. */
31
- export declare const NEEDS_SESSION_ID: string;
32
- /** A completed scan's ids come from startAccessibilityScan, or from the agent. */
33
- export declare const NEEDS_A11Y_SCAN_ID: string;
34
- /** Auth-config ids are not listable by any tool here. */
35
- export declare const NEEDS_A11Y_CONFIG_ID: string;
36
- /** Test ids come from listTestIds, which itself needs a build id. */
37
- export declare const NEEDS_TEST_IDS: string;
@@ -1,47 +0,0 @@
1
- /**
2
- * Precondition sentences appended to a tool's description.
3
- *
4
- * WHY THESE EXIST. Nothing routes an MCP call: the client's model picks a tool from the
5
- * descriptions alone. A tool that needs an identifier the caller does not have is a dead
6
- * end — the model either asks the user for something they also do not know, or gives up.
7
- * These sentences turn that dead end into a HANDOFF by naming, in the description itself,
8
- * where the missing identifier comes from.
9
- *
10
- * Point at a sibling tool whenever one can produce the id — it is faster and more
11
- * predictable than an agent. Point at `askBrowserStackAI` only when NO tool here can.
12
- *
13
- * The one that matters most: 15 of the 17 Test Management tools require a project
14
- * identifier and NONE of them accepts its absence, yet no tool in this server lists
15
- * projects. "List my projects" is the first step of nearly every Test Management journey
16
- * and it was unserved, which is exactly why that request did not reach any tool unless a
17
- * user named one explicitly.
18
- *
19
- * Keep these as shared constants, not per-tool prose: the wording is a routing signal, and
20
- * twenty hand-written variants drift into twenty different signals.
21
- */
22
- /** No tool lists projects, so this genuinely has to go to the agent. */
23
- export const NEEDS_PROJECT_ID = " Requires a project identifier (PR-*). No tool here lists projects, so if you do not " +
24
- 'have one, call askBrowserStackAI with product "tm" and ask which projects exist, then ' +
25
- "retry this tool with the identifier it returns.";
26
- /** A sibling tool can produce the id — prefer it over the agent. */
27
- export function needsIdFrom(idLabel, sourceTool) {
28
- return ` Requires ${idLabel}. Call ${sourceTool} first if you do not have it.`;
29
- }
30
- /** A test plan id (TP-*) comes from listTestPlans. */
31
- export const NEEDS_TEST_PLAN_ID = needsIdFrom("a test plan identifier (TP-*)", "listTestPlans");
32
- /** A build id comes from either build-lookup tool. */
33
- export const NEEDS_BUILD_ID = needsIdFrom("a BrowserStack build id", "getBuildId or listBuildId");
34
- /** Session ids are not listable by any tool here. */
35
- export const NEEDS_SESSION_ID = " Requires a session id, which no tool here lists. If you only know the build, call " +
36
- "getBuildId or listBuildId; if you have neither, call askBrowserStackAI with product " +
37
- '"tra" and describe the run you mean.';
38
- /** A completed scan's ids come from startAccessibilityScan, or from the agent. */
39
- export const NEEDS_A11Y_SCAN_ID = " Requires the ids of a completed scan. They are returned by startAccessibilityScan; " +
40
- 'for a scan run earlier, call askBrowserStackAI with product "a11y" to locate it, since ' +
41
- "no tool here lists past scans.";
42
- /** Auth-config ids are not listable by any tool here. */
43
- export const NEEDS_A11Y_CONFIG_ID = " Requires the numeric id returned by createAccessibilityAuthConfig. No tool here lists " +
44
- "existing configurations, so if you do not have the id, call askBrowserStackAI with " +
45
- 'product "a11y".';
46
- /** Test ids come from listTestIds, which itself needs a build id. */
47
- export const NEEDS_TEST_IDS = needsIdFrom("test ids", "listTestIds");