clearotron 0.3.0 → 0.3.1-beta.0
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/bin/example.mjs +28 -15
- package/bin/onboard.mjs +7 -1
- package/bin/start.mjs +13 -4
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +20 -2
- package/driver/CHANGELOG.md +10 -0
- package/driver/band-size.mjs +66 -0
- package/driver/corrections-feedforward.mjs +128 -5
- package/driver/demo-container.mjs +83 -6
- package/driver/gateway.mjs +3 -3
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +9 -9
- package/driver/pipeline.mjs +113 -28
- package/driver/portal-service.mjs +8 -1
- package/driver/repair-composers.mjs +8 -0
- package/driver/stages.mjs +13 -1
- package/driver/suite-census.json +38 -14
- package/mcp-server/CHANGELOG.md +6 -0
- package/mcp-server/lib/ops.mjs +10 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +13 -2
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/release-version.mjs +84 -4
- package/shared/trigger-lane.mjs +66 -0
- package/shared/wsl.mjs +20 -3
|
@@ -25,6 +25,7 @@ import { join, dirname } from "node:path";
|
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { findings, sentences } from "./changelog-plain-language.mjs";
|
|
27
27
|
import { tagsHere } from "./release-cut-decision.mjs";
|
|
28
|
+
import { preModeFrom } from "./release-dist-tag.mjs";
|
|
28
29
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
29
30
|
|
|
30
31
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -41,6 +42,70 @@ export function alreadyOut({ version, tags = [], published = false }) {
|
|
|
41
42
|
return where;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// ── THE CHANNEL DECIDES THE MODE, AND IT HAS TO BE DECIDED HERE ───────────────────────────────────
|
|
46
|
+
//
|
|
47
|
+
// `changeset version` computes a pre-release number or a stable one depending on whether the tree is in
|
|
48
|
+
// pre-release mode when it runs. So something has to put the tree in the mode the chosen channel needs.
|
|
49
|
+
//
|
|
50
|
+
// THAT USED TO BE A STEP IN THE WORKFLOW, AND IT COULD NEVER HAVE WORKED. The step edited
|
|
51
|
+
// `.changeset/pre.json` in the working tree, and the action that runs this script rebuilds that tree
|
|
52
|
+
// before running it: `git checkout changeset-release/main`, then `git reset --hard` to the commit the
|
|
53
|
+
// run was dispatched from. Read in the pinned action's own source, in this order — the rebuild, then
|
|
54
|
+
// this script, then a commit made with `git add .` from the working tree, and nothing in between that
|
|
55
|
+
// touches git. So an edit made before the action is discarded, and an edit made here is committed.
|
|
56
|
+
//
|
|
57
|
+
// The failure had two halves and only one of them was visible. A stable dispatch could not leave
|
|
58
|
+
// pre-release mode, so it computed another beta and refused at the channel check — loud, and it cost a
|
|
59
|
+
// cut. The other half is silent: a stable deletes `pre.json` rather than leaving it saying `exit`, so a
|
|
60
|
+
// later beta dispatch finds no flag, cannot enter pre mode, and computes a STABLE version from a button
|
|
61
|
+
// marked beta. Fixing only the direction that announces itself would leave that one live, and it would
|
|
62
|
+
// surface weeks later as a release published to the wrong channel.
|
|
63
|
+
//
|
|
64
|
+
// Both directions are therefore driven from here, and both are no-ops when the tree is already right:
|
|
65
|
+
// the mode a cut needs is a fact about the channel asked for, not about what the last cut happened to do.
|
|
66
|
+
export const CUT_FLAG = "--cut=";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* PURE. What this cut must do to the tree's pre-release mode before the version is computed.
|
|
70
|
+
*
|
|
71
|
+
* `null` means leave the tree alone — a rehearsal, an ordinary push, or a tree already in the right
|
|
72
|
+
* mode. Anything else is the `changeset pre` arguments that put it there.
|
|
73
|
+
*
|
|
74
|
+
* An UNKNOWN channel leaves the tree alone rather than guessing. This runs on every push as well as on
|
|
75
|
+
* a dispatch, and a push carries no channel at all: changing the mode there would make an ordinary
|
|
76
|
+
* merge silently switch the line it is publishing on.
|
|
77
|
+
*/
|
|
78
|
+
export function modeTransition({ cut, mode }) {
|
|
79
|
+
const inPre = mode === "pre";
|
|
80
|
+
if (cut === "beta") return inPre ? null : ["pre", "enter", "beta"];
|
|
81
|
+
if (cut === "stable") return inPre ? ["pre", "exit"] : null;
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The channel this run asked for, and the arguments to forward on without it.
|
|
87
|
+
*
|
|
88
|
+
* SPLIT RATHER THAN READ, because everything not consumed here is forwarded verbatim to
|
|
89
|
+
* `changeset version`, which refuses an argument it does not know. A flag added to this script that is
|
|
90
|
+
* not removed from that list stops the cut.
|
|
91
|
+
*/
|
|
92
|
+
export function splitCut(args = []) {
|
|
93
|
+
const flag = args.find((a) => a.startsWith(CUT_FLAG));
|
|
94
|
+
return { cut: flag ? flag.slice(CUT_FLAG.length) : "", rest: args.filter((a) => a !== flag) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The tree's pre-release mode, or `"none"` when it carries no flag at all.
|
|
99
|
+
*
|
|
100
|
+
* `"none"` is a real state here rather than a missing reading: it is what a tree looks like after a
|
|
101
|
+
* stable, because `changeset version` DELETES the file rather than leaving it saying `exit`.
|
|
102
|
+
*/
|
|
103
|
+
export function preModeHere(root = ROOT) {
|
|
104
|
+
const p = join(root, ".changeset", "pre.json");
|
|
105
|
+
if (!existsSync(p)) return "none";
|
|
106
|
+
return preModeFrom(readFileSync(p, "utf8")) ? "pre" : "exit";
|
|
107
|
+
}
|
|
108
|
+
|
|
44
109
|
/**
|
|
45
110
|
* Whether the registry already carries `name@version`: true, false, or null when it could not be asked.
|
|
46
111
|
*
|
|
@@ -223,11 +288,23 @@ export function writeRootChangelog({ version, groups }, root = ROOT) {
|
|
|
223
288
|
}
|
|
224
289
|
|
|
225
290
|
function main() {
|
|
226
|
-
const
|
|
291
|
+
const { cut, rest } = splitCut(process.argv.slice(2));
|
|
227
292
|
const run = (...a) => execFileSync(process.execPath,
|
|
228
293
|
[join(ROOT, "node_modules/@changesets/cli/bin.js"), ...a], { cwd: ROOT, stdio: "inherit" });
|
|
229
294
|
|
|
230
|
-
|
|
295
|
+
// BEFORE THE VERSION IS COMPUTED, because the mode is what decides the number. The header above says
|
|
296
|
+
// why this cannot live in the workflow. It is announced either way: a cut that did nothing to the mode
|
|
297
|
+
// and a cut whose mode change was lost look identical in a log that only speaks up when it acts.
|
|
298
|
+
const modeWas = preModeHere();
|
|
299
|
+
const transition = modeTransition({ cut, mode: modeWas });
|
|
300
|
+
console.log(`release-version: asked for ${cut || "no channel"}, tree is in pre-release mode ${modeWas}`
|
|
301
|
+
+ ` — ${transition ? `running changeset ${transition.join(" ")}` : "nothing to change"}`);
|
|
302
|
+
if (transition) {
|
|
303
|
+
run(...transition);
|
|
304
|
+
console.log(`release-version: pre-release mode is now ${preModeHere()}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
run("version", ...rest);
|
|
231
308
|
const version = groupVersion();
|
|
232
309
|
|
|
233
310
|
// ── A VERSION ALREADY OUT IS REFUSED HERE, BEFORE ANYTHING IS STAMPED ─────────────────────────────
|
|
@@ -258,8 +335,11 @@ function main() {
|
|
|
258
335
|
console.error(`release-version: this cut computed ${version}, and that version is already out: `
|
|
259
336
|
+ `${out.join(" and ")}. Publishing it would fail after main is stamped and the notes are consumed.\n`
|
|
260
337
|
+ " The pre-release line keeps no record of its last number: .changeset/pre.json holds only its mode "
|
|
261
|
-
+ "and tag, and a stable release deletes it, so this was counted again from package.json
|
|
262
|
-
+ "
|
|
338
|
+
+ "and tag, and a stable release deletes it, so this was counted again from package.json.\n"
|
|
339
|
+
+ " The MODE is not the thing to repair — this step sets it from the channel that was asked for, and "
|
|
340
|
+
+ "the line above says which mode it found and what it did. What is wrong is the NUMBER the manifests "
|
|
341
|
+
+ "count from: open an ordinary pull request setting them to the last published version, which "
|
|
342
|
+
+ "publishes nothing, and cut again.");
|
|
263
343
|
process.exitCode = 1;
|
|
264
344
|
return;
|
|
265
345
|
}
|
package/shared/trigger-lane.mjs
CHANGED
|
@@ -61,6 +61,27 @@ export const SUPERVISED = "supervised";
|
|
|
61
61
|
* because a command printed without it is command-not-found in the terminal the reader is sitting in:
|
|
62
62
|
* doctor's own guard runs every command doctor prints, and it caught this one.
|
|
63
63
|
*/
|
|
64
|
+
/**
|
|
65
|
+
* Which credential did the door ask for when it refused? `"proxy"`, `"key"`, or null for "it did not say".
|
|
66
|
+
*
|
|
67
|
+
* THE DOOR'S OWN SENTENCE IS THE EVIDENCE, because the header is not. An identity-proxy door and a key
|
|
68
|
+
* door both answer an unauthenticated request with 401 and neither sends `www-authenticate`, so the only
|
|
69
|
+
* thing on the wire that distinguishes them is what the refusal says it wants.
|
|
70
|
+
*
|
|
71
|
+
* THIS MATCHES A SPELLING, AND A SPELLING CAN MOVE. The two sentences are written by the door
|
|
72
|
+
* (`mcp-server/lib/cf-access.mjs` for the proxy refusal, `mcp-server/lib/http-handler.mjs` for the key
|
|
73
|
+
* one). If either is reworded this stops recognising it and the verdict degrades to "did not say" — the
|
|
74
|
+
* reported state, not a wrong confident one, which is the right direction to fail in. A test pins both
|
|
75
|
+
* sentences against the doors that send them so the drift is named rather than silent.
|
|
76
|
+
*/
|
|
77
|
+
export function doorCredential(probe) {
|
|
78
|
+
const body = String(probe?.body ?? "");
|
|
79
|
+
if (!body) return null;
|
|
80
|
+
if (/auth-proxy jwt|jwt-assertion|access token/i.test(body)) return "proxy";
|
|
81
|
+
if (/access key|[?&]token=/i.test(body)) return "key";
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
export function triggerLaneVerdict({ url = null, hasToken = false, verbs = null, posture = SUPERVISED, probe = null, invoke = "" } = {}) {
|
|
65
86
|
const startCmd = `${invoke}clearotron start`;
|
|
66
87
|
const raw = String(url ?? "").trim();
|
|
@@ -131,6 +152,51 @@ export function triggerLaneVerdict({ url = null, hasToken = false, verbs = null,
|
|
|
131
152
|
const challenge = challengeVerdict(probe);
|
|
132
153
|
if (challenge.blocked) return { state: "fail", message: blockedByAccessChallenge(raw, probe.status) };
|
|
133
154
|
if (probe.ok) {
|
|
155
|
+
// ── A 401 IS ANSWERED BY STATUS AND REFUSED IN SUBSTANCE ─────────────────────────────────────────
|
|
156
|
+
//
|
|
157
|
+
// `probe.ok` is `status < 500`, so a door REFUSING the caller lands here and was reported as a lane
|
|
158
|
+
// that answers. That is not a near miss: an identity-proxy door and a key door both refuse an
|
|
159
|
+
// unauthenticated GET with 401 and NEITHER sends `www-authenticate`, so the challenge test above
|
|
160
|
+
// cannot separate them and this line printed the same sentence for a deployment that works and one
|
|
161
|
+
// that cannot carry a Start. A line that is identical in both states carries no information about
|
|
162
|
+
// either, which is what makes it a defect rather than an optimistic guess.
|
|
163
|
+
//
|
|
164
|
+
// WHAT SEPARATES THEM IS THE DOOR'S OWN SENTENCE. Each mode says which credential it wants, in the
|
|
165
|
+
// body: the proxy door asks for a proxy assertion, the key door asks for an access key. This portal
|
|
166
|
+
// holds a key and cannot produce a proxy identity, so the first is unreachable to it no matter what
|
|
167
|
+
// the key is.
|
|
168
|
+
const wants = doorCredential(probe);
|
|
169
|
+
if (probe.status === 401 && wants === "proxy") {
|
|
170
|
+
return { state: "fail",
|
|
171
|
+
message: `${raw} is fronted by an identity proxy: it refused an unauthenticated request by asking `
|
|
172
|
+
+ "for a proxy identity, and this portal presents an access key. The Start button will fail at "
|
|
173
|
+
+ "the door with a 401 upstream and a 502 to the client, whatever the key is. Either put the "
|
|
174
|
+
+ "portal behind the same proxy, or give it a door that accepts the key." };
|
|
175
|
+
}
|
|
176
|
+
if (probe.status === 401 && wants === "key") {
|
|
177
|
+
return { state: "pass",
|
|
178
|
+
message: `the trigger lane answers at ${raw} (401 to an unauthenticated check, asking for the `
|
|
179
|
+
+ `access key this portal holds)${challengeNote(challenge)}` };
|
|
180
|
+
}
|
|
181
|
+
if (probe.status === 401) {
|
|
182
|
+
// THE BODY IS NEW EVIDENCE, NOT A REPLACEMENT FOR WHAT WE HAD. A door that challenges for a bearer
|
|
183
|
+
// token has said, in the header, that it takes the kind of credential this portal holds — that is
|
|
184
|
+
// the reading this verdict was built on and it stays. Only when the refusal names no credential in
|
|
185
|
+
// EITHER place is there nothing to go on.
|
|
186
|
+
if (/bearer|oauth/i.test(String(probe.challenge ?? ""))) {
|
|
187
|
+
return { state: "pass",
|
|
188
|
+
message: `the trigger lane answers at ${raw} (401 to an unauthenticated check, challenging for a `
|
|
189
|
+
+ `token)${challengeNote(challenge)}` };
|
|
190
|
+
}
|
|
191
|
+
// REPORTED, NOT JUDGED — the doctrine this block already follows for an ambiguous challenge. A 401
|
|
192
|
+
// that names no credential in its body and offers no challenge may be a proxy we have not met or a
|
|
193
|
+
// door that changed its wording; claiming either way would be the original defect with the
|
|
194
|
+
// confidence pointed somewhere new.
|
|
195
|
+
return { state: "unsettled",
|
|
196
|
+
message: `${raw} refused an unauthenticated check with 401 and did not say which credential it `
|
|
197
|
+
+ "wants, in its challenge or its body, so this cannot tell whether the portal's access key will "
|
|
198
|
+
+ "be accepted. Present the key against it before relying on the Start button." };
|
|
199
|
+
}
|
|
134
200
|
return { state: "pass",
|
|
135
201
|
message: `the trigger lane answers at ${raw}${probe.status ? ` (${probe.status})` : ""}${challengeNote(challenge)}` };
|
|
136
202
|
}
|
package/shared/wsl.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Whether this is a Linux running under Windows. Shared because two readers need the one answer: the install
|
|
5
5
|
// (which binaries on a Windows drive to pass over) and the connect lines (which must say to run them inside
|
|
6
6
|
// WSL, where their paths exist).
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* BOTH SIGNALS INJECTABLE, for the reason `platformEngineRefusal` gives: the readers this protects are the
|
|
@@ -15,9 +15,26 @@ import { readFileSync } from "node:fs";
|
|
|
15
15
|
* resolution exactly as it was before this existed. Claiming WSL on a could-not-read would start refusing
|
|
16
16
|
* candidates under /mnt on an ordinary Linux box with an ordinary mount.
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
// THE INTEROP REGISTRATION BOTH GENERATIONS INSTALL, and the reason this file no longer matches a vendor
|
|
19
|
+
// string. The older generation's /proc/version carries a vendor name and NOT the token "wsl", so the only
|
|
20
|
+
// thing catching it was that name — identifying a platform by a third party's name, on a public surface,
|
|
21
|
+
// for want of another signal. This is the other signal, and it names the platform rather than a company.
|
|
22
|
+
//
|
|
23
|
+
// IT SHIPS ON REASONING, which is worth saying rather than implying. Neither generation runs on any box
|
|
24
|
+
// here, so nothing in this suite can demonstrate that the older one registers this entry: the newer one
|
|
25
|
+
// demonstrably does, the older one is documented to, and a walk through a real install is the test that
|
|
26
|
+
// exists. If it turns out not to, the failure is the one this file already fails safe into — "not WSL",
|
|
27
|
+
// which leaves the resolution exactly as it was, rather than claiming WSL on a box that is not one.
|
|
28
|
+
export const WSL_INTEROP_ENTRY = "/proc/sys/fs/binfmt_misc/WSLInterop";
|
|
29
|
+
|
|
30
|
+
export function isWsl({ env = process.env, procVersion = null, interopEntry = null } = {}) {
|
|
19
31
|
if (String(env.WSL_DISTRO_NAME ?? "").trim()) return true;
|
|
20
32
|
if (String(env.WSL_INTEROP ?? "").trim()) return true;
|
|
33
|
+
// Injectable like the other two, and for the same reason this file already gives: the readers this
|
|
34
|
+
// protects cannot run the suite to find out, so a Linux runner has to be able to drive both answers
|
|
35
|
+
// rather than read the source and agree with it.
|
|
36
|
+
const interop = interopEntry ?? (() => { try { return existsSync(WSL_INTEROP_ENTRY); } catch { return false; } })();
|
|
37
|
+
if (interop) return true;
|
|
21
38
|
const v = procVersion ?? (() => { try { return readFileSync("/proc/version", "utf8"); } catch { return ""; } })();
|
|
22
|
-
return /
|
|
39
|
+
return /wsl/i.test(v);
|
|
23
40
|
}
|