@browserstack/mcp-server 1.4.0-beta.3 → 1.5.0-beta.10
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/capability/loadtesting.capability-index.json +1792 -0
- package/capability/tm.capability-index.json +20094 -0
- package/dist/config.d.ts +1 -4
- package/dist/config.js +2 -23
- package/dist/index.js +2 -5
- package/dist/server-factory.js +5 -5
- package/dist/tools/accessibility.js +2 -5
- package/dist/tools/capability-registry/bind.d.ts +29 -0
- package/dist/tools/capability-registry/bind.js +134 -0
- package/dist/tools/capability-registry/config.d.ts +62 -0
- package/dist/tools/capability-registry/config.js +218 -0
- package/dist/tools/capability-registry/discovery.d.ts +44 -0
- package/dist/tools/capability-registry/discovery.js +99 -0
- package/dist/tools/capability-registry/egress.d.ts +44 -0
- package/dist/tools/capability-registry/egress.js +128 -0
- package/dist/tools/capability-registry/index-loader.d.ts +133 -0
- package/dist/tools/capability-registry/index-loader.js +369 -0
- package/dist/tools/capability-registry/register.d.ts +34 -0
- package/dist/tools/capability-registry/register.js +396 -0
- package/dist/tools/capability-registry/resolve.d.ts +38 -0
- package/dist/tools/capability-registry/resolve.js +45 -0
- package/dist/tools/capability-registry/search.d.ts +97 -0
- package/dist/tools/capability-registry/search.js +527 -0
- package/dist/tools/capability-registry/types.d.ts +232 -0
- package/dist/tools/capability-registry/types.js +33 -0
- package/dist/tools/get-failure-logs.js +1 -3
- package/dist/tools/rca-agent.js +2 -5
- package/dist/tools/selfheal.js +2 -5
- package/dist/tools/testmanagement.js +15 -37
- package/package.json +3 -2
- package/dist/tools/ask-browserstack/central-oauth.d.ts +0 -120
- package/dist/tools/ask-browserstack/central-oauth.js +0 -277
- package/dist/tools/ask-browserstack/config.d.ts +0 -102
- package/dist/tools/ask-browserstack/config.js +0 -140
- package/dist/tools/ask-browserstack/egress.d.ts +0 -34
- package/dist/tools/ask-browserstack/egress.js +0 -31
- package/dist/tools/ask-browserstack/register.d.ts +0 -61
- package/dist/tools/ask-browserstack/register.js +0 -416
- package/dist/tools/ask-browserstack/relay.d.ts +0 -201
- package/dist/tools/ask-browserstack/relay.js +0 -577
- package/dist/tools/ask-browserstack/stream.d.ts +0 -116
- package/dist/tools/ask-browserstack/stream.js +0 -236
- package/dist/tools/ask-browserstack/types.d.ts +0 -196
- package/dist/tools/ask-browserstack/types.js +0 -14
- package/dist/tools/tool-handoff.d.ts +0 -62
- package/dist/tools/tool-handoff.js +0 -75
|
@@ -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,236 +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 { apiClient } from "../../lib/apiClient.js";
|
|
26
|
-
import logger from "../../logger.js";
|
|
27
|
-
import { AskError } from "./config.js";
|
|
28
|
-
/**
|
|
29
|
-
* CONTRACT v2 §4 — the whole-run guard, so a runaway run cannot hold a tool call open
|
|
30
|
-
* forever. Deliberately generous: it is a backstop against a hung server, not a budget
|
|
31
|
-
* for a human's attention, and the thing that actually bounds one approval is Atlas's
|
|
32
|
-
* 300s gate.
|
|
33
|
-
*/
|
|
34
|
-
export const WHOLE_RUN_TIMEOUT_MS = 1_800_000;
|
|
35
|
-
export const EVENT_RUN = "run";
|
|
36
|
-
export const EVENT_PERMISSION = "permission";
|
|
37
|
-
export const EVENT_RESULT = "result";
|
|
38
|
-
/** Atlas's `f"perm-{uuid.uuid4().hex}"`, and nothing else. */
|
|
39
|
-
export const PERM_ID_PATTERN = /^perm-[0-9a-f]{32}$/;
|
|
40
|
-
/**
|
|
41
|
-
* Read an ask out of a `permission` frame's data, or return null.
|
|
42
|
-
*
|
|
43
|
-
* Came over from the transport this one replaced, and the reasons it existed did not
|
|
44
|
-
* change with the transport — only the direction the ask arrives from did. It is not a
|
|
45
|
-
* trust check on Atlas: it is what keeps a malformed frame from turning into a prompt
|
|
46
|
-
* that cannot be honoured.
|
|
47
|
-
*
|
|
48
|
-
* A blank description is rejected rather than relayed: the description IS the whole of
|
|
49
|
-
* what the human is shown, so an empty one is a prompt asking a person to approve
|
|
50
|
-
* nothing. A `perm_id` off Atlas's shape is rejected because it is the only thing that
|
|
51
|
-
* routes the answer back — the decision endpoint matches on it, so an id we could not
|
|
52
|
-
* have received is an answer that can never be delivered.
|
|
53
|
-
*
|
|
54
|
-
* Only the four fields of CONTRACT §2 are carried forward. `op_key`, `method`, `path`
|
|
55
|
-
* and `host` are Atlas-private (v1.1 §A) and it does not send them; if a future one
|
|
56
|
-
* ever did, they would stop here rather than reach an elicitation prompt or a result.
|
|
57
|
-
*/
|
|
58
|
-
export function parseAsk(data) {
|
|
59
|
-
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
60
|
-
return null;
|
|
61
|
-
const record = data;
|
|
62
|
-
const permId = record.perm_id;
|
|
63
|
-
const description = record.description;
|
|
64
|
-
if (typeof permId !== "string" || !PERM_ID_PATTERN.test(permId))
|
|
65
|
-
return null;
|
|
66
|
-
if (typeof description !== "string" || !description.trim())
|
|
67
|
-
return null;
|
|
68
|
-
return {
|
|
69
|
-
perm_id: permId,
|
|
70
|
-
product: typeof record.product === "string" ? record.product : "",
|
|
71
|
-
mode: typeof record.mode === "string" ? record.mode : "",
|
|
72
|
-
description,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Split a buffer into complete SSE frames, returning the leftover.
|
|
77
|
-
*
|
|
78
|
-
* Exported for its own tests because chunk boundaries are where SSE parsers break: a
|
|
79
|
-
* frame can arrive split across two reads, two frames can arrive in one read, and a
|
|
80
|
-
* `data:` line can contain anything except a newline. Getting this wrong shows up as an
|
|
81
|
-
* ask that is silently dropped — a write that never gets approved and never explains
|
|
82
|
-
* why — so it is tested directly rather than only through the happy path.
|
|
83
|
-
*/
|
|
84
|
-
export function splitFrames(buffer) {
|
|
85
|
-
const frames = [];
|
|
86
|
-
let rest = buffer;
|
|
87
|
-
for (;;) {
|
|
88
|
-
const idx = rest.indexOf("\n\n");
|
|
89
|
-
if (idx === -1)
|
|
90
|
-
break;
|
|
91
|
-
frames.push(rest.slice(0, idx));
|
|
92
|
-
rest = rest.slice(idx + 2);
|
|
93
|
-
}
|
|
94
|
-
return { frames, rest };
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Parse one frame. Returns null for anything that is not an event we can use —
|
|
98
|
-
* including the heartbeat, which is a bare `:` comment and is SUPPOSED to be ignored
|
|
99
|
-
* here: its only job is to be a read on the socket so the ingress does not time the
|
|
100
|
-
* connection out while a human is thinking.
|
|
101
|
-
*/
|
|
102
|
-
export function parseFrame(frame) {
|
|
103
|
-
let event = "";
|
|
104
|
-
const dataLines = [];
|
|
105
|
-
for (const line of frame.split("\n")) {
|
|
106
|
-
if (line.startsWith(":"))
|
|
107
|
-
continue; // comment / heartbeat
|
|
108
|
-
if (line.startsWith("event:"))
|
|
109
|
-
event = line.slice(6).trim();
|
|
110
|
-
else if (line.startsWith("data:"))
|
|
111
|
-
dataLines.push(line.slice(5).trim());
|
|
112
|
-
}
|
|
113
|
-
if (!event)
|
|
114
|
-
return null;
|
|
115
|
-
if (dataLines.length === 0)
|
|
116
|
-
return { event, data: null };
|
|
117
|
-
try {
|
|
118
|
-
return { event, data: JSON.parse(dataLines.join("\n")) };
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
// A frame we cannot read is not a frame we may guess at. Dropping it is safe
|
|
122
|
-
// because the only consequence is that an ask goes unanswered and the gate denies
|
|
123
|
-
// on its own expiry — never that something is approved.
|
|
124
|
-
logger.warn("askBrowserStackAI: unparseable stream frame, ignoring");
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* A fetch-based streaming transport.
|
|
130
|
-
*
|
|
131
|
-
* `timeoutMs` bounds the WHOLE run, not one request — CONTRACT v2 §4 replaced the old
|
|
132
|
-
* 330s outer rung because under A1 the stream lives for the run and may contain several
|
|
133
|
-
* 300s approvals in series. The per-ask rung (270s elicitation inside Atlas's 300s gate)
|
|
134
|
-
* is unchanged and still enforced where it belongs.
|
|
135
|
-
*/
|
|
136
|
-
export function fetchAgentStreamTransport(timeoutMs = WHOLE_RUN_TIMEOUT_MS) {
|
|
137
|
-
return function stream(url, headers, body) {
|
|
138
|
-
return {
|
|
139
|
-
async *[Symbol.asyncIterator]() {
|
|
140
|
-
const controller = new AbortController();
|
|
141
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
142
|
-
try {
|
|
143
|
-
let response;
|
|
144
|
-
try {
|
|
145
|
-
response = await fetch(url, {
|
|
146
|
-
method: "POST",
|
|
147
|
-
headers: { ...headers, Accept: "text/event-stream" },
|
|
148
|
-
body: JSON.stringify(body),
|
|
149
|
-
// A redirect from an authenticated API is usually a login bounce, and
|
|
150
|
-
// following it turns a clear 401 into a 200 carrying an HTML page.
|
|
151
|
-
redirect: "manual",
|
|
152
|
-
signal: controller.signal,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
// The same sentence the request/response transport gives, and for the same
|
|
157
|
-
// reason: the upstream detail ("connection reset", "fetch failed") names our
|
|
158
|
-
// plumbing rather than anything the reader can act on, and letting it through
|
|
159
|
-
// once already produced a result that read like the user had done something
|
|
160
|
-
// wrong. What they need to know is that BrowserStack was not reachable.
|
|
161
|
-
throw new AskError("BrowserStack AI could not be reached");
|
|
162
|
-
}
|
|
163
|
-
const contentType = response.headers.get("content-type") || "";
|
|
164
|
-
// GRACEFUL DEGRADE, and the reason A1 is safe to ship before Atlas has it
|
|
165
|
-
// everywhere: an Atlas that does not know `mode: "stream"` answers with an
|
|
166
|
-
// ordinary JSON body (a read-only run, `permission_relay.reason: "disabled"`).
|
|
167
|
-
// Yielding it as a single `result` means the caller needs no version
|
|
168
|
-
// negotiation and no flag — it gets a correct read-only answer instead of a
|
|
169
|
-
// parse failure against a body that was never SSE.
|
|
170
|
-
if (contentType.includes("json")) {
|
|
171
|
-
const parsed = await response.json().catch(() => null);
|
|
172
|
-
yield {
|
|
173
|
-
event: EVENT_RESULT,
|
|
174
|
-
data: parsed,
|
|
175
|
-
status: response.status,
|
|
176
|
-
};
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (!response.ok || !response.body) {
|
|
180
|
-
// Neither a stream nor a JSON result. Surface it as a first-class failure
|
|
181
|
-
// rather than an empty iteration, which the caller could not tell apart
|
|
182
|
-
// from "the run finished and said nothing".
|
|
183
|
-
throw new AskError(`BrowserStack AI refused the stream (HTTP ${response.status}).`);
|
|
184
|
-
}
|
|
185
|
-
const decoder = new TextDecoder();
|
|
186
|
-
let buffer = "";
|
|
187
|
-
for await (const chunk of response.body) {
|
|
188
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
189
|
-
const { frames, rest } = splitFrames(buffer);
|
|
190
|
-
buffer = rest;
|
|
191
|
-
for (const frame of frames) {
|
|
192
|
-
const parsed = parseFrame(frame);
|
|
193
|
-
if (parsed)
|
|
194
|
-
yield parsed;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
// A trailing frame with no terminating blank line still counts.
|
|
198
|
-
const tail = parseFrame(buffer);
|
|
199
|
-
if (tail)
|
|
200
|
-
yield tail;
|
|
201
|
-
}
|
|
202
|
-
finally {
|
|
203
|
-
clearTimeout(timer);
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
/** The decision POST. 30s, because it is an ordinary short request. */
|
|
210
|
-
export function fetchDecisionTransport(timeoutMs = 30_000) {
|
|
211
|
-
return async (url, headers, body) => {
|
|
212
|
-
try {
|
|
213
|
-
// Through `apiClient` per rules/security.md. `raise_error: false` because the caller
|
|
214
|
-
// reads the STATUS: a 404 (run gone) and a 409 (already decided) are both answers,
|
|
215
|
-
// and a thrown AxiosError would collapse them into the unreachable case below.
|
|
216
|
-
const response = await apiClient.post({
|
|
217
|
-
url,
|
|
218
|
-
headers,
|
|
219
|
-
body,
|
|
220
|
-
timeout: timeoutMs,
|
|
221
|
-
raise_error: false,
|
|
222
|
-
});
|
|
223
|
-
return response.status;
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
// The gate on the far side is still waiting and will deny on its own expiry, so
|
|
227
|
-
// a lost decision is safe — it is never an approval. 0 says "never delivered" so
|
|
228
|
-
// the caller can say that rather than implying a human refused.
|
|
229
|
-
return 0;
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
/** `POST /agent/{run_id}/permission`, built from the base URL the tool already resolved. */
|
|
234
|
-
export function decisionUrl(agentUrl, runId) {
|
|
235
|
-
return `${agentUrl.replace(/\/+$/, "")}/${encodeURIComponent(runId)}/permission`;
|
|
236
|
-
}
|
|
@@ -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", "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,14 +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
|
-
// a11y is deliberately ABSENT while Ask AI is in limited alpha. Atlas itself serves the
|
|
10
|
-
// product; this tool just does not offer it yet. Re-adding it means this list, the `product`
|
|
11
|
-
// describe text in register.ts, and the two a11y handoffs in tool-handoff.ts — which stop
|
|
12
|
-
// pointing here precisely because the call would now be rejected.
|
|
13
|
-
export const PRODUCTS = ["tm", "tra"];
|
|
14
|
-
export const ASK_STATUSES = ["ok", "blocked", "error", "rate_limited"];
|
|
@@ -1,62 +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
|
-
/**
|
|
27
|
-
* createProjectOrFolder must NOT carry NEEDS_PROJECT_ID: `project_identifier` is optional
|
|
28
|
-
* there, and the create-a-PROJECT half needs no id at all. With the generic constant the
|
|
29
|
-
* tool read "Requires a project identifier ... call askBrowserStackAI", which routed
|
|
30
|
-
* "create me a project" through the agent before letting the tool run.
|
|
31
|
-
*/
|
|
32
|
-
export declare const PROJECT_ID_ONLY_FOR_FOLDER: string;
|
|
33
|
-
/** A test plan id (TP-*) comes from listTestPlans. */
|
|
34
|
-
export declare const NEEDS_TEST_PLAN_ID: string;
|
|
35
|
-
/**
|
|
36
|
-
* The ONLY capability handoff here: every other constant points at a tool that produces a
|
|
37
|
-
* missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
|
|
38
|
-
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. Atlas can do them
|
|
39
|
-
* (the tm harness allows POST /api/v1/projects/{id}/test-plans plus /update, /delete,
|
|
40
|
-
* /clone, /test-runs and /test-runs/unlink), so without this line the model reads the four
|
|
41
|
-
* read tools, finds no create, and reports the capability as absent — which is exactly what
|
|
42
|
-
* a QA eval concluded.
|
|
43
|
-
*
|
|
44
|
-
* Deliberately narrow: it names the specific operations that are missing rather than
|
|
45
|
-
* inviting the model to route plan work to the agent generally, because the tool
|
|
46
|
-
* descriptions otherwise say to prefer a specific tool whenever one fits.
|
|
47
|
-
*
|
|
48
|
-
* Caveat worth knowing: askBrowserStackAI pins every write to human approval, so this path
|
|
49
|
-
* only completes on a client that can show a prompt. On one that cannot, the intended write
|
|
50
|
-
* comes back in `needs_approval` instead of happening.
|
|
51
|
-
*/
|
|
52
|
-
export declare const PLAN_WRITES_VIA_AGENT: string;
|
|
53
|
-
/** A build id comes from either build-lookup tool. */
|
|
54
|
-
export declare const NEEDS_BUILD_ID: string;
|
|
55
|
-
/** Session ids are not listable by any tool here. */
|
|
56
|
-
export declare const NEEDS_SESSION_ID: string;
|
|
57
|
-
/** A completed scan's ids come from startAccessibilityScan, or from the agent. */
|
|
58
|
-
export declare const NEEDS_A11Y_SCAN_ID: string;
|
|
59
|
-
/** Auth-config ids are not listable by any tool here. */
|
|
60
|
-
export declare const NEEDS_A11Y_CONFIG_ID: string;
|
|
61
|
-
/** Test ids come from listTestIds, which itself needs a build id. */
|
|
62
|
-
export declare const NEEDS_TEST_IDS: string;
|