rainskills 0.1.44 → 0.1.46
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/SKILL.md +3 -1
- package/bin/bridge-errors.js +26 -0
- package/bin/component-recipe.js +66 -0
- package/bin/confirmed-batch.js +120 -0
- package/bin/protected-poll.js +33 -16
- package/bin/rainskills-tools.js +133 -58
- package/bin/rainskills.js +5 -0
- package/bin/read-snapshot.js +3 -11
- package/bin/runtime-integrity.js +27 -0
- package/bin/runtime-smoke.js +63 -0
- package/bin/tool-policy.js +21 -0
- package/contracts/component-recipe.schema.yaml +30 -0
- package/contracts/confirmed-batch.schema.yaml +42 -0
- package/contracts/runtime/cli-base.md +3 -1
- package/contracts/runtime/overlay.schema.json +109 -10
- package/contracts/runtime/skills/rainbond-app-assistant.yaml +2 -1
- package/contracts/runtime/skills/rainbond-fullstack-bootstrap.yaml +2 -1
- package/contracts/runtime/skills/rainbond-fullstack-troubleshooter.yaml +2 -1
- package/install.sh +1 -1
- 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 +4 -2
- package/package.json +2 -2
- package/rainbond-ai-assistant/references/generated/runtime-gate.md +5 -3
- package/rainbond-app-assistant/SKILL.md +9 -8
- package/rainbond-app-assistant/references/customer-result.md +43 -0
- package/rainbond-app-assistant/references/generated/runtime-gate.md +53 -5
- package/rainbond-app-assistant/references/generated/runtime-routing.md +1 -1
- package/rainbond-app-assistant/references/mainline.md +27 -0
- package/rainbond-app-assistant/references/objects/catalog-and-source.md +361 -0
- package/rainbond-app-assistant/references/objects/runtime-delivery-and-lifecycle.md +424 -0
- package/rainbond-app-assistant/references/objects/skill-boundaries-and-gaps.md +243 -0
- package/rainbond-app-assistant/references/objects/source-resolution-and-projection.md +515 -0
- package/rainbond-app-assistant/references/output-contract.md +2 -431
- package/rainbond-app-assistant/references/phases/bootstrap.md +17 -0
- package/rainbond-app-assistant/references/phases/customer-rules.md +13 -0
- package/rainbond-app-assistant/references/phases/deliver.md +13 -0
- package/rainbond-app-assistant/references/phases/identity.md +27 -0
- package/rainbond-app-assistant/references/phases/initialize.md +11 -0
- package/rainbond-app-assistant/references/phases/input-literals.md +21 -0
- package/rainbond-app-assistant/references/phases/local-inputs.md +23 -0
- package/rainbond-app-assistant/references/phases/mainline-details.md +89 -0
- package/rainbond-app-assistant/references/phases/narration.md +33 -0
- package/rainbond-app-assistant/references/phases/orchestration-details.md +178 -0
- package/rainbond-app-assistant/references/phases/promote.md +3 -0
- package/rainbond-app-assistant/references/phases/routing-details.md +128 -0
- package/rainbond-app-assistant/references/phases/source-build.md +27 -0
- package/rainbond-app-assistant/references/phases/source-create.md +13 -0
- package/rainbond-app-assistant/references/phases/source-failure.md +29 -0
- package/rainbond-app-assistant/references/phases/source-policy.md +18 -0
- package/rainbond-app-assistant/references/phases/source-proxy.md +22 -0
- package/rainbond-app-assistant/references/phases/topology-selection.md +42 -0
- package/rainbond-app-assistant/references/phases/troubleshoot.md +23 -0
- package/rainbond-app-assistant/references/product-object-model.md +6 -1536
- package/rainbond-app-assistant/references/results/structured-result-details-2.md +191 -0
- package/rainbond-app-assistant/references/results/structured-result-details.md +205 -0
- package/rainbond-app-assistant/references/rule-ownership.json +265 -0
- package/rainbond-app-assistant/references/structured-result.md +7 -0
- package/rainbond-app-assistant/references/workflow-rules.md +22 -709
- package/rainbond-app-assistant/schemas/generated/component-recipe.schema.yaml +32 -0
- package/rainbond-app-assistant/schemas/generated/confirmed-batch.schema.yaml +44 -0
- package/rainbond-app-assistant/scripts/validate_progressive_loading.py +8 -4
- package/rainbond-app-version-assistant/references/generated/runtime-gate.md +5 -3
- package/rainbond-delivery-verifier/references/generated/runtime-gate.md +5 -3
- package/rainbond-env-sync/references/generated/runtime-gate.md +5 -3
- package/rainbond-fullstack-bootstrap/SKILL.md +6 -2
- package/rainbond-fullstack-bootstrap/modules/50-workflow.md +1 -1
- package/rainbond-fullstack-bootstrap/modules/52-cli-batching.md +29 -0
- package/rainbond-fullstack-bootstrap/references/generated/runtime-gate.md +53 -5
- package/rainbond-fullstack-bootstrap/references/generated/runtime-routing.md +1 -1
- package/rainbond-fullstack-bootstrap/schemas/generated/component-recipe.schema.yaml +32 -0
- package/rainbond-fullstack-bootstrap/schemas/generated/confirmed-batch.schema.yaml +44 -0
- package/rainbond-fullstack-troubleshooter/references/generated/runtime-gate.md +52 -4
- package/rainbond-fullstack-troubleshooter/references/generated/runtime-routing.md +1 -1
- package/rainbond-fullstack-troubleshooter/schemas/generated/component-recipe.schema.yaml +32 -0
- package/rainbond-fullstack-troubleshooter/schemas/generated/confirmed-batch.schema.yaml +44 -0
- package/rainbond-opensource-app-deploy/references/generated/runtime-gate.md +5 -3
- package/rainbond-platform-installer/SKILL.md +1 -1
- package/rainbond-platform-installer/scripts/installed-version.js +1 -1
- package/rainbond-platform-plugin-manager/references/generated/runtime-gate.md +5 -3
- package/rainbond-platform-query/references/generated/runtime-gate.md +5 -3
- package/rainbond-project-init/references/generated/runtime-gate.md +5 -3
- package/rainbond-template-installer/references/generated/runtime-gate.md +5 -3
- package/scripts/build-skill-manifest.mjs +3 -2
- package/scripts/install-local-cli.mjs +191 -88
package/bin/rainskills-tools.js
CHANGED
|
@@ -32,20 +32,7 @@ const SENSITIVE_RESPONSE_KEY_PATTERN = /(?:authorization|jwt|token|password|secr
|
|
|
32
32
|
const SENSITIVE_ENV_NAME_PATTERN = /(?:^|_)(?:PASSWORD|PASSWD|PASS|SECRET|TOKEN|API_?KEY|PRIVATE_?KEY|CREDENTIALS?|AUTH(?:ORIZATION)?|COOKIE|CERT(?:IFICATE)?)(?:_|$)/i;
|
|
33
33
|
const ENV_NAME_FIELDS = Object.freeze(["attr_name", "name", "note", "env_name"]);
|
|
34
34
|
const ENV_VALUE_FIELDS = new Set(["attr_value", "value"]);
|
|
35
|
-
const RISK_POLICY =
|
|
36
|
-
version: "1",
|
|
37
|
-
readPrefixes: [
|
|
38
|
-
"rainbond_query_", "rainbond_get_", "rainbond_list_", "rainbond_check_",
|
|
39
|
-
"rainbond_describe_", "rainbond_validate_", "rainbond_verify_", "rainbond_search_",
|
|
40
|
-
],
|
|
41
|
-
destructiveFragments: ["delete", "remove", "purge", "destroy"],
|
|
42
|
-
});
|
|
43
|
-
const MIXED_TOOL_OPERATION_POLICY = Object.freeze({
|
|
44
|
-
rainbond_manage_component_dependency: Object.freeze({
|
|
45
|
-
readOperations: Object.freeze(["summary"]),
|
|
46
|
-
destructiveOperations: Object.freeze(["delete"]),
|
|
47
|
-
}),
|
|
48
|
-
});
|
|
35
|
+
const { RISK_POLICY, classifyTool } = require("./tool-policy.js");
|
|
49
36
|
const CALL_OPTION_DEFINITIONS = Object.freeze({
|
|
50
37
|
"--confirm": Object.freeze({ property: "confirmation", pattern: OPERATION_ID_PATTERN }),
|
|
51
38
|
"--skill-id": Object.freeze({ property: "skillId", pattern: SKILL_ID_PATTERN }),
|
|
@@ -79,20 +66,7 @@ const HANDOFF_SKILLS = new Set([
|
|
|
79
66
|
"rainbond-template-installer",
|
|
80
67
|
]);
|
|
81
68
|
|
|
82
|
-
const EXIT =
|
|
83
|
-
USAGE: 2,
|
|
84
|
-
CONFIG: 3,
|
|
85
|
-
TRANSPORT: 4,
|
|
86
|
-
TOOL: 5,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
class BridgeError extends Error {
|
|
90
|
-
constructor(message, exitCode, payload) {
|
|
91
|
-
super(message);
|
|
92
|
-
this.exitCode = exitCode;
|
|
93
|
-
this.payload = payload;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
69
|
+
const { BridgeError, EXIT, inputValidation, errorResult } = require("./bridge-errors.js");
|
|
96
70
|
|
|
97
71
|
function requireRuntimeModule(filename) {
|
|
98
72
|
const candidates = [
|
|
@@ -308,6 +282,11 @@ function parseCommand(args) {
|
|
|
308
282
|
if (command === "read" && remaining.length === 4 && remaining[1] && remaining[2] === "--input" && remaining[3] === "-") {
|
|
309
283
|
return { command, toolName: remaining[1], input: remaining[3], ...context };
|
|
310
284
|
}
|
|
285
|
+
if (command === "batch" || command === "recipe") {
|
|
286
|
+
if (!["rainbond-app-assistant", "rainbond-fullstack-bootstrap", "rainbond-fullstack-troubleshooter"].includes(skillId)) throw new BridgeError("batch is not allowed for this skill", EXIT.USAGE);
|
|
287
|
+
const parsed = parseCommand(["call", "rainskills_confirmed_batch", ...remaining.slice(1), "--skill-id", skillId]);
|
|
288
|
+
return { ...parsed, command };
|
|
289
|
+
}
|
|
311
290
|
if (command === "call" && remaining.length >= 4 && remaining[1] && remaining[2] === "--input" && remaining[3] === "-") {
|
|
312
291
|
const parsed = { command, toolName: remaining[1], input: remaining[3], ...context };
|
|
313
292
|
for (let index = 4; index < remaining.length; index += 2) {
|
|
@@ -550,7 +529,8 @@ async function executeWithUsageTelemetry(command, config) {
|
|
|
550
529
|
: command;
|
|
551
530
|
const output = await execute(executionCommand, config);
|
|
552
531
|
if (telemetry) {
|
|
553
|
-
const
|
|
532
|
+
const workflowFailed = ["batch", "recipe"].includes(command.command) && output.status !== "succeeded";
|
|
533
|
+
const firstUse = telemetry.recordFirstUse(workflowFailed ? "failed" : "success");
|
|
554
534
|
const active = telemetry.recordActiveDaily();
|
|
555
535
|
await Promise.allSettled([firstUse.delivery, active.delivery]);
|
|
556
536
|
}
|
|
@@ -570,7 +550,7 @@ async function executeWithUsageTelemetry(command, config) {
|
|
|
570
550
|
}
|
|
571
551
|
|
|
572
552
|
function commandTracksBusinessUsage(command) {
|
|
573
|
-
return ["read", "call", "snapshot", "poll"].includes(command?.command);
|
|
553
|
+
return ["read", "call", "snapshot", "poll", "batch", "recipe"].includes(command?.command);
|
|
574
554
|
}
|
|
575
555
|
|
|
576
556
|
function loadSkillBinding(config, skillId, rootSkillId) {
|
|
@@ -622,23 +602,6 @@ function loadSkillBinding(config, skillId, rootSkillId) {
|
|
|
622
602
|
};
|
|
623
603
|
}
|
|
624
604
|
|
|
625
|
-
function classifyTool(toolName, argumentsValue = {}) {
|
|
626
|
-
const normalized = String(toolName || "").toLowerCase();
|
|
627
|
-
const operation = typeof argumentsValue.operation === "string"
|
|
628
|
-
? argumentsValue.operation.trim().toLowerCase()
|
|
629
|
-
: "";
|
|
630
|
-
const mixedPolicy = MIXED_TOOL_OPERATION_POLICY[normalized];
|
|
631
|
-
if (mixedPolicy) {
|
|
632
|
-
if (mixedPolicy.destructiveOperations.includes(operation)) return "destructive";
|
|
633
|
-
if (mixedPolicy.readOperations.includes(operation)) return "read";
|
|
634
|
-
}
|
|
635
|
-
if (RISK_POLICY.destructiveFragments.some((fragment) => normalized.includes(fragment))) {
|
|
636
|
-
return "destructive";
|
|
637
|
-
}
|
|
638
|
-
if (RISK_POLICY.readPrefixes.some((prefix) => normalized.startsWith(prefix))) return "read";
|
|
639
|
-
return "write";
|
|
640
|
-
}
|
|
641
|
-
|
|
642
605
|
function operationFile(config, operationId) {
|
|
643
606
|
return path.join(operationsPath(config), `${operationId}.json`);
|
|
644
607
|
}
|
|
@@ -728,6 +691,7 @@ function confirmOperation(
|
|
|
728
691
|
if (!record || record.status !== "awaiting_confirmation" || record.tool_name !== toolName ||
|
|
729
692
|
record.operation_class !== operationClass ||
|
|
730
693
|
record.arguments_digest !== argumentsDigest(argumentsValue) ||
|
|
694
|
+
(record.expires_at && Date.now() >= Date.parse(record.expires_at)) ||
|
|
731
695
|
record.skill_id !== skillBinding.skillId ||
|
|
732
696
|
record.root_skill_id !== skillBinding.rootSkillId ||
|
|
733
697
|
record.skill_package_version !== skillBinding.packageVersion ||
|
|
@@ -862,6 +826,7 @@ function sanitizeToolResult(value, pathParts = [], sensitiveArguments = []) {
|
|
|
862
826
|
const safe = {};
|
|
863
827
|
const sensitiveEnv = hasSensitiveEnvName(value);
|
|
864
828
|
for (const [key, item] of Object.entries(value)) {
|
|
829
|
+
if (key === "request_body") continue;
|
|
865
830
|
// Console destructive operations may return this one-time control value.
|
|
866
831
|
// It is not a credential and must remain available for the confirmation flow.
|
|
867
832
|
const isPackageUploadAuthMode = key === "authorization"
|
|
@@ -915,12 +880,15 @@ function safeJson(value, secrets = []) {
|
|
|
915
880
|
function httpJsonRpcRequest(config, endpointPath, message, {
|
|
916
881
|
timeoutMs = REQUEST_TIMEOUT_MS,
|
|
917
882
|
maxResponseBytes = RESPONSE_MAX_BYTES,
|
|
883
|
+
signal,
|
|
918
884
|
} = {}) {
|
|
919
885
|
const endpoint = new URL(`${config.baseUrl.replace(/\/+$/, "")}${endpointPath}`);
|
|
920
886
|
const payload = JSON.stringify(message);
|
|
921
887
|
const client = endpoint.protocol === "https:" ? https : http;
|
|
922
888
|
|
|
923
889
|
return new Promise((resolve, reject) => {
|
|
890
|
+
if (signal?.aborted) { reject(new BridgeError("request cancelled", EXIT.TRANSPORT)); return; }
|
|
891
|
+
let abortRequest;
|
|
924
892
|
let settled = false;
|
|
925
893
|
let responseRef;
|
|
926
894
|
let wallTimer;
|
|
@@ -929,6 +897,7 @@ function httpJsonRpcRequest(config, endpointPath, message, {
|
|
|
929
897
|
if (settled) return;
|
|
930
898
|
settled = true;
|
|
931
899
|
clearTimeout(wallTimer);
|
|
900
|
+
signal?.removeEventListener("abort", abortRequest);
|
|
932
901
|
callback(value);
|
|
933
902
|
};
|
|
934
903
|
const fail = (error, abort = false) => {
|
|
@@ -990,6 +959,8 @@ function httpJsonRpcRequest(config, endpointPath, message, {
|
|
|
990
959
|
? error
|
|
991
960
|
: new BridgeError("unable to reach Rainbond API", EXIT.TRANSPORT));
|
|
992
961
|
});
|
|
962
|
+
abortRequest = () => fail(new BridgeError("request cancelled", EXIT.TRANSPORT), true);
|
|
963
|
+
signal?.addEventListener("abort", abortRequest, { once: true });
|
|
993
964
|
wallTimer = setTimeout(() => {
|
|
994
965
|
fail(new BridgeError("Rainbond API request timed out", EXIT.TRANSPORT), true);
|
|
995
966
|
}, timeoutMs);
|
|
@@ -1073,7 +1044,7 @@ async function rpcRequest(config, method, params = {}, options = {}) {
|
|
|
1073
1044
|
id: 1,
|
|
1074
1045
|
method,
|
|
1075
1046
|
params,
|
|
1076
|
-
}, options);
|
|
1047
|
+
}, { ...options, signal: options.signal || config.signal });
|
|
1077
1048
|
if (isVerifiedMissingRpcRoute(response)) {
|
|
1078
1049
|
throw new BridgeError(
|
|
1079
1050
|
"当前 Rainbond 版本不支持 Rainskills CLI,请先将 Rainbond 升级到 v6.9.9 或更高版本",
|
|
@@ -1351,7 +1322,91 @@ async function resolveStatelessContext(input, config) {
|
|
|
1351
1322
|
return workspaceSelectionResult(enterpriseId, matchingOptions);
|
|
1352
1323
|
}
|
|
1353
1324
|
|
|
1325
|
+
async function executeConfirmedToolCall(command, config, toolName, argumentsValue, executeStep = execute) {
|
|
1326
|
+
if (command.signal?.aborted) throw new BridgeError("workflow cancelled", EXIT.USAGE, undefined, "E_CANCELLED");
|
|
1327
|
+
const child = { command: "call", toolName, argumentsValue, skillId: command.skillId, rootSkillId: command.rootSkillId, telemetryInstallationId: command.telemetryInstallationId, signal: command.signal };
|
|
1328
|
+
const prepared = await executeStep(child, config);
|
|
1329
|
+
if (command.signal?.aborted) throw new BridgeError("workflow cancelled", EXIT.USAGE, undefined, "E_CANCELLED");
|
|
1330
|
+
if (!prepared.requires_confirmation || !prepared.confirmation_id) throw new BridgeError("workflow write did not return a confirmation", EXIT.CONFIG);
|
|
1331
|
+
return executeStep({ ...child, confirmation: prepared.confirmation_id }, config);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
async function executeConfirmedWorkflow(command, config) {
|
|
1335
|
+
const { validateBatch, executeBatch, configurationSatisfied } = require("./confirmed-batch.js");
|
|
1336
|
+
const { preflightRecipe, executeRecipe } = require("./component-recipe.js");
|
|
1337
|
+
const input = command.argumentsValue;
|
|
1338
|
+
const recipe = command.command === "recipe";
|
|
1339
|
+
const tool = recipe ? "rainskills_component_recipe" : "rainskills_confirmed_batch";
|
|
1340
|
+
const validate = (name, args) => validateToolArguments(config, name, args);
|
|
1341
|
+
const read = (toolName, argumentsValue) => execute({ command: "read", toolName, argumentsValue, skillId: command.skillId, signal: command.signal }, config);
|
|
1342
|
+
const skipKeys = [];
|
|
1343
|
+
const snapshots = new Map();
|
|
1344
|
+
const dirtyComponents = new Set();
|
|
1345
|
+
let completedWrites = 0;
|
|
1346
|
+
const currentConfiguration = async (step) => {
|
|
1347
|
+
const { team_name, region_name, app_id, service_id } = step.arguments;
|
|
1348
|
+
const key = `${step.tool}:${service_id}`;
|
|
1349
|
+
if (!snapshots.has(key)) {
|
|
1350
|
+
const args = { team_name, region_name, app_id, service_id, operation: "summary" };
|
|
1351
|
+
try { await validate(step.tool, args); }
|
|
1352
|
+
catch (error) { if (error.exitCode === EXIT.USAGE) return false; throw error; }
|
|
1353
|
+
snapshots.set(key, await read(step.tool, args));
|
|
1354
|
+
}
|
|
1355
|
+
return configurationSatisfied(step.tool, step.arguments, snapshots.get(key));
|
|
1356
|
+
};
|
|
1357
|
+
let steps;
|
|
1358
|
+
if (recipe) {
|
|
1359
|
+
const plan = await preflightRecipe(input, { validate, read });
|
|
1360
|
+
steps = [{ key: "create", tool: plan.tool }, ...plan.steps];
|
|
1361
|
+
} else {
|
|
1362
|
+
steps = validateBatch(input);
|
|
1363
|
+
for (const step of steps) await validate(step.tool, step.arguments);
|
|
1364
|
+
for (const step of steps) {
|
|
1365
|
+
const satisfied = await currentConfiguration(step);
|
|
1366
|
+
if (satisfied && !dirtyComponents.has(step.arguments.service_id)) skipKeys.push(step.key);
|
|
1367
|
+
else {
|
|
1368
|
+
dirtyComponents.add(step.arguments.service_id);
|
|
1369
|
+
if (step.arguments.dep_service_id) dirtyComponents.add(step.arguments.dep_service_id);
|
|
1370
|
+
for (const id of step.arguments.dep_service_ids || []) dirtyComponents.add(id);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const binding = loadSkillBinding(config, command.skillId, command.rootSkillId);
|
|
1375
|
+
const protectedInput = { input, skipped: skipKeys, identity: argumentsDigest({ endpoint: config.baseUrl, identity: config.jwt }) };
|
|
1376
|
+
let operation;
|
|
1377
|
+
if (!command.confirmation) {
|
|
1378
|
+
operation = prepareOperation(config, tool, "write", protectedInput, binding);
|
|
1379
|
+
writeOperation(config, { ...operation, expires_at: new Date(Date.now() + 10 * 60_000).toISOString() });
|
|
1380
|
+
return { requires_confirmation: true, confirmation_id: operation.operation_id, operation_class: "write", steps: steps.map(({ key, tool: name }) => ({ key, tool: name, status: skipKeys.includes(key) ? "skipped" : "planned" })), summary: "Confirm this exact workflow within the current authorized app scope." };
|
|
1381
|
+
}
|
|
1382
|
+
operation = confirmOperation(config, command.confirmation, tool, "write", protectedInput, binding);
|
|
1383
|
+
try {
|
|
1384
|
+
const adapters = {
|
|
1385
|
+
validate, read, signal: command.signal,
|
|
1386
|
+
isSatisfied: async (step) => {
|
|
1387
|
+
const satisfied = await currentConfiguration(step);
|
|
1388
|
+
if (skipKeys.includes(step.key) && !satisfied) throw new BridgeError("confirmed no-op state changed; prepare a new batch", EXIT.USAGE, undefined, "E_PLAN_STALE");
|
|
1389
|
+
return satisfied;
|
|
1390
|
+
},
|
|
1391
|
+
call: (toolName, argumentsValue) => executeConfirmedToolCall(command, config, toolName, argumentsValue),
|
|
1392
|
+
onCreated: async (identity) => { operation = { ...operation, created: identity }; writeOperation(config, operation); },
|
|
1393
|
+
onProgress: async (output) => {
|
|
1394
|
+
if (output.completed !== completedWrites) { snapshots.clear(); completedWrites = output.completed; }
|
|
1395
|
+
operation = { ...operation, progress: output }; writeOperation(config, operation);
|
|
1396
|
+
},
|
|
1397
|
+
};
|
|
1398
|
+
const output = await (recipe ? executeRecipe(input, adapters) : executeBatch(input, adapters));
|
|
1399
|
+
writeOperation(config, { ...operation, status: output.status, progress: output });
|
|
1400
|
+
return output;
|
|
1401
|
+
} catch (error) {
|
|
1402
|
+
updateOperation(config, operation, error.exitCode === EXIT.TRANSPORT ? "unknown" : "failed");
|
|
1403
|
+
throw error;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1354
1407
|
async function execute(command, config) {
|
|
1408
|
+
if (command.signal?.aborted) throw new BridgeError("workflow cancelled", EXIT.USAGE, undefined, "E_CANCELLED");
|
|
1409
|
+
if (["batch", "recipe"].includes(command.command)) return executeConfirmedWorkflow(command, config);
|
|
1355
1410
|
if (command.command === "context") {
|
|
1356
1411
|
return resolveStatelessContext(command.argumentsValue, config);
|
|
1357
1412
|
}
|
|
@@ -1359,7 +1414,8 @@ async function execute(command, config) {
|
|
|
1359
1414
|
return executePackageUpload(command, config);
|
|
1360
1415
|
}
|
|
1361
1416
|
if (command.command === "snapshot") {
|
|
1362
|
-
const { executeReadSnapshot } = require("./read-snapshot.js");
|
|
1417
|
+
const { executeReadSnapshot, validateSnapshotInput } = require("./read-snapshot.js");
|
|
1418
|
+
inputValidation(() => validateSnapshotInput(command.argumentsValue));
|
|
1363
1419
|
return executeReadSnapshot(command.argumentsValue, {
|
|
1364
1420
|
callTool: async (name, argumentsValue) => {
|
|
1365
1421
|
const result = await rpcRequest(config, "tools/call", { name, arguments: argumentsValue });
|
|
@@ -1375,10 +1431,12 @@ async function execute(command, config) {
|
|
|
1375
1431
|
});
|
|
1376
1432
|
}
|
|
1377
1433
|
if (command.command === "poll") {
|
|
1378
|
-
const { executeProtectedPoll } = require("./protected-poll.js");
|
|
1434
|
+
const { executeProtectedPoll, validatePollInput } = require("./protected-poll.js");
|
|
1435
|
+
inputValidation(() => validatePollInput(command.toolName, command.argumentsValue));
|
|
1379
1436
|
return executeProtectedPoll(command.toolName, command.argumentsValue, {
|
|
1380
|
-
|
|
1381
|
-
|
|
1437
|
+
signal: command.signal,
|
|
1438
|
+
callTool: async (name, argumentsValue, options) => {
|
|
1439
|
+
const result = await rpcRequest(config, "tools/call", { name, arguments: argumentsValue }, options);
|
|
1382
1440
|
if (!result || result.isError || !("structuredContent" in result)) {
|
|
1383
1441
|
throw new BridgeError("poll read failed", EXIT.TOOL);
|
|
1384
1442
|
}
|
|
@@ -1418,6 +1476,9 @@ async function execute(command, config) {
|
|
|
1418
1476
|
}
|
|
1419
1477
|
|
|
1420
1478
|
const argumentsValue = command.argumentsValue || readArguments(command.input);
|
|
1479
|
+
if (command.toolName === "rainbond_wait_for_build_completion") {
|
|
1480
|
+
if (argumentsValue.timeout !== undefined && (!Number.isInteger(argumentsValue.timeout) || argumentsValue.timeout < 1 || argumentsValue.timeout > 60)) throw new BridgeError("wait timeout must be between 1 and 60 seconds", EXIT.USAGE);
|
|
1481
|
+
}
|
|
1421
1482
|
const operationClass = classifyTool(command.toolName, argumentsValue);
|
|
1422
1483
|
if (command.command === "read" && operationClass !== "read") {
|
|
1423
1484
|
throw new BridgeError(
|
|
@@ -1472,7 +1533,7 @@ async function execute(command, config) {
|
|
|
1472
1533
|
installation_id: command.telemetryInstallationId,
|
|
1473
1534
|
};
|
|
1474
1535
|
}
|
|
1475
|
-
const result = await rpcRequest(config, "tools/call", params);
|
|
1536
|
+
const result = await rpcRequest(config, "tools/call", params, command.toolName === "rainbond_wait_for_build_completion" ? { timeoutMs: (argumentsValue.timeout || 60) * 1000, signal: command.signal } : { signal: command.signal });
|
|
1476
1537
|
if (!result || typeof result !== "object") {
|
|
1477
1538
|
throw new BridgeError("Rainbond tool result is invalid", EXIT.TRANSPORT);
|
|
1478
1539
|
}
|
|
@@ -1498,6 +1559,7 @@ async function execute(command, config) {
|
|
|
1498
1559
|
} catch (error) {
|
|
1499
1560
|
if (operation) {
|
|
1500
1561
|
updateOperation(config, operation, error.exitCode === EXIT.TRANSPORT ? "unknown" : "failed");
|
|
1562
|
+
if (error.exitCode === EXIT.TRANSPORT) error.code = "E_WRITE_RESULT_UNKNOWN";
|
|
1501
1563
|
}
|
|
1502
1564
|
throw error;
|
|
1503
1565
|
}
|
|
@@ -1507,9 +1569,16 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1507
1569
|
let config;
|
|
1508
1570
|
let command;
|
|
1509
1571
|
let argumentRedactions = [];
|
|
1572
|
+
const controller = new AbortController();
|
|
1573
|
+
const cancel = () => controller.abort();
|
|
1510
1574
|
try {
|
|
1511
1575
|
command = parseCommand(args);
|
|
1512
|
-
|
|
1576
|
+
command.signal = controller.signal;
|
|
1577
|
+
if (["poll", "batch", "recipe"].includes(command.command)) {
|
|
1578
|
+
process.once("SIGINT", cancel);
|
|
1579
|
+
process.once("SIGTERM", cancel);
|
|
1580
|
+
}
|
|
1581
|
+
if (["read", "call", "package-upload", "query", "context", "delivery", "handoff", "snapshot", "poll", "batch", "recipe"].includes(command.command)) {
|
|
1513
1582
|
const argumentsValue = readArguments(command.input);
|
|
1514
1583
|
command.argumentsValue = argumentsValue;
|
|
1515
1584
|
argumentRedactions = collectArgumentRedactions(argumentsValue);
|
|
@@ -1548,14 +1617,14 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1548
1617
|
}
|
|
1549
1618
|
if (command.command === "handoff") {
|
|
1550
1619
|
const { createHandoffContext, validateHandoffContext } = require("./handoff-context.js");
|
|
1551
|
-
const output = command.action === "create"
|
|
1620
|
+
const output = inputValidation(() => command.action === "create"
|
|
1552
1621
|
? createHandoffContext(command.argumentsValue)
|
|
1553
1622
|
: validateHandoffContext(command.argumentsValue.context, {
|
|
1554
1623
|
current: command.argumentsValue.current,
|
|
1555
1624
|
signal: command.argumentsValue.signal,
|
|
1556
1625
|
now: command.argumentsValue.now,
|
|
1557
1626
|
maxAgeMs: command.argumentsValue.max_age_ms,
|
|
1558
|
-
});
|
|
1627
|
+
}));
|
|
1559
1628
|
process.stdout.write(`${JSON.stringify(fitOutput(output))}\n`);
|
|
1560
1629
|
return;
|
|
1561
1630
|
}
|
|
@@ -1565,20 +1634,26 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1565
1634
|
if (config.isInsecureHttp) {
|
|
1566
1635
|
process.stderr.write('{"warning":"using insecure HTTP transport"}\n');
|
|
1567
1636
|
}
|
|
1637
|
+
config.signal = command.signal;
|
|
1568
1638
|
const output = await executeWithUsageTelemetry(command, config);
|
|
1569
1639
|
process.stdout.write(`${JSON.stringify(fitOutput(output))}\n`);
|
|
1570
1640
|
} catch (error) {
|
|
1571
1641
|
const bridgeError = error instanceof BridgeError
|
|
1572
1642
|
? error
|
|
1573
|
-
: new BridgeError("unexpected
|
|
1643
|
+
: new BridgeError(error.code === "MODULE_NOT_FOUND" ? "installed Rainskills CLI runtime is incomplete" : "unexpected internal CLI error", error.code === "MODULE_NOT_FOUND" ? EXIT.CONFIG : EXIT.TRANSPORT, undefined, error.code === "MODULE_NOT_FOUND" ? "E_RUNTIME_INCOMPLETE" : "E_INTERNAL");
|
|
1574
1644
|
const secrets = [config && config.jwt, config && config.baseUrl, ...argumentRedactions];
|
|
1575
|
-
const output = bridgeError
|
|
1645
|
+
const output = sanitizeToolResult(errorResult(bridgeError), [], argumentRedactions);
|
|
1576
1646
|
process.stderr.write(`${safeJson(output, secrets)}\n`);
|
|
1577
1647
|
process.exitCode = bridgeError.exitCode;
|
|
1648
|
+
} finally {
|
|
1649
|
+
process.removeListener("SIGINT", cancel);
|
|
1650
|
+
process.removeListener("SIGTERM", cancel);
|
|
1578
1651
|
}
|
|
1579
1652
|
}
|
|
1580
1653
|
|
|
1581
1654
|
module.exports = {
|
|
1655
|
+
main,
|
|
1656
|
+
executeConfirmedToolCall,
|
|
1582
1657
|
REQUEST_TIMEOUT_MS,
|
|
1583
1658
|
INPUT_MAX_BYTES,
|
|
1584
1659
|
RESPONSE_MAX_BYTES,
|
package/bin/rainskills.js
CHANGED
|
@@ -278,6 +278,11 @@ async function runBuiltin(args, {
|
|
|
278
278
|
throw new Error("runtime status 只支持固定参数 --json");
|
|
279
279
|
}
|
|
280
280
|
const packageVersion = require("../package.json").version;
|
|
281
|
+
const integrity = require("./runtime-integrity.js").checkRuntimeIntegrity();
|
|
282
|
+
if (integrity.status !== "ok") {
|
|
283
|
+
write(`${JSON.stringify({ schema: "rainskills.runtime-status.v1", state: "needs_reconnect", usable: false, package_version: packageVersion, integrity, repair_required: true })}\n`);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
281
286
|
if (runtimeStateManager) {
|
|
282
287
|
write(`${JSON.stringify({
|
|
283
288
|
...(await runtimeStateManager.status()),
|
package/bin/read-snapshot.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const SCOPES = new Set(["runtime", "app", "component", "delivery"]);
|
|
4
|
-
const
|
|
5
|
-
"rainbond_query_", "rainbond_get_", "rainbond_list_", "rainbond_check_",
|
|
6
|
-
"rainbond_describe_", "rainbond_validate_", "rainbond_verify_", "rainbond_search_",
|
|
7
|
-
];
|
|
8
|
-
const DESTRUCTIVE = /(?:delete|remove|purge|destroy|update|create|set|manage|operate|restart|deploy)/i;
|
|
4
|
+
const { classifyTool } = require("./tool-policy.js");
|
|
9
5
|
const SENSITIVE_KEY = /(?:authorization|jwt|token|password|secret|credential|private[_-]?key|cookie|certificate|ssl)/i;
|
|
10
6
|
const LOG_OR_EVENT = /(?:log|event)/i;
|
|
11
7
|
const BOUND_KEYS = new Set(["cursor", "since_seconds", "tail"]);
|
|
@@ -16,11 +12,6 @@ function isPlainObject(value) {
|
|
|
16
12
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
function readOnlyTool(name) {
|
|
20
|
-
return typeof name === "string"
|
|
21
|
-
&& READ_PREFIXES.some((prefix) => name.startsWith(prefix))
|
|
22
|
-
&& !DESTRUCTIVE.test(name);
|
|
23
|
-
}
|
|
24
15
|
|
|
25
16
|
function validateSnapshotInput(input) {
|
|
26
17
|
if (!isPlainObject(input) || !SCOPES.has(input.scope)) throw new Error("snapshot scope is invalid");
|
|
@@ -28,7 +19,7 @@ function validateSnapshotInput(input) {
|
|
|
28
19
|
throw new Error("snapshot requires one to eight requests");
|
|
29
20
|
}
|
|
30
21
|
for (const entry of input.requests) {
|
|
31
|
-
if (!isPlainObject(entry) ||
|
|
22
|
+
if (!isPlainObject(entry) || classifyTool(entry.name, entry.arguments) !== "read" || !isPlainObject(entry.arguments)) {
|
|
32
23
|
throw new Error("snapshot accepts read-only tool requests with object arguments");
|
|
33
24
|
}
|
|
34
25
|
if (LOG_OR_EVENT.test(entry.name) && !Object.keys(entry.arguments).some((key) => BOUND_KEYS.has(key))) {
|
|
@@ -43,6 +34,7 @@ function redact(value) {
|
|
|
43
34
|
if (!isPlainObject(value)) return value;
|
|
44
35
|
const output = {};
|
|
45
36
|
for (const [key, child] of Object.entries(value)) {
|
|
37
|
+
if (key === "request_body") continue;
|
|
46
38
|
if (SENSITIVE_KEY.test(key)) continue;
|
|
47
39
|
output[key] = redact(child);
|
|
48
40
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const MODULES = Object.freeze({
|
|
6
|
+
"rainskills-tools.js": "main",
|
|
7
|
+
"handoff-context.js": "createHandoffContext",
|
|
8
|
+
"read-snapshot.js": "executeReadSnapshot",
|
|
9
|
+
"protected-poll.js": "executeProtectedPoll",
|
|
10
|
+
"delivery-probe.js": "createDeliveryProbe",
|
|
11
|
+
"bridge-errors.js": "errorResult",
|
|
12
|
+
"confirmed-batch.js": "validateBatch",
|
|
13
|
+
"component-recipe.js": "prepareRecipe",
|
|
14
|
+
"tool-policy.js": "classifyTool",
|
|
15
|
+
});
|
|
16
|
+
function checkRuntimeIntegrity(directory = __dirname) {
|
|
17
|
+
const missing = [];
|
|
18
|
+
for (const [name, entrypoint] of Object.entries(MODULES)) {
|
|
19
|
+
try {
|
|
20
|
+
const target = path.join(directory, name);
|
|
21
|
+
const info = fs.lstatSync(target);
|
|
22
|
+
if (!info.isFile() || info.isSymbolicLink() || typeof require(target)[entrypoint] !== "function") missing.push(name);
|
|
23
|
+
} catch { missing.push(name); }
|
|
24
|
+
}
|
|
25
|
+
return { status: missing.length ? "failed" : "ok", missing };
|
|
26
|
+
}
|
|
27
|
+
module.exports = { checkRuntimeIntegrity };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const http = require("node:http");
|
|
7
|
+
const { spawn } = require("node:child_process");
|
|
8
|
+
const { checkRuntimeIntegrity } = require("./runtime-integrity.js");
|
|
9
|
+
|
|
10
|
+
async function smokeInstalledRuntime(home) {
|
|
11
|
+
if (checkRuntimeIntegrity().status !== "ok") throw new Error("installed runtime integrity failed");
|
|
12
|
+
const probeHome = fs.mkdtempSync(path.join(os.tmpdir(), "rainskills-smoke-"));
|
|
13
|
+
const sockets = new Set();
|
|
14
|
+
const server = http.createServer((request, response) => {
|
|
15
|
+
let body = "";
|
|
16
|
+
request.on("data", (chunk) => { body += chunk; });
|
|
17
|
+
request.on("end", () => {
|
|
18
|
+
const message = JSON.parse(body);
|
|
19
|
+
const result = message.method === "tools/list"
|
|
20
|
+
? { tools: [{ name: "rainbond_get_smoke_status", inputSchema: { type: "object" } }] }
|
|
21
|
+
: { structuredContent: { status: "Running" } };
|
|
22
|
+
response.writeHead(200, { "Content-Type": "application/json" });
|
|
23
|
+
response.end(JSON.stringify({ jsonrpc: "2.0", id: message.id, result }));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
server.on("connection", (socket) => { sockets.add(socket); socket.on("close", () => sockets.delete(socket)); });
|
|
27
|
+
try {
|
|
28
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
29
|
+
require("../rainbond-platform-installer/scripts/single-runtime.js").createSingleRuntimeStore({ home: probeHome }).write({ kind: "private", consoleOrigin: `http://127.0.0.1:${server.address().port}`, token: "smoke.payload.signature", allowInsecureHttp: true });
|
|
30
|
+
const bridge = path.join(home, ".rainbond/bin/rainskills-tools.js");
|
|
31
|
+
const run = (args, input) => new Promise((resolve, reject) => {
|
|
32
|
+
const child = spawn(process.execPath, [bridge, ...args, "--input", "-", "--skill-id", "rainbond-app-assistant"], {
|
|
33
|
+
cwd: probeHome, env: { ...process.env, HOME: probeHome, USERPROFILE: probeHome, RAINSKILLS_TELEMETRY_DISABLED: "1", RAINSKILLS_CREDENTIAL_SOURCE: "", RAINBOND_URL: "", RAINBOND_JWT: "" }, stdio: ["pipe", "pipe", "pipe"],
|
|
34
|
+
});
|
|
35
|
+
let stdout = "";
|
|
36
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 5000);
|
|
37
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
38
|
+
child.stderr.resume();
|
|
39
|
+
child.on("error", (error) => { clearTimeout(timer); reject(error); });
|
|
40
|
+
child.on("close", (code) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
if (code !== 0) return reject(new Error(`installed ${args[0]} smoke failed`));
|
|
43
|
+
try { resolve(JSON.parse(stdout)); } catch { reject(new Error("invalid installed smoke output")); }
|
|
44
|
+
});
|
|
45
|
+
child.stdin.end(JSON.stringify(input));
|
|
46
|
+
});
|
|
47
|
+
const context = await run(["handoff", "create"], { producer: "rainbond-app-assistant", profile: "cli", observed_at: new Date().toISOString(), enterprise_id: "smoke", team_id: "smoke", team_name: "smoke", region_name: "smoke", app_id: null, app_name: "smoke", endpoint: "https://example.invalid", runtime_revision: "smoke", snapshot_revision: null, source_kind: "image", runtime_state: "topology_missing", next_phase: "init", invalidation_reason: null });
|
|
48
|
+
const snapshot = await run(["snapshot", "app"], { scope: "app", requests: [{ name: "rainbond_get_smoke_status", arguments: {} }] });
|
|
49
|
+
const poll = await run(["poll", "rainbond_get_smoke_status"], { arguments: {}, status_path: "status", terminal_values: ["Running", "Failed"], failure_values: ["Failed"], max_attempts: 1, interval_ms: 0, timeout_ms: 1000 });
|
|
50
|
+
if (context.schema !== "rainskills.handoff-context.v1" || snapshot.evidence?.[0]?.available !== true || poll.completed !== true) throw new Error("installed runtime smoke contract failed");
|
|
51
|
+
return { status: "ok", checked: ["handoff", "snapshot", "poll"] };
|
|
52
|
+
} finally {
|
|
53
|
+
for (const socket of sockets) socket.destroy();
|
|
54
|
+
await new Promise((resolve) => server.close(resolve));
|
|
55
|
+
fs.rmSync(probeHome, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
module.exports = { smokeInstalledRuntime };
|
|
59
|
+
if (require.main === module) {
|
|
60
|
+
const home = process.argv[2];
|
|
61
|
+
if (!home) { process.stderr.write("installed home is required\n"); process.exitCode = 2; }
|
|
62
|
+
else smokeInstalledRuntime(path.resolve(home)).then((result) => process.stdout.write(`${JSON.stringify(result)}\n`)).catch(() => { process.stderr.write("installed runtime smoke failed\n"); process.exitCode = 1; });
|
|
63
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const RISK_POLICY = Object.freeze({
|
|
4
|
+
version: "2",
|
|
5
|
+
readPrefixes: ["rainbond_query_", "rainbond_get_", "rainbond_list_", "rainbond_check_", "rainbond_describe_", "rainbond_validate_", "rainbond_verify_", "rainbond_search_"],
|
|
6
|
+
destructiveFragments: ["delete", "remove", "purge", "destroy"],
|
|
7
|
+
});
|
|
8
|
+
const SUMMARY_TOOLS = new Set(["ports", "dependency", "storage", "connection_envs", "envs"].map((name) => `rainbond_manage_component_${name}`));
|
|
9
|
+
function classifyTool(toolName, argumentsValue = {}) {
|
|
10
|
+
const name = String(toolName || "").toLowerCase();
|
|
11
|
+
const operation = typeof argumentsValue.operation === "string" ? argumentsValue.operation.trim().toLowerCase() : "";
|
|
12
|
+
if (SUMMARY_TOOLS.has(name)) {
|
|
13
|
+
if (operation === "summary") return "read";
|
|
14
|
+
if (RISK_POLICY.destructiveFragments.some((part) => operation.includes(part))) return "destructive";
|
|
15
|
+
return "write";
|
|
16
|
+
}
|
|
17
|
+
if (RISK_POLICY.destructiveFragments.some((part) => name.includes(part))) return "destructive";
|
|
18
|
+
if (name === "rainbond_wait_for_build_completion" || RISK_POLICY.readPrefixes.some((prefix) => name.startsWith(prefix))) return "read";
|
|
19
|
+
return "write";
|
|
20
|
+
}
|
|
21
|
+
module.exports = { RISK_POLICY, classifyTool };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
$schema: https://json-schema.org/draft/2020-12/schema
|
|
2
|
+
$id: rainskills.component-recipe.v1.request
|
|
3
|
+
title: Explicit component creation and configuration recipe
|
|
4
|
+
type: object
|
|
5
|
+
additionalProperties: false
|
|
6
|
+
required: [schema, scope, enterprise_id, kind, create]
|
|
7
|
+
properties:
|
|
8
|
+
schema: {const: rainskills.component-recipe.v1.request}
|
|
9
|
+
scope: {$ref: 'confirmed-batch.schema.yaml#/$defs/scope'}
|
|
10
|
+
enterprise_id: {type: string, minLength: 1}
|
|
11
|
+
kind: {enum: [image, source, package]}
|
|
12
|
+
create:
|
|
13
|
+
type: object
|
|
14
|
+
required: [service_cname]
|
|
15
|
+
description: Exact live create-tool arguments. is_deploy must be false; source URL/ref and package event remain unchanged. Existing names block creation.
|
|
16
|
+
configure:
|
|
17
|
+
type: array
|
|
18
|
+
maxItems: 32
|
|
19
|
+
description: Configuration batch steps without service_id. Only the create response may supply that ID.
|
|
20
|
+
items: {type: object, required: [key, tool, arguments]}
|
|
21
|
+
stateful: {type: boolean}
|
|
22
|
+
storage:
|
|
23
|
+
type: object
|
|
24
|
+
required: [volume_name, volume_type, volume_path]
|
|
25
|
+
description: Explicit create_volume parameters, applied before deployment. Scope, operation and service_id are supplied by the recipe.
|
|
26
|
+
allOf:
|
|
27
|
+
- if:
|
|
28
|
+
properties: {stateful: {const: true}}
|
|
29
|
+
required: [stateful]
|
|
30
|
+
then: {required: [storage]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
$schema: https://json-schema.org/draft/2020-12/schema
|
|
2
|
+
$id: rainskills.confirmed-batch.v1.request
|
|
3
|
+
title: Confirmed configuration batch
|
|
4
|
+
type: object
|
|
5
|
+
additionalProperties: false
|
|
6
|
+
required: [schema, scope, steps]
|
|
7
|
+
properties:
|
|
8
|
+
schema: {const: rainskills.confirmed-batch.v1.request}
|
|
9
|
+
scope: {$ref: '#/$defs/scope'}
|
|
10
|
+
steps:
|
|
11
|
+
type: array
|
|
12
|
+
minItems: 1
|
|
13
|
+
maxItems: 32
|
|
14
|
+
items:
|
|
15
|
+
type: object
|
|
16
|
+
additionalProperties: false
|
|
17
|
+
required: [key, tool, arguments]
|
|
18
|
+
properties:
|
|
19
|
+
key: {type: string, minLength: 1, maxLength: 64, description: At most 64 UTF-8 bytes.}
|
|
20
|
+
tool:
|
|
21
|
+
enum: [rainbond_manage_component_envs, rainbond_manage_component_ports, rainbond_manage_component_connection_envs, rainbond_manage_component_dependency]
|
|
22
|
+
arguments:
|
|
23
|
+
type: object
|
|
24
|
+
description: Validated against the live tool schema and the exact configuration-operation allowlist. Scope overrides are rejected. No delete or arbitrary result bindings.
|
|
25
|
+
$defs:
|
|
26
|
+
scope:
|
|
27
|
+
type: object
|
|
28
|
+
additionalProperties: false
|
|
29
|
+
required: [team_name, region_name, app_id]
|
|
30
|
+
properties:
|
|
31
|
+
team_name: {type: string, minLength: 1, maxLength: 128}
|
|
32
|
+
region_name: {type: string, minLength: 1, maxLength: 128}
|
|
33
|
+
app_id: {type: integer, minimum: 1}
|
|
34
|
+
result:
|
|
35
|
+
type: object
|
|
36
|
+
required: [schema, status, completed, steps, blocker]
|
|
37
|
+
properties:
|
|
38
|
+
schema: {const: rainskills.confirmed-batch.v1.result}
|
|
39
|
+
status: {enum: [succeeded, partial, failed, unknown]}
|
|
40
|
+
completed: {type: integer, minimum: 0}
|
|
41
|
+
steps: {type: array, maxItems: 32}
|
|
42
|
+
blocker: {type: [object, 'null']}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<!-- rainskills-runtime-gate:start -->
|
|
2
2
|
# 单运行环境 CLI 门禁(生成文件)
|
|
3
3
|
|
|
4
|
-
本机只允许连接一个 Rainbond 运行环境。当前 Skill 在本会话第一次调用 Rainbond 前,执行固定 launcher 的 `runtime status --json`。只有命令退出码为 0、`state=connected` 且 `usable=true` 时才继续。不得配置或直接调用客户端 MCP
|
|
4
|
+
本机只允许连接一个 Rainbond 运行环境。当前 Skill 在本会话第一次调用 Rainbond 前,执行固定 launcher 的 `runtime status --json`。只有命令退出码为 0、`state=connected` 且 `usable=true` 时才继续。不得配置或直接调用客户端 MCP,不得枚举环境、读取凭据、创建自定义业务 operation,或生成运行环境 ID、业务 operation ID 与 intent JSON。固定 batch/recipe 请求和 CLI 返回的 confirmation_id 属于受保护调用协议,不是模型自建业务工作流。
|
|
5
5
|
|
|
6
6
|
{{VERSION_GUARD}}
|
|
7
7
|
|
|
8
|
+
如果 integrity.status=failed 或 repair_required=true,停止业务调用并修复相同版本的本地安装;不得将缺模块当作 API 参数错误排查或绕过兼容入口。
|
|
9
|
+
|
|
8
10
|
没有可用运行环境时,严格按下方 overlay 的 `missing_runtime_mode` 进入对应的 Runtime Routing;不得扩大可选环境范围。连接和重新授权必须使用浏览器 Device Flow,不复用 Shell 中缓存的 JWT;Device Flow 不依赖 stdin TTY,必须保持进程附着直到完成。新凭据只有通过 live probe 后才可覆盖唯一运行环境。CLI 返回 401 时,只读调用可在 `runtime reconnect` 成功后重试一次;写调用不得自动重放,必须先查询平台真实状态。403 直接停止,不重新授权。
|
|
9
11
|
|
|
10
12
|
授权命令是同步门禁。命令返回“进程仍在运行”或会话 ID 时,只等待或轮询同一个命令会话;在该会话结束前禁止任何后续业务步骤。浏览器页面显示成功不代表完成;只有原命令退出码为 0,并输出 `rainskills.runtime-connect-result.v1` 且 `state=connected`,才可继续。不得另起 `runtime status` 猜测完成。
|