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.
Files changed (40) hide show
  1. package/README.md +34 -39
  2. package/SKILL.md +12 -10
  3. package/bin/rainskills-tools.js +114 -240
  4. package/bin/rainskills.js +134 -591
  5. package/install.sh +68 -94
  6. package/marketplace/rainskills/.claude-plugin/plugin.json +1 -1
  7. package/marketplace/rainskills/.codex-plugin/plugin.json +1 -1
  8. package/marketplace/rainskills/skills/rainskills/SKILL.md +13 -11
  9. package/package.json +2 -2
  10. package/rainbond-app-assistant/SKILL.md +116 -151
  11. package/rainbond-app-version-assistant/SKILL.md +98 -98
  12. package/rainbond-delivery-verifier/SKILL.md +98 -68
  13. package/rainbond-env-sync/SKILL.md +98 -69
  14. package/rainbond-fullstack-bootstrap/SKILL.md +101 -89
  15. package/rainbond-fullstack-bootstrap/modules/10-context-loading.md +1 -1
  16. package/rainbond-fullstack-troubleshooter/SKILL.md +98 -68
  17. package/rainbond-opensource-app-deploy/SKILL.md +102 -81
  18. package/rainbond-platform-installer/SKILL.md +4 -4
  19. package/rainbond-platform-installer/scripts/auto-update.js +0 -9
  20. package/rainbond-platform-installer/scripts/installed-version.js +1 -1
  21. package/rainbond-platform-installer/scripts/platform-installer.js +3 -89
  22. package/rainbond-platform-installer/scripts/runtime-state.js +14 -251
  23. package/rainbond-platform-installer/scripts/single-runtime.js +114 -0
  24. package/rainbond-platform-installer/scripts/user-message.js +3 -49
  25. package/rainbond-platform-installer/scripts/windows-client-config.js +0 -51
  26. package/rainbond-platform-installer/scripts/windows-onboarding.js +0 -3
  27. package/rainbond-platform-installer/scripts/windows-platform.ps1 +1 -1
  28. package/rainbond-platform-query/SKILL.md +103 -61
  29. package/rainbond-project-init/SKILL.md +102 -79
  30. package/rainbond-template-installer/SKILL.md +102 -80
  31. package/rainbond-platform-installer/scripts/environment-credentials.js +0 -225
  32. package/rainbond-platform-installer/scripts/environment-registry.js +0 -349
  33. package/rainbond-platform-installer/scripts/local-runtime-commands.js +0 -180
  34. package/rainbond-platform-installer/scripts/local-runtime.js +0 -58
  35. package/rainbond-platform-installer/scripts/runtime-context-resolver.js +0 -189
  36. package/rainbond-platform-installer/scripts/runtime-credentials.js +0 -163
  37. package/rainbond-platform-installer/scripts/runtime-intents.js +0 -386
  38. package/rainbond-platform-installer/scripts/runtime-operations.js +0 -597
  39. package/rainbond-platform-installer/scripts/windows-client-config.ps1 +0 -9
  40. 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 { persistWindowsEnvironment, validateMcp } = require("./windows-client-config.js");
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", "intent", "operation_id", "created_at", "updated_at", "failed_step",
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
- if (operationId !== undefined && !UUID_PATTERN.test(operationId || "")) {
208
- throw new Error("runtime state operation_id 无效");
209
- }
210
- const statePath = operationId
211
- ? path.join(home, ".rainbond", "rainskills", "runtime-connections-v1", `${operationId}.json`)
212
- : path.join(home, ".rainbond", "rainskills", "runtime-connection-v1.json");
213
- const writeCredential = credentialWriter || (platform === "win32"
214
- ? ({ token, baseUrl }) => persistWindowsEnvironment({ token, baseUrl })
215
- : ({ token, baseUrl }) => persistPosixCredential({ home, stateStore, token, baseUrl }));
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
+ };