rainskills 0.1.19 → 0.1.21
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/README.md +34 -39
- package/SKILL.md +12 -10
- package/bin/rainskills-tools.js +114 -240
- package/bin/rainskills.js +134 -591
- package/install.sh +68 -94
- package/marketplace/rainskills/.claude-plugin/plugin.json +1 -1
- package/marketplace/rainskills/.codex-plugin/plugin.json +1 -1
- package/marketplace/rainskills/skills/rainskills/SKILL.md +13 -11
- package/package.json +2 -2
- package/rainbond-app-assistant/SKILL.md +116 -151
- package/rainbond-app-version-assistant/SKILL.md +98 -98
- package/rainbond-delivery-verifier/SKILL.md +98 -68
- package/rainbond-env-sync/SKILL.md +98 -69
- package/rainbond-fullstack-bootstrap/SKILL.md +101 -89
- package/rainbond-fullstack-bootstrap/modules/10-context-loading.md +1 -1
- package/rainbond-fullstack-troubleshooter/SKILL.md +98 -68
- package/rainbond-opensource-app-deploy/SKILL.md +102 -81
- package/rainbond-platform-installer/SKILL.md +4 -4
- package/rainbond-platform-installer/scripts/auto-update.js +0 -9
- package/rainbond-platform-installer/scripts/installed-version.js +1 -1
- package/rainbond-platform-installer/scripts/platform-installer.js +3 -89
- package/rainbond-platform-installer/scripts/runtime-state.js +14 -251
- package/rainbond-platform-installer/scripts/single-runtime.js +114 -0
- package/rainbond-platform-installer/scripts/user-message.js +3 -49
- package/rainbond-platform-installer/scripts/windows-client-config.js +0 -51
- package/rainbond-platform-installer/scripts/windows-onboarding.js +0 -3
- package/rainbond-platform-installer/scripts/windows-platform.ps1 +1 -1
- package/rainbond-platform-query/SKILL.md +103 -61
- package/rainbond-project-init/SKILL.md +102 -79
- package/rainbond-template-installer/SKILL.md +102 -80
- package/rainbond-platform-installer/scripts/environment-credentials.js +0 -225
- package/rainbond-platform-installer/scripts/environment-registry.js +0 -349
- package/rainbond-platform-installer/scripts/local-runtime-commands.js +0 -180
- package/rainbond-platform-installer/scripts/local-runtime.js +0 -58
- package/rainbond-platform-installer/scripts/runtime-context-resolver.js +0 -189
- package/rainbond-platform-installer/scripts/runtime-credentials.js +0 -163
- package/rainbond-platform-installer/scripts/runtime-intents.js +0 -386
- package/rainbond-platform-installer/scripts/runtime-operations.js +0 -597
- package/rainbond-platform-installer/scripts/windows-client-config.ps1 +0 -9
- package/rainbond-platform-installer/scripts/windows-read-user-environment.ps1 +0 -16
|
@@ -11,7 +11,6 @@ const readline = require("node:readline/promises");
|
|
|
11
11
|
const { spawn, spawnSync } = require("node:child_process");
|
|
12
12
|
const { createSecureStateStore } = require("./secure-state.js");
|
|
13
13
|
const { writeUserMessage } = require("./user-message.js");
|
|
14
|
-
const { assertIntentCanInstallNewPlatform, validateIntent } = require("./runtime-intents.js");
|
|
15
14
|
const {
|
|
16
15
|
selectPlatformRoute,
|
|
17
16
|
validatePersistedRouteTuple,
|
|
@@ -256,6 +255,9 @@ function readOnboardingState(filePath, expectedOperationId, stateStore = secureS
|
|
|
256
255
|
if (state.deployment_mode !== "self-hosted") {
|
|
257
256
|
throw new Error("状态文件不是私有化部署流程");
|
|
258
257
|
}
|
|
258
|
+
if (Object.hasOwn(state, "intent")) {
|
|
259
|
+
throw new Error("平台 onboarding 状态不能包含业务 intent");
|
|
260
|
+
}
|
|
259
261
|
if (state.control_mode !== undefined) {
|
|
260
262
|
if (!["windows-native", "wsl", "posix"].includes(state.control_mode)) {
|
|
261
263
|
throw new Error("状态文件中的 control_mode 无效");
|
|
@@ -269,9 +271,6 @@ function readOnboardingState(filePath, expectedOperationId, stateStore = secureS
|
|
|
269
271
|
throw new Error("非 WSL 状态不能包含 control_distro");
|
|
270
272
|
}
|
|
271
273
|
}
|
|
272
|
-
if (state.intent !== undefined && state.intent !== null) {
|
|
273
|
-
state.intent = validateIntent(state.intent);
|
|
274
|
-
}
|
|
275
274
|
return state;
|
|
276
275
|
}
|
|
277
276
|
|
|
@@ -2305,7 +2304,6 @@ async function installWindowsRainbond({ adapter, onboarding, options, paths, sta
|
|
|
2305
2304
|
|
|
2306
2305
|
function resumeInvocationForOnboarding(onboarding, execPath = process.execPath) {
|
|
2307
2306
|
assertOperationId(onboarding.operation_id);
|
|
2308
|
-
const intent = validateIntent(onboarding.intent);
|
|
2309
2307
|
const args = [
|
|
2310
2308
|
path.resolve(__dirname, "..", "..", "bin", "rainskills.js"),
|
|
2311
2309
|
"runtime",
|
|
@@ -2315,32 +2313,12 @@ function resumeInvocationForOnboarding(onboarding, execPath = process.execPath)
|
|
|
2315
2313
|
onboarding.console_url,
|
|
2316
2314
|
];
|
|
2317
2315
|
if (onboarding.console_url.startsWith("http://")) args.push("--allow-insecure-http");
|
|
2318
|
-
args.push(
|
|
2319
|
-
"--onboarding-id",
|
|
2320
|
-
onboarding.operation_id,
|
|
2321
|
-
"--intent-json",
|
|
2322
|
-
JSON.stringify(intent)
|
|
2323
|
-
);
|
|
2324
2316
|
return {
|
|
2325
2317
|
executable: execPath,
|
|
2326
2318
|
args,
|
|
2327
2319
|
};
|
|
2328
2320
|
}
|
|
2329
2321
|
|
|
2330
|
-
function intentResumeInvocationForOnboarding(onboarding, execPath = process.execPath) {
|
|
2331
|
-
assertOperationId(onboarding.operation_id);
|
|
2332
|
-
return {
|
|
2333
|
-
executable: execPath,
|
|
2334
|
-
args: [
|
|
2335
|
-
path.resolve(__dirname, "..", "..", "bin", "rainskills.js"),
|
|
2336
|
-
"intent",
|
|
2337
|
-
"resume",
|
|
2338
|
-
"--onboarding-id",
|
|
2339
|
-
onboarding.operation_id,
|
|
2340
|
-
],
|
|
2341
|
-
};
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
2322
|
function startWindowsRuntimeLease({
|
|
2345
2323
|
controlMode = "windows-native",
|
|
2346
2324
|
systemRoot = process.env.SystemRoot || "C:\\Windows",
|
|
@@ -2433,10 +2411,7 @@ async function runResume(onboardingId, {
|
|
|
2433
2411
|
consoleReadiness = waitForWindowsConsole,
|
|
2434
2412
|
onboardingUpdater = updateOnboarding,
|
|
2435
2413
|
invocationBuilder = resumeInvocationForOnboarding,
|
|
2436
|
-
intentInvocationBuilder = intentResumeInvocationForOnboarding,
|
|
2437
|
-
credentialReader = null,
|
|
2438
2414
|
environment = process.env,
|
|
2439
|
-
credentialHome = environment.USERPROFILE || environment.HOME || os.homedir(),
|
|
2440
2415
|
attachedRunner = spawnAttached,
|
|
2441
2416
|
write = (value) => process.stdout.write(value),
|
|
2442
2417
|
abortState = null,
|
|
@@ -2536,63 +2511,6 @@ async function runResume(onboardingId, {
|
|
|
2536
2511
|
write(`\nRainbond 已部署,授权尚未完成。稍后继续:\n npx rainskills@${packageManifest.version} resume --onboarding-id ${onboardingId}\n`);
|
|
2537
2512
|
throw new Error(`RainSkills 授权流程退出码为 ${result.code}`);
|
|
2538
2513
|
}
|
|
2539
|
-
let latestCredential;
|
|
2540
|
-
try {
|
|
2541
|
-
assertHostOperationActive(abortState);
|
|
2542
|
-
if (credentialReader) {
|
|
2543
|
-
latestCredential = await credentialReader({
|
|
2544
|
-
expectedOrigin: onboarding.console_url,
|
|
2545
|
-
home: credentialHome,
|
|
2546
|
-
platform: onboarding.control_mode === "windows-native" ? "win32" : process.platform,
|
|
2547
|
-
});
|
|
2548
|
-
} else {
|
|
2549
|
-
const credentialPlatform = onboarding.control_mode === "windows-native" ? "win32" : process.platform;
|
|
2550
|
-
const { createEnvironmentRegistry } = require("./environment-registry.js");
|
|
2551
|
-
const { createEnvironmentCredentialStore } = require("./environment-credentials.js");
|
|
2552
|
-
const { createRuntimeOperationStore } = require("./runtime-operations.js");
|
|
2553
|
-
const registry = createEnvironmentRegistry({
|
|
2554
|
-
platform: credentialPlatform,
|
|
2555
|
-
home: credentialHome,
|
|
2556
|
-
});
|
|
2557
|
-
const operations = createRuntimeOperationStore({
|
|
2558
|
-
platform: credentialPlatform,
|
|
2559
|
-
home: credentialHome,
|
|
2560
|
-
registry,
|
|
2561
|
-
});
|
|
2562
|
-
const operation = operations.read(onboardingId);
|
|
2563
|
-
if (!operation?.environment_id) throw new Error("运行环境操作尚未绑定环境");
|
|
2564
|
-
latestCredential = createEnvironmentCredentialStore({
|
|
2565
|
-
platform: credentialPlatform,
|
|
2566
|
-
home: credentialHome,
|
|
2567
|
-
}).read({
|
|
2568
|
-
environmentId: operation.environment_id,
|
|
2569
|
-
expectedOrigin: onboarding.console_url,
|
|
2570
|
-
});
|
|
2571
|
-
}
|
|
2572
|
-
assertHostOperationActive(abortState);
|
|
2573
|
-
} catch {
|
|
2574
|
-
write(`\n运行环境已连接,但无法安全读取最新凭据。稍后继续:\n npx rainskills@${packageManifest.version} resume --onboarding-id ${onboardingId}\n`);
|
|
2575
|
-
throw new Error("无法安全读取与当前 Rainbond origin 匹配的最新运行凭据");
|
|
2576
|
-
}
|
|
2577
|
-
const intentEnvironment = {
|
|
2578
|
-
...resumeEnvironment,
|
|
2579
|
-
RAINBOND_JWT: latestCredential.token,
|
|
2580
|
-
RAINBOND_URL: latestCredential.origin,
|
|
2581
|
-
};
|
|
2582
|
-
const intentInvocation = intentInvocationBuilder(onboarding);
|
|
2583
|
-
assertHostOperationActive(abortState);
|
|
2584
|
-
const intentResult = await attachedRunner(
|
|
2585
|
-
intentInvocation.executable,
|
|
2586
|
-
intentInvocation.args,
|
|
2587
|
-
{ env: intentEnvironment },
|
|
2588
|
-
null
|
|
2589
|
-
);
|
|
2590
|
-
assertHostOperationActive(abortState);
|
|
2591
|
-
if (intentResult.signal || intentResult.code !== 0) {
|
|
2592
|
-
write(`\n运行环境已连接,原始操作尚未恢复。稍后继续:\n npx rainskills@${packageManifest.version} resume --onboarding-id ${onboardingId}\n`);
|
|
2593
|
-
if (intentResult.signal) throw new Error(`原始 intent 恢复流程被信号 ${intentResult.signal} 中断`);
|
|
2594
|
-
throw new Error(`原始 intent 恢复流程退出码为 ${intentResult.code}`);
|
|
2595
|
-
}
|
|
2596
2514
|
if (windowsContext) {
|
|
2597
2515
|
const adapter = windowsAdapterFactory(onboarding);
|
|
2598
2516
|
write("\n最后会弹出一次 Windows 管理员确认,用于清理自动恢复任务。\n");
|
|
@@ -2795,9 +2713,6 @@ async function runInstallOperation(options, {
|
|
|
2795
2713
|
if (!["awaiting-platform", "platform-ready", "authorizing"].includes(onboarding.stage)) {
|
|
2796
2714
|
throw new Error(`当前 onboarding 阶段不能安装平台:${onboarding.stage}`);
|
|
2797
2715
|
}
|
|
2798
|
-
if (onboarding.stage === "awaiting-platform" && onboarding.intent) {
|
|
2799
|
-
onboarding = { ...onboarding, intent: assertIntentCanInstallNewPlatform(onboarding.intent) };
|
|
2800
|
-
}
|
|
2801
2716
|
if (onboarding.stage === "platform-ready" || onboarding.stage === "authorizing") {
|
|
2802
2717
|
if (!options.noResume) await runResume(options.onboardingId);
|
|
2803
2718
|
return;
|
|
@@ -3377,7 +3292,6 @@ module.exports = {
|
|
|
3377
3292
|
evaluatePreflight,
|
|
3378
3293
|
extractConsoleUrl,
|
|
3379
3294
|
inspectRemoteSystem,
|
|
3380
|
-
intentResumeInvocationForOnboarding,
|
|
3381
3295
|
normalizeConsoleHost,
|
|
3382
3296
|
normalizeRainbondImage,
|
|
3383
3297
|
normalizeRemoteTarget,
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const crypto = require("node:crypto");
|
|
4
3
|
const fs = require("node:fs");
|
|
5
4
|
const os = require("node:os");
|
|
6
5
|
const path = require("node:path");
|
|
7
6
|
const { isDeepStrictEqual } = require("node:util");
|
|
8
7
|
|
|
9
8
|
const { createSecureStateStore } = require("./secure-state.js");
|
|
10
|
-
const {
|
|
11
|
-
const { INTENT_DEFINITIONS, validateIntent } = require("./runtime-intents.js");
|
|
9
|
+
const { validateMcp } = require("./windows-client-config.js");
|
|
12
10
|
|
|
13
11
|
const RUNTIME_SCHEMA = "rainskills.runtime-connection.v1";
|
|
14
12
|
const STATUS_SCHEMA = "rainskills.runtime-status.v1";
|
|
@@ -16,13 +14,10 @@ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3
|
|
|
16
14
|
const STATES = new Set(["not_started", "connecting", "connected"]);
|
|
17
15
|
const TARGET_CLIENTS = new Set(["codex", "claude", "pi", "all"]);
|
|
18
16
|
const ENVIRONMENT_KINDS = new Set(["saas", "private"]);
|
|
19
|
-
const FAILURE_CATEGORIES = new Set(["credential-expired", "permission-denied"]);
|
|
20
|
-
const MAX_RETRY_COUNT = 1000;
|
|
21
17
|
const RUNTIME_STATE_LOCK_ID = "cf1c1a85-63e7-45f8-9dd5-20ea1d511c48";
|
|
22
18
|
const STATE_FIELDS = new Set([
|
|
23
19
|
"schema", "version", "state", "target_client", "environment_kind", "console_origin",
|
|
24
|
-
"validated_probe_at", "
|
|
25
|
-
"retry_count", "retry_budget", "last_failure_category",
|
|
20
|
+
"validated_probe_at", "operation_id", "created_at", "updated_at",
|
|
26
21
|
]);
|
|
27
22
|
|
|
28
23
|
function normalizeConsoleOrigin(value) {
|
|
@@ -84,61 +79,6 @@ async function fetchPinnedMcpEndpoint(endpoint, fetchImpl, options) {
|
|
|
84
79
|
return response;
|
|
85
80
|
}
|
|
86
81
|
|
|
87
|
-
function shellSingleQuote(value) {
|
|
88
|
-
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function persistPosixCredential({ home, stateStore, token, baseUrl }) {
|
|
92
|
-
const directory = stateStore.ensurePrivateDirectory(path.join(home, ".rainbond"));
|
|
93
|
-
const target = stateStore.assertInsideHome(path.join(directory, "mcp.env"), "凭据文件");
|
|
94
|
-
try {
|
|
95
|
-
fs.lstatSync(target);
|
|
96
|
-
stateStore.assertProtectedRegularFile(target);
|
|
97
|
-
} catch (error) {
|
|
98
|
-
if (error.code !== "ENOENT") throw error;
|
|
99
|
-
}
|
|
100
|
-
const temporary = path.join(
|
|
101
|
-
directory,
|
|
102
|
-
`.mcp.env.${process.pid}.${crypto.randomBytes(8).toString("hex")}`
|
|
103
|
-
);
|
|
104
|
-
const content = [
|
|
105
|
-
`export RAINBOND_JWT=${shellSingleQuote(token)}`,
|
|
106
|
-
`export RAINBOND_URL=${shellSingleQuote(baseUrl)}`,
|
|
107
|
-
"",
|
|
108
|
-
].join("\n");
|
|
109
|
-
let fd;
|
|
110
|
-
try {
|
|
111
|
-
fd = fs.openSync(temporary, "wx", 0o600);
|
|
112
|
-
fs.writeFileSync(fd, content, "utf8");
|
|
113
|
-
fs.fsyncSync(fd);
|
|
114
|
-
fs.closeSync(fd);
|
|
115
|
-
fd = undefined;
|
|
116
|
-
stateStore.protectRegularFile(temporary);
|
|
117
|
-
try {
|
|
118
|
-
fs.lstatSync(target);
|
|
119
|
-
stateStore.assertProtectedRegularFile(target);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
if (error.code !== "ENOENT") throw error;
|
|
122
|
-
}
|
|
123
|
-
fs.renameSync(temporary, target);
|
|
124
|
-
stateStore.protectRegularFile(target);
|
|
125
|
-
const directoryFd = fs.openSync(directory, "r");
|
|
126
|
-
try {
|
|
127
|
-
fs.fsyncSync(directoryFd);
|
|
128
|
-
} finally {
|
|
129
|
-
fs.closeSync(directoryFd);
|
|
130
|
-
}
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
133
|
-
try {
|
|
134
|
-
fs.unlinkSync(temporary);
|
|
135
|
-
} catch (cleanupError) {
|
|
136
|
-
if (cleanupError.code !== "ENOENT") throw cleanupError;
|
|
137
|
-
}
|
|
138
|
-
throw error;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
82
|
async function probeConfiguredMcp(state, { env, fetchImpl }) {
|
|
143
83
|
const originalToken = env.RAINBOND_JWT;
|
|
144
84
|
const endpoint = `${state.console_origin}/console/mcp/rainskills/api/query`;
|
|
@@ -197,22 +137,21 @@ function createRuntimeStateManager({
|
|
|
197
137
|
platform = process.platform,
|
|
198
138
|
home = os.homedir(),
|
|
199
139
|
stateStore = createDefaultStore(platform, home),
|
|
200
|
-
operationId,
|
|
201
140
|
liveProbe,
|
|
202
141
|
fetchImpl = globalThis.fetch,
|
|
203
142
|
env = process.env,
|
|
204
143
|
credentialWriter,
|
|
205
144
|
now = () => new Date().toISOString(),
|
|
206
145
|
} = {}) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
146
|
+
const statePath = path.join(home, ".rainbond", "rainskills", "runtime-connection-v1.json");
|
|
147
|
+
const writeCredential = credentialWriter || (({ token, baseUrl, kind }) => require(
|
|
148
|
+
"./single-runtime.js"
|
|
149
|
+
).createSingleRuntimeStore({ platform, home, stateStore }).write({
|
|
150
|
+
consoleOrigin: baseUrl,
|
|
151
|
+
kind,
|
|
152
|
+
token,
|
|
153
|
+
allowInsecureHttp: baseUrl.startsWith("http://"),
|
|
154
|
+
}));
|
|
216
155
|
const probe = liveProbe || ((state) => probeConfiguredMcp(state, {
|
|
217
156
|
env,
|
|
218
157
|
fetchImpl,
|
|
@@ -230,30 +169,15 @@ function createRuntimeStateManager({
|
|
|
230
169
|
if (!ENVIRONMENT_KINDS.has(state.environment_kind)) throw new Error("environment_kind 无效");
|
|
231
170
|
const consoleOrigin = normalizeConsoleOrigin(state.console_origin);
|
|
232
171
|
if (!UUID_PATTERN.test(state.operation_id || "")) throw new Error("operation_id 无效");
|
|
233
|
-
const intent = state.intent === undefined || state.intent === null ? null : validateIntent(state.intent);
|
|
234
172
|
if (!isIsoTimestamp(state.created_at) || !isIsoTimestamp(state.updated_at)) {
|
|
235
173
|
throw new Error("runtime state timestamp 无效");
|
|
236
174
|
}
|
|
237
175
|
if (state.validated_probe_at !== null && !isIsoTimestamp(state.validated_probe_at)) {
|
|
238
176
|
throw new Error("runtime state validated probe timestamp 无效");
|
|
239
177
|
}
|
|
240
|
-
const fixedSteps = intent ? INTENT_DEFINITIONS[intent.type].steps : [];
|
|
241
|
-
if (state.failed_step !== null && !fixedSteps.includes(state.failed_step)) {
|
|
242
|
-
throw new Error("runtime state failed step 不是 intent 的固定步骤");
|
|
243
|
-
}
|
|
244
|
-
if (!Number.isInteger(state.retry_count) || state.retry_count < 0 || state.retry_count > MAX_RETRY_COUNT) {
|
|
245
|
-
throw new Error("runtime state retry count 无效");
|
|
246
|
-
}
|
|
247
|
-
if (!Number.isInteger(state.retry_budget) || state.retry_budget < 0 || state.retry_budget > 1) {
|
|
248
|
-
throw new Error("runtime state retry budget 无效");
|
|
249
|
-
}
|
|
250
|
-
if (state.last_failure_category !== null && !FAILURE_CATEGORIES.has(state.last_failure_category)) {
|
|
251
|
-
throw new Error("runtime state failure category 无效");
|
|
252
|
-
}
|
|
253
178
|
return {
|
|
254
179
|
...state,
|
|
255
180
|
console_origin: consoleOrigin,
|
|
256
|
-
intent,
|
|
257
181
|
};
|
|
258
182
|
}
|
|
259
183
|
|
|
@@ -271,7 +195,6 @@ function createRuntimeStateManager({
|
|
|
271
195
|
target_client: input.target_client,
|
|
272
196
|
environment_kind: input.environment_kind,
|
|
273
197
|
console_origin: normalizeConsoleOrigin(input.console_origin),
|
|
274
|
-
intent: input.intent === undefined ? null : validateIntent(input.intent),
|
|
275
198
|
operation_id: input.operation_id,
|
|
276
199
|
};
|
|
277
200
|
}
|
|
@@ -321,7 +244,7 @@ function createRuntimeStateManager({
|
|
|
321
244
|
}
|
|
322
245
|
if (renewedCredential) {
|
|
323
246
|
try {
|
|
324
|
-
await writeCredential(renewedCredential);
|
|
247
|
+
await writeCredential({ ...renewedCredential, kind: state.environment_kind });
|
|
325
248
|
} catch {
|
|
326
249
|
const failed = credentialFailureState
|
|
327
250
|
? writeStateUnlocked(credentialFailureState(current))
|
|
@@ -340,26 +263,18 @@ function createRuntimeStateManager({
|
|
|
340
263
|
&& left.operation_id === right.operation_id
|
|
341
264
|
&& left.target_client === right.target_client
|
|
342
265
|
&& left.environment_kind === right.environment_kind
|
|
343
|
-
&& left.console_origin === right.console_origin
|
|
344
|
-
&& JSON.stringify(left.intent) === JSON.stringify(right.intent);
|
|
266
|
+
&& left.console_origin === right.console_origin;
|
|
345
267
|
}
|
|
346
268
|
|
|
347
269
|
function startConnecting(input) {
|
|
348
270
|
return withRuntimeStateLock(() => {
|
|
349
271
|
const prior = read();
|
|
350
272
|
const fields = connectionFields(input);
|
|
351
|
-
if (prior.operation_id === fields.operation_id && prior.last_failure_category !== null) {
|
|
352
|
-
if (prior.last_failure_category === "permission-denied") {
|
|
353
|
-
throw new Error("permission denied 不允许重新授权");
|
|
354
|
-
}
|
|
355
|
-
throw new Error("凭据失效恢复必须使用受保护的 runtime reconnect 重试门禁");
|
|
356
|
-
}
|
|
357
273
|
if (prior.state === "connecting") {
|
|
358
274
|
const identical = prior.operation_id === fields.operation_id
|
|
359
275
|
&& prior.target_client === fields.target_client
|
|
360
276
|
&& prior.environment_kind === fields.environment_kind
|
|
361
|
-
&& prior.console_origin === fields.console_origin
|
|
362
|
-
&& isDeepStrictEqual(prior.intent, fields.intent);
|
|
277
|
+
&& prior.console_origin === fields.console_origin;
|
|
363
278
|
if (identical) return prior;
|
|
364
279
|
throw new Error("另一个 runtime connecting operation 正在进行中");
|
|
365
280
|
}
|
|
@@ -372,29 +287,10 @@ function createRuntimeStateManager({
|
|
|
372
287
|
validated_probe_at: null,
|
|
373
288
|
created_at: prior.created_at || timestamp,
|
|
374
289
|
updated_at: timestamp,
|
|
375
|
-
failed_step: null,
|
|
376
|
-
retry_count: 0,
|
|
377
|
-
retry_budget: 0,
|
|
378
|
-
last_failure_category: null,
|
|
379
290
|
});
|
|
380
291
|
});
|
|
381
292
|
}
|
|
382
293
|
|
|
383
|
-
function persistConnectingCredential({ operationId, token }) {
|
|
384
|
-
if (!UUID_PATTERN.test(operationId || "")) throw new Error("operation_id 无效");
|
|
385
|
-
if (typeof token !== "string" || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token)) {
|
|
386
|
-
throw new Error("runtime credential 无效");
|
|
387
|
-
}
|
|
388
|
-
return withRuntimeStateLock(() => {
|
|
389
|
-
const current = read();
|
|
390
|
-
if (current.state !== "connecting" || current.operation_id !== operationId) {
|
|
391
|
-
throw new Error("runtime credential 与 connecting operation 不匹配");
|
|
392
|
-
}
|
|
393
|
-
writeCredential({ token, baseUrl: current.console_origin });
|
|
394
|
-
return { persisted: true };
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
294
|
async function markConnected(input) {
|
|
399
295
|
const prior = read();
|
|
400
296
|
const fields = connectionFields(input);
|
|
@@ -406,7 +302,6 @@ function createRuntimeStateManager({
|
|
|
406
302
|
|| prior.target_client !== fields.target_client
|
|
407
303
|
|| prior.environment_kind !== fields.environment_kind
|
|
408
304
|
|| prior.console_origin !== fields.console_origin
|
|
409
|
-
|| JSON.stringify(prior.intent) !== JSON.stringify(fields.intent)
|
|
410
305
|
) {
|
|
411
306
|
throw new Error("connected 写入必须匹配当前 connecting operation");
|
|
412
307
|
}
|
|
@@ -429,10 +324,6 @@ function createRuntimeStateManager({
|
|
|
429
324
|
validated_probe_at: timestamp,
|
|
430
325
|
created_at: prior.created_at || timestamp,
|
|
431
326
|
updated_at: timestamp,
|
|
432
|
-
failed_step: prior.failed_step || null,
|
|
433
|
-
retry_count: prior.retry_count || 0,
|
|
434
|
-
retry_budget: prior.retry_budget || 0,
|
|
435
|
-
last_failure_category: prior.last_failure_category || null,
|
|
436
327
|
}, {
|
|
437
328
|
renewedCredential: probeResult.renewedCredential,
|
|
438
329
|
});
|
|
@@ -503,143 +394,16 @@ function createRuntimeStateManager({
|
|
|
503
394
|
};
|
|
504
395
|
}
|
|
505
396
|
|
|
506
|
-
function assertMatchingOperation(current, operationId) {
|
|
507
|
-
if (!UUID_PATTERN.test(operationId || "")) throw new Error("operation_id 无效");
|
|
508
|
-
if (current.state === "not_started" || current.operation_id !== operationId) {
|
|
509
|
-
throw new Error("runtime operation_id 与参数不匹配");
|
|
510
|
-
}
|
|
511
|
-
if (!current.intent) throw new Error("runtime state 缺少 validated intent");
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function recordFailure({ operationId, step, reason } = {}) {
|
|
515
|
-
if (!FAILURE_CATEGORIES.has(reason)) throw new Error("runtime failure reason 无效");
|
|
516
|
-
return withRuntimeStateLock(() => {
|
|
517
|
-
const current = read();
|
|
518
|
-
assertMatchingOperation(current, operationId);
|
|
519
|
-
const fixedSteps = INTENT_DEFINITIONS[current.intent.type].steps;
|
|
520
|
-
if (!fixedSteps.includes(step)) throw new Error("runtime failure step 不是 intent 的固定步骤");
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
reason === "credential-expired"
|
|
524
|
-
&& current.state === "connecting"
|
|
525
|
-
&& current.last_failure_category === reason
|
|
526
|
-
&& current.failed_step === step
|
|
527
|
-
&& current.retry_count === 0
|
|
528
|
-
&& current.retry_budget === 1
|
|
529
|
-
) {
|
|
530
|
-
return current;
|
|
531
|
-
}
|
|
532
|
-
if (current.state !== "connected") {
|
|
533
|
-
throw new Error("只有 connected runtime 才能记录业务操作失败");
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const credentialExpired = reason === "credential-expired";
|
|
537
|
-
const retryBudget = credentialExpired && current.retry_count === 0 ? 1 : 0;
|
|
538
|
-
return writeStateUnlocked({
|
|
539
|
-
...current,
|
|
540
|
-
state: credentialExpired ? "connecting" : "connected",
|
|
541
|
-
validated_probe_at: credentialExpired ? null : current.validated_probe_at,
|
|
542
|
-
failed_step: step,
|
|
543
|
-
retry_budget: retryBudget,
|
|
544
|
-
last_failure_category: reason,
|
|
545
|
-
updated_at: now(),
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function reconnectInput(operationId) {
|
|
551
|
-
return withRuntimeStateLock(() => {
|
|
552
|
-
const current = read();
|
|
553
|
-
assertMatchingOperation(current, operationId);
|
|
554
|
-
if (current.last_failure_category === "permission-denied") {
|
|
555
|
-
throw new Error("permission denied 不允许重新授权或重试");
|
|
556
|
-
}
|
|
557
|
-
if (
|
|
558
|
-
current.state !== "connecting"
|
|
559
|
-
|| current.last_failure_category !== "credential-expired"
|
|
560
|
-
|| current.retry_budget !== 1
|
|
561
|
-
|| current.retry_count !== 0
|
|
562
|
-
) {
|
|
563
|
-
throw new Error("runtime credential retry budget 已用尽或尚未记录凭据失效");
|
|
564
|
-
}
|
|
565
|
-
return connectionFields(current);
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
async function withReconnectLease(operationId, action) {
|
|
570
|
-
if (!UUID_PATTERN.test(operationId || "")) throw new Error("operation_id 无效");
|
|
571
|
-
if (typeof action !== "function") throw new Error("runtime reconnect action 无效");
|
|
572
|
-
let lease;
|
|
573
|
-
try {
|
|
574
|
-
lease = stateStore.acquireOperationLock({ operationId });
|
|
575
|
-
} catch {
|
|
576
|
-
throw new Error("该 runtime reconnect 正在运行;请稍后重试");
|
|
577
|
-
}
|
|
578
|
-
try {
|
|
579
|
-
const connection = reconnectInput(operationId);
|
|
580
|
-
return await action(connection);
|
|
581
|
-
} finally {
|
|
582
|
-
lease.release();
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function assertContinuationEligible(operationId) {
|
|
587
|
-
return withRuntimeStateLock(() => {
|
|
588
|
-
const current = read();
|
|
589
|
-
assertMatchingOperation(current, operationId);
|
|
590
|
-
if (current.state !== "connected") throw new Error("runtime 尚未 connected,不能恢复 intent");
|
|
591
|
-
if (current.last_failure_category === "permission-denied") {
|
|
592
|
-
throw new Error("permission denied:当前操作没有权限,不能自动重试");
|
|
593
|
-
}
|
|
594
|
-
if (
|
|
595
|
-
current.last_failure_category === "credential-expired"
|
|
596
|
-
&& (current.retry_budget !== 1 || current.retry_count !== 0)
|
|
597
|
-
) {
|
|
598
|
-
throw new Error("runtime credential retry budget 已用尽");
|
|
599
|
-
}
|
|
600
|
-
return current;
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function prepareContinuation(operationId) {
|
|
605
|
-
return withRuntimeStateLock(() => {
|
|
606
|
-
const current = read();
|
|
607
|
-
assertMatchingOperation(current, operationId);
|
|
608
|
-
if (current.state !== "connected") throw new Error("runtime 尚未 connected,不能恢复 intent");
|
|
609
|
-
if (current.last_failure_category === "permission-denied") {
|
|
610
|
-
throw new Error("permission denied:当前操作没有权限,不能自动重试");
|
|
611
|
-
}
|
|
612
|
-
if (current.last_failure_category !== "credential-expired") return current;
|
|
613
|
-
if (current.retry_budget !== 1 || current.retry_count !== 0) {
|
|
614
|
-
throw new Error("runtime credential retry budget 已用尽");
|
|
615
|
-
}
|
|
616
|
-
return writeStateUnlocked({
|
|
617
|
-
...current,
|
|
618
|
-
retry_count: current.retry_count + 1,
|
|
619
|
-
retry_budget: 0,
|
|
620
|
-
updated_at: now(),
|
|
621
|
-
});
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
397
|
return {
|
|
626
398
|
markConnected,
|
|
627
399
|
path: statePath,
|
|
628
|
-
persistConnectingCredential,
|
|
629
|
-
assertContinuationEligible,
|
|
630
|
-
prepareContinuation,
|
|
631
400
|
read,
|
|
632
|
-
reconnectInput,
|
|
633
|
-
recordFailure,
|
|
634
401
|
startConnecting,
|
|
635
402
|
status,
|
|
636
|
-
withReconnectLease,
|
|
637
403
|
};
|
|
638
404
|
}
|
|
639
405
|
|
|
640
406
|
module.exports = {
|
|
641
|
-
FAILURE_CATEGORIES,
|
|
642
|
-
MAX_RETRY_COUNT,
|
|
643
407
|
RUNTIME_STATE_LOCK_ID,
|
|
644
408
|
RUNTIME_SCHEMA,
|
|
645
409
|
STATUS_SCHEMA,
|
|
@@ -648,6 +412,5 @@ module.exports = {
|
|
|
648
412
|
normalizeConsoleOrigin,
|
|
649
413
|
normalizeProbeResult,
|
|
650
414
|
fetchPinnedMcpEndpoint,
|
|
651
|
-
persistPosixCredential,
|
|
652
415
|
probeConfiguredMcp,
|
|
653
416
|
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
const { normalizeConsoleOrigin } = require("./console-origin.js");
|
|
8
|
+
const { createSecureStateStore } = require("./secure-state.js");
|
|
9
|
+
const { looksLikeJwt } = require("./windows-auth.js");
|
|
10
|
+
|
|
11
|
+
const SINGLE_RUNTIME_SCHEMA = "rainskills.single-runtime.v1";
|
|
12
|
+
const KINDS = new Set(["saas", "private"]);
|
|
13
|
+
const FIELDS = new Set([
|
|
14
|
+
"schema", "version", "console_origin", "kind", "token",
|
|
15
|
+
"allow_insecure_http", "created_at", "updated_at",
|
|
16
|
+
]);
|
|
17
|
+
const MAX_BYTES = 16384;
|
|
18
|
+
|
|
19
|
+
function isIsoTimestamp(value) {
|
|
20
|
+
return typeof value === "string"
|
|
21
|
+
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value)
|
|
22
|
+
&& !Number.isNaN(Date.parse(value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateSingleRuntime(input) {
|
|
26
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
27
|
+
throw new Error("单运行环境记录无效");
|
|
28
|
+
}
|
|
29
|
+
for (const field of Object.keys(input)) {
|
|
30
|
+
if (!FIELDS.has(field)) throw new Error(`单运行环境记录包含未知字段:${field}`);
|
|
31
|
+
}
|
|
32
|
+
if (
|
|
33
|
+
Object.keys(input).length !== FIELDS.size
|
|
34
|
+
|| input.schema !== SINGLE_RUNTIME_SCHEMA
|
|
35
|
+
|| input.version !== 1
|
|
36
|
+
|| !KINDS.has(input.kind)
|
|
37
|
+
|| typeof input.allow_insecure_http !== "boolean"
|
|
38
|
+
|| !looksLikeJwt(input.token)
|
|
39
|
+
|| !isIsoTimestamp(input.created_at)
|
|
40
|
+
|| !isIsoTimestamp(input.updated_at)
|
|
41
|
+
) {
|
|
42
|
+
throw new Error("单运行环境记录 schema 无效");
|
|
43
|
+
}
|
|
44
|
+
const origin = normalizeConsoleOrigin(input.console_origin);
|
|
45
|
+
if (origin.startsWith("http://") && !input.allow_insecure_http) {
|
|
46
|
+
throw new Error("明文 HTTP 运行环境必须显式允许不安全传输");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
schema: SINGLE_RUNTIME_SCHEMA,
|
|
50
|
+
version: 1,
|
|
51
|
+
console_origin: origin,
|
|
52
|
+
kind: input.kind,
|
|
53
|
+
token: input.token,
|
|
54
|
+
allow_insecure_http: input.allow_insecure_http,
|
|
55
|
+
created_at: input.created_at,
|
|
56
|
+
updated_at: input.updated_at,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createSingleRuntimeStore({
|
|
61
|
+
platform = process.platform,
|
|
62
|
+
home = os.homedir(),
|
|
63
|
+
stateStore = createSecureStateStore({ platform, home }),
|
|
64
|
+
now = () => new Date().toISOString(),
|
|
65
|
+
} = {}) {
|
|
66
|
+
const runtimePath = path.join(home, ".rainbond", "rainskills", "single-runtime-v1.json");
|
|
67
|
+
|
|
68
|
+
function read() {
|
|
69
|
+
if (!fs.existsSync(runtimePath)) return null;
|
|
70
|
+
stateStore.assertProtectedRegularFile(runtimePath);
|
|
71
|
+
if (fs.lstatSync(runtimePath).size > MAX_BYTES) {
|
|
72
|
+
throw new Error("单运行环境记录文件过大");
|
|
73
|
+
}
|
|
74
|
+
return validateSingleRuntime(stateStore.readProtectedJson(runtimePath));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function write({
|
|
78
|
+
consoleOrigin,
|
|
79
|
+
kind,
|
|
80
|
+
token,
|
|
81
|
+
allowInsecureHttp = false,
|
|
82
|
+
} = {}) {
|
|
83
|
+
if (fs.existsSync(runtimePath)) stateStore.assertProtectedRegularFile(runtimePath);
|
|
84
|
+
const current = read();
|
|
85
|
+
const timestamp = now();
|
|
86
|
+
const value = validateSingleRuntime({
|
|
87
|
+
schema: SINGLE_RUNTIME_SCHEMA,
|
|
88
|
+
version: 1,
|
|
89
|
+
console_origin: consoleOrigin,
|
|
90
|
+
kind,
|
|
91
|
+
token,
|
|
92
|
+
allow_insecure_http: allowInsecureHttp,
|
|
93
|
+
created_at: current?.created_at || timestamp,
|
|
94
|
+
updated_at: timestamp,
|
|
95
|
+
});
|
|
96
|
+
stateStore.atomicWriteJson(runtimePath, value);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function remove() {
|
|
101
|
+
if (!fs.existsSync(runtimePath)) return false;
|
|
102
|
+
stateStore.assertProtectedRegularFile(runtimePath);
|
|
103
|
+
fs.unlinkSync(runtimePath);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { path: runtimePath, read, remove, write };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
SINGLE_RUNTIME_SCHEMA,
|
|
112
|
+
createSingleRuntimeStore,
|
|
113
|
+
validateSingleRuntime,
|
|
114
|
+
};
|