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/SKILL.md
CHANGED
|
@@ -18,6 +18,8 @@ If the adjacent installer is missing, check the local Node.js version before cho
|
|
|
18
18
|
|
|
19
19
|
Skills-only 安装不需要 Node.js;CDN fallback 只负责安装 Skill 文件,不代表运行环境连接、应用部署或平台安装已经可执行。用户首次提出需要运行环境的动作时,对应业务 Skill 才检查 Node.js;固定 Rainskills launcher 需要 Node.js 18 或更高版本。缺失或版本过低时保留原始 intent 并停止,等待用户或 agent 明确同意安装或升级 Node.js,安装完成消息不得提前提示 Node.js。
|
|
20
20
|
|
|
21
|
+
WorkBuddy 中安装、更新和修复必须以前台附着方式启动,不得主动设置后台执行、使用 `&` 或 `nohup`。需要访问 `~/.rainbond` 或确认批量删除时,通过宿主的正常授权流程处理;发生权限或 `SAFE_DELETE_BULK_CONFIRM_REQUIRED` 错误时,先处理授权,不得重复启动后台重试或拆分删除来规避守卫。宿主自动返回持久句柄时继续等待原句柄;安装失败时保留 `[rainskills-cli]` 阶段和原始错误,不能仅凭 Skill 已复制就报告完整安装成功。安装验证成功后的 `[rainskills-cli][cleanup]` 提示表示临时文件待清理,不等于安装失败。
|
|
22
|
+
|
|
21
23
|
执行优化:同一会话内只检查一次 Node.js 和运行环境状态;仅在 Node.js、Rainskills 或唯一运行环境发生变化后失效。固定 launcher 和 argv 已在本 Skill 中,禁止读取、搜索或探测 `rainskills.js`,也禁止执行 `npm root -g`。
|
|
22
24
|
|
|
23
25
|
## Completion Message
|
|
@@ -54,7 +56,7 @@ Hermes Agent 中必须使用 `terminal` 以 `background=true` 启动授权命令
|
|
|
54
56
|
|
|
55
57
|
DeepSeek Harness 和 WorkBuddy 也必须保持原命令附着直到退出;如果宿主把长命令转成持久终端或后台任务,只能轮询该原始命令句柄,不得另起 `runtime status` 推测授权结果。WorkBuddy 安装后若当前任务的 Skill 清单尚未刷新,在下一条业务动作前先刷新 Skill 列表或新建任务,不得退回内置 Sites 代替 Rainbond 部署。
|
|
56
58
|
|
|
57
|
-
Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.
|
|
59
|
+
Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.46`):
|
|
58
60
|
|
|
59
61
|
- 状态:执行 `runtime status --json`。
|
|
60
62
|
- 首次连接:执行 `runtime connect <target> --saas` 或 `runtime connect <target> --rainbond-url <Console origin>`。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const EXIT = Object.freeze({ USAGE: 2, CONFIG: 3, TRANSPORT: 4, TOOL: 5 });
|
|
4
|
+
class BridgeError extends Error {
|
|
5
|
+
constructor(message, exitCode, payload, code) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.exitCode = exitCode;
|
|
8
|
+
this.payload = payload;
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function inputValidation(action) {
|
|
13
|
+
try { return action(); } catch (error) {
|
|
14
|
+
if (error instanceof BridgeError) throw error;
|
|
15
|
+
throw new BridgeError("input validation failed; check the command schema", EXIT.USAGE, undefined, "E_INPUT_INVALID");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function errorResult(error) {
|
|
19
|
+
const code = error.code || ({ 2: "E_INPUT_INVALID", 3: "E_CONFIGURATION", 4: "E_TRANSPORT", 5: "E_PLATFORM" })[error.exitCode] || "E_INTERNAL";
|
|
20
|
+
return {
|
|
21
|
+
...(error.payload || { error: error.message }), code,
|
|
22
|
+
stage: code === "E_RUNTIME_INCOMPLETE" ? "local" : error.exitCode === EXIT.USAGE ? "input" : error.exitCode === EXIT.CONFIG ? "configuration" : "request",
|
|
23
|
+
next_action: code === "E_RUNTIME_INCOMPLETE" ? "repair_runtime" : error.exitCode === EXIT.USAGE ? "correct_input" : code === "E_WRITE_RESULT_UNKNOWN" ? "query_state" : "inspect_error",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
module.exports = { BridgeError, EXIT, inputValidation, errorResult };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { BridgeError, EXIT } = require("./bridge-errors.js");
|
|
3
|
+
const { validateBatch, executeBatch, projectResult } = require("./confirmed-batch.js");
|
|
4
|
+
const CREATE_TOOLS = Object.freeze({ image: "rainbond_create_component_from_image", source: "rainbond_create_component_from_source", package: "rainbond_create_component_from_package" });
|
|
5
|
+
const PLACEHOLDER = "0".repeat(32);
|
|
6
|
+
function invalid(message = "invalid component recipe; use the component-recipe schema") { throw new BridgeError(message, EXIT.USAGE); }
|
|
7
|
+
function prepareRecipe(input) {
|
|
8
|
+
if (!input || input.schema !== "rainskills.component-recipe.v1.request" || Buffer.byteLength(JSON.stringify(input)) > 128 * 1024) invalid();
|
|
9
|
+
if (Object.keys(input).some((key) => !["schema", "scope", "enterprise_id", "kind", "create", "configure", "stateful", "storage"].includes(key))) invalid();
|
|
10
|
+
if (!Object.hasOwn(CREATE_TOOLS, input.kind) || typeof input.enterprise_id !== "string" || !input.enterprise_id) invalid();
|
|
11
|
+
if (!input.create || Array.isArray(input.create) || typeof input.create.service_cname !== "string" || !input.create.service_cname) invalid();
|
|
12
|
+
if (input.create.is_deploy !== undefined && input.create.is_deploy !== false) invalid("creation recipes configure before deployment");
|
|
13
|
+
if (input.stateful !== undefined && typeof input.stateful !== "boolean") invalid();
|
|
14
|
+
if (input.stateful && (!input.storage || !input.storage.volume_path || !input.storage.volume_name || !input.storage.volume_type)) invalid("stateful providers require explicit persistent storage before deployment");
|
|
15
|
+
if (input.kind === "image" && !input.create.image) invalid();
|
|
16
|
+
if (input.kind === "source" && (!input.create.git_url || !input.create.code_from)) invalid();
|
|
17
|
+
if (input.kind === "package" && !input.create.event_id) invalid();
|
|
18
|
+
const configuration = input.configure || [];
|
|
19
|
+
if (!Array.isArray(configuration) || configuration.length > 32 || configuration.some((s) => !s || typeof s !== "object" || Array.isArray(s) || !s.arguments || typeof s.arguments !== "object" || Array.isArray(s.arguments) || Object.hasOwn(s.arguments, "service_id"))) invalid();
|
|
20
|
+
const steps = configuration.map((step) => ({ ...step, arguments: { ...step.arguments, service_id: PLACEHOLDER } }));
|
|
21
|
+
// Reuse the same scope and configuration policy even for an empty recipe.
|
|
22
|
+
validateBatch({ schema: "rainskills.confirmed-batch.v1.request", scope: input.scope, steps: steps.length ? steps : [{ key: "scope-validation", tool: "rainbond_manage_component_ports", arguments: { service_id: PLACEHOLDER, operation: "add", port: 80, protocol: "http" } }] });
|
|
23
|
+
for (const [key, value] of Object.entries(input.scope)) if (Object.hasOwn(input.create, key) && input.create[key] !== value) invalid();
|
|
24
|
+
if (input.storage && Object.keys(input.storage).some((key) => ["service_id", "team_name", "region_name", "app_id", "operation"].includes(key))) invalid();
|
|
25
|
+
return { tool: CREATE_TOOLS[input.kind], arguments: { ...input.create, ...input.scope, is_deploy: false }, steps };
|
|
26
|
+
}
|
|
27
|
+
async function preflightRecipe(input, { validate, read }) {
|
|
28
|
+
const plan = prepareRecipe(input);
|
|
29
|
+
await validate(plan.tool, plan.arguments);
|
|
30
|
+
for (const step of plan.steps) await validate(step.tool, { ...step.arguments, ...input.scope });
|
|
31
|
+
if (input.storage) await validate("rainbond_manage_component_storage", { ...input.storage, ...input.scope, service_id: PLACEHOLDER, operation: "create_volume" });
|
|
32
|
+
const query = { enterprise_id: input.enterprise_id, app_id: input.scope.app_id, query: input.create.service_cname, page: 1, page_size: 20 };
|
|
33
|
+
await validate("rainbond_query_components", query);
|
|
34
|
+
const observed = await read("rainbond_query_components", query);
|
|
35
|
+
const items = observed?.list || observed?.items;
|
|
36
|
+
if (!Array.isArray(items) || (typeof observed.total === "number" && observed.total > items.length)) invalid("component lookup is incomplete; resolve the exact existing topology first");
|
|
37
|
+
if (items.some((item) => item.service_cname === input.create.service_cname || item.service_alias === input.create.service_cname)) invalid("component already exists; adopt its verified service_id instead of creating again");
|
|
38
|
+
if (input.kind === "package") {
|
|
39
|
+
const args = { team_name: input.scope.team_name, region_name: input.scope.region_name, event_id: input.create.event_id };
|
|
40
|
+
await validate("rainbond_get_package_upload_status", args);
|
|
41
|
+
const status = await read("rainbond_get_package_upload_status", args);
|
|
42
|
+
if (!Array.isArray(status?.uploaded_packages) || !status.uploaded_packages.length) invalid("package upload is empty; complete the protected upload and local cleanup before creation");
|
|
43
|
+
}
|
|
44
|
+
return plan;
|
|
45
|
+
}
|
|
46
|
+
async function executeRecipe(input, adapters) {
|
|
47
|
+
const plan = await preflightRecipe(input, adapters);
|
|
48
|
+
if (adapters.signal?.aborted) throw new BridgeError("recipe cancelled", EXIT.USAGE, undefined, "E_CANCELLED");
|
|
49
|
+
const created = await adapters.call(plan.tool, plan.arguments);
|
|
50
|
+
const serviceId = created?.service_id || created?.component?.service_id || created?.service?.service_id;
|
|
51
|
+
if (typeof serviceId !== "string" || !serviceId || serviceId.length > 128) throw new BridgeError("creation result lacks a verified service_id; query the exact target before continuing", EXIT.TRANSPORT, undefined, "E_WRITE_RESULT_UNKNOWN");
|
|
52
|
+
const identity = { service_id: serviceId, ...projectResult(created) };
|
|
53
|
+
delete identity.status;
|
|
54
|
+
if (adapters.onCreated) await adapters.onCreated(identity);
|
|
55
|
+
if (adapters.signal?.aborted) return { schema: "rainskills.component-recipe.v1.result", ...identity, status: "partial", blocker: { code: "E_CANCELLED" }, next_action: "query_state" };
|
|
56
|
+
if (input.storage) {
|
|
57
|
+
try { await adapters.call("rainbond_manage_component_storage", { ...input.storage, ...input.scope, service_id: serviceId, operation: "create_volume" }); }
|
|
58
|
+
catch (error) {
|
|
59
|
+
return { schema: "rainskills.component-recipe.v1.result", status: error.exitCode === EXIT.TRANSPORT ? "unknown" : "partial", ...identity, blocker: { code: error.exitCode === EXIT.TRANSPORT ? "E_WRITE_RESULT_UNKNOWN" : "E_PLATFORM" }, next_action: "query_state" };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let configured = { status: "succeeded", completed: 0, steps: [], blocker: null };
|
|
63
|
+
if (plan.steps.length) configured = await executeBatch({ schema: "rainskills.confirmed-batch.v1.request", scope: input.scope, steps: plan.steps.map((step) => ({ ...step, arguments: { ...step.arguments, service_id: serviceId } })) }, adapters);
|
|
64
|
+
return { schema: "rainskills.component-recipe.v1.result", ...identity, status: configured.status === "failed" ? "partial" : configured.status, configured: configured.completed, steps: configured.steps, blocker: configured.blocker, next_action: configured.status === "succeeded" ? "deploy_and_verify" : "query_state" };
|
|
65
|
+
}
|
|
66
|
+
module.exports = { prepareRecipe, preflightRecipe, executeRecipe };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { BridgeError, EXIT } = require("./bridge-errors.js");
|
|
4
|
+
const SCHEMA = "rainskills.confirmed-batch.v1.request";
|
|
5
|
+
const CONFIG_OPERATIONS = Object.freeze({
|
|
6
|
+
rainbond_manage_component_envs: ["upsert"],
|
|
7
|
+
rainbond_manage_component_ports: ["add", "enable_inner", "enable_outer", "disable_outer", "update_protocol", "update_alias"],
|
|
8
|
+
rainbond_manage_component_connection_envs: ["create", "update"],
|
|
9
|
+
rainbond_manage_component_dependency: ["add"],
|
|
10
|
+
});
|
|
11
|
+
const plain = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
12
|
+
const validString = (v) => typeof v === "string" && v.length > 0 && v.length <= 128 && !/[\u0000-\u001f\u007f]/u.test(v);
|
|
13
|
+
function invalid() { throw new BridgeError("invalid batch; use the confirmed-batch schema and one verified app scope", EXIT.USAGE); }
|
|
14
|
+
function keys(value, allowed) { if (!plain(value) || Object.keys(value).some((key) => !allowed.includes(key))) invalid(); }
|
|
15
|
+
function validateBatch(input) {
|
|
16
|
+
if (Buffer.byteLength(JSON.stringify(input) || "") > 128 * 1024) invalid();
|
|
17
|
+
keys(input, ["schema", "scope", "steps"]);
|
|
18
|
+
if (input.schema !== SCHEMA) invalid();
|
|
19
|
+
keys(input.scope, ["team_name", "region_name", "app_id"]);
|
|
20
|
+
const scope = input.scope;
|
|
21
|
+
if (!validString(scope.team_name) || !validString(scope.region_name) || !Number.isInteger(scope.app_id) || scope.app_id < 1) invalid();
|
|
22
|
+
if (!Array.isArray(input.steps) || input.steps.length < 1 || input.steps.length > 32) invalid();
|
|
23
|
+
const names = new Set();
|
|
24
|
+
return input.steps.map((step) => {
|
|
25
|
+
keys(step, ["key", "tool", "arguments"]);
|
|
26
|
+
if (!validString(step.key) || Buffer.byteLength(step.key) > 64 || names.has(step.key) || !plain(step.arguments)) invalid();
|
|
27
|
+
names.add(step.key);
|
|
28
|
+
if (!Object.hasOwn(CONFIG_OPERATIONS, step.tool) || !CONFIG_OPERATIONS[step.tool].includes(step.arguments.operation)) invalid();
|
|
29
|
+
if (!validString(step.arguments.service_id)) invalid();
|
|
30
|
+
for (const [key, value] of Object.entries(scope)) {
|
|
31
|
+
if (Object.hasOwn(step.arguments, key) && step.arguments[key] !== value) invalid();
|
|
32
|
+
}
|
|
33
|
+
return { key: step.key, tool: step.tool, arguments: { ...step.arguments, ...scope } };
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function projectResult(value) {
|
|
37
|
+
const output = {};
|
|
38
|
+
for (const key of ["service_id", "event_id", "app_id", "created", "updated", "status"]) {
|
|
39
|
+
if (["string", "boolean", "number"].includes(typeof value?.[key])) output[key] = typeof value[key] === "string" ? value[key].slice(0, 128) : value[key];
|
|
40
|
+
}
|
|
41
|
+
return output;
|
|
42
|
+
}
|
|
43
|
+
function boundedResult(output) {
|
|
44
|
+
if (Buffer.byteLength(JSON.stringify(output)) <= 8192) return output;
|
|
45
|
+
return { ...output, details_omitted: true, steps: output.steps.map(({ key, status }) => ({ key, status })) };
|
|
46
|
+
}
|
|
47
|
+
function configurationSatisfied(tool, args, snapshot) {
|
|
48
|
+
if (!snapshot || typeof snapshot !== "object") return false;
|
|
49
|
+
if (tool === "rainbond_manage_component_ports") {
|
|
50
|
+
const items = snapshot.ports?.items;
|
|
51
|
+
if (!Array.isArray(items)) return false;
|
|
52
|
+
const desired = args.ports || [{ port: args.port, protocol: args.protocol, port_alias: args.port_alias, is_inner_service: args.is_inner_service ?? args.enable_inner }];
|
|
53
|
+
return desired.length > 0 && desired.every((item) => {
|
|
54
|
+
const port = typeof item === "number" ? item : item.port;
|
|
55
|
+
const current = items.find((value) => (value.container_port ?? value.port) === port);
|
|
56
|
+
if (!current) return false;
|
|
57
|
+
if (args.operation === "add") {
|
|
58
|
+
const requested = typeof item === "number" ? {} : { ...item, is_inner_service: item.is_inner_service ?? item.enable_inner };
|
|
59
|
+
return ["protocol", "port_alias", "is_inner_service", "k8s_service_name"].every((key) => requested[key] === undefined || current[key] === requested[key]);
|
|
60
|
+
}
|
|
61
|
+
if (args.operation === "enable_inner") return current.is_inner_service === true;
|
|
62
|
+
if (args.operation === "enable_outer") return current.is_outer_service === true;
|
|
63
|
+
if (args.operation === "disable_outer") return current.is_outer_service === false;
|
|
64
|
+
return args.operation === "update_protocol" ? current.protocol === args.protocol : args.operation === "update_alias" && ["port_alias", "k8s_service_name"].some((key) => args[key] !== undefined) && ["port_alias", "k8s_service_name"].every((key) => args[key] === undefined || current[key] === args[key]);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (tool === "rainbond_manage_component_dependency") {
|
|
68
|
+
if (!Array.isArray(snapshot.dependencies) || args.open_inner) return false;
|
|
69
|
+
const desired = args.dep_service_ids || [args.dep_service_id];
|
|
70
|
+
return desired.length > 0 && desired.every((id) => id && snapshot.dependencies.some((value) => (typeof value === "string" ? value : value.service_id) === id));
|
|
71
|
+
}
|
|
72
|
+
if (tool === "rainbond_manage_component_envs" && args.operation === "upsert") {
|
|
73
|
+
const items = snapshot.custom_envs?.items;
|
|
74
|
+
const desired = args.envs || [{ attr_name: args.attr_name || args.name, attr_value: args.attr_value, is_change: args.is_change }];
|
|
75
|
+
if (!Array.isArray(items) || !desired.length) return false;
|
|
76
|
+
return desired.every((env) => items.some((item) => item.attr_name === (env.attr_name || env.name)
|
|
77
|
+
&& item.attr_value !== "[REDACTED]" && item.attr_value === env.attr_value
|
|
78
|
+
&& (env.is_change === undefined || item.is_change === env.is_change)));
|
|
79
|
+
}
|
|
80
|
+
if (tool === "rainbond_manage_component_connection_envs") {
|
|
81
|
+
const items = snapshot.connection_envs?.items;
|
|
82
|
+
if (!Array.isArray(items) || args.operation !== "create") return false;
|
|
83
|
+
return items.some((item) => item.attr_name === (args.attr_name || args.name) && item.attr_value !== "[REDACTED]" && item.attr_value === args.attr_value);
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
async function executeBatch(input, { validate, call, signal, isSatisfied = async () => false, onProgress = async () => {} }) {
|
|
88
|
+
const steps = validateBatch(input);
|
|
89
|
+
for (const step of steps) await validate(step.tool, step.arguments);
|
|
90
|
+
const output = { schema: "rainskills.confirmed-batch.v1.result", status: "succeeded", completed: 0, skipped: 0, steps: steps.map(({ key }) => ({ key, status: "not_executed" })), blocker: null };
|
|
91
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
92
|
+
const step = steps[index];
|
|
93
|
+
try {
|
|
94
|
+
if (signal?.aborted) throw Object.assign(new Error("cancelled"), { code: "E_CANCELLED" });
|
|
95
|
+
if (await isSatisfied(step)) {
|
|
96
|
+
output.skipped += 1;
|
|
97
|
+
output.steps[index] = { key: step.key, status: "skipped" };
|
|
98
|
+
await onProgress(output);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (signal?.aborted) throw Object.assign(new Error("cancelled"), { code: "E_CANCELLED" });
|
|
102
|
+
const value = await call(step.tool, step.arguments);
|
|
103
|
+
output.steps[index] = { key: step.key, status: "succeeded", ...projectResult(value) };
|
|
104
|
+
// The batch status describes execution, not a platform-specific status field.
|
|
105
|
+
output.steps[index].status = "succeeded";
|
|
106
|
+
output.completed += 1;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const unknown = error.code === "E_WRITE_RESULT_UNKNOWN" || error.exitCode === EXIT.TRANSPORT;
|
|
109
|
+
output.steps[index] = { key: step.key, status: unknown ? "unknown" : "failed" };
|
|
110
|
+
output.status = unknown ? "unknown" : output.completed ? "partial" : "failed";
|
|
111
|
+
const code = unknown ? "E_WRITE_RESULT_UNKNOWN" : ["E_CANCELLED", "E_PLAN_STALE"].includes(error.code) ? error.code : "E_PLATFORM";
|
|
112
|
+
output.blocker = { key: step.key, code, next_action: unknown ? "query_state" : "inspect_error" };
|
|
113
|
+
await onProgress(output);
|
|
114
|
+
return boundedResult(output);
|
|
115
|
+
}
|
|
116
|
+
await onProgress(output);
|
|
117
|
+
}
|
|
118
|
+
return boundedResult(output);
|
|
119
|
+
}
|
|
120
|
+
module.exports = { validateBatch, executeBatch, projectResult, configurationSatisfied };
|
package/bin/protected-poll.js
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
"rainbond_query_", "rainbond_get_", "rainbond_list_", "rainbond_check_",
|
|
5
|
-
"rainbond_describe_", "rainbond_validate_", "rainbond_verify_", "rainbond_search_",
|
|
6
|
-
];
|
|
7
|
-
const DESTRUCTIVE = /(?:delete|remove|purge|destroy|update|create|set|manage|operate|restart|deploy)/i;
|
|
3
|
+
const { classifyTool } = require("./tool-policy.js");
|
|
8
4
|
const STATUS_PATH = /^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*){0,3}$/;
|
|
9
5
|
|
|
10
6
|
function plain(value) {
|
|
11
7
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
12
8
|
}
|
|
13
9
|
|
|
14
|
-
function readOnly(name) {
|
|
15
|
-
return typeof name === "string"
|
|
16
|
-
&& READ_PREFIXES.some((prefix) => name.startsWith(prefix))
|
|
17
|
-
&& !DESTRUCTIVE.test(name);
|
|
18
|
-
}
|
|
19
10
|
|
|
20
11
|
function stringArray(value, label) {
|
|
21
12
|
if (!Array.isArray(value) || value.length < 1 || value.some((item) => typeof item !== "string" || !item)) {
|
|
@@ -24,7 +15,7 @@ function stringArray(value, label) {
|
|
|
24
15
|
}
|
|
25
16
|
|
|
26
17
|
function validatePollInput(toolName, input) {
|
|
27
|
-
if (
|
|
18
|
+
if (classifyTool(toolName, input?.arguments) !== "read") throw new Error("poll accepts only a read-only tool");
|
|
28
19
|
if (!plain(input) || !plain(input.arguments)) throw new Error("poll input and arguments must be objects");
|
|
29
20
|
if (!STATUS_PATH.test(input.status_path || "") || input.status_path.includes("__proto__")) throw new Error("status_path is invalid");
|
|
30
21
|
stringArray(input.terminal_values, "terminal_values");
|
|
@@ -36,6 +27,25 @@ function validatePollInput(toolName, input) {
|
|
|
36
27
|
return input;
|
|
37
28
|
}
|
|
38
29
|
|
|
30
|
+
async function withinBudget(action, milliseconds, signal) {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
let timer;
|
|
33
|
+
let abort;
|
|
34
|
+
const stopped = new Promise((_resolve, reject) => {
|
|
35
|
+
abort = () => { controller.abort(); reject(new Error("cancelled")); };
|
|
36
|
+
if (signal?.aborted) return abort();
|
|
37
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
38
|
+
timer = setTimeout(() => { controller.abort(); reject(new Error("timeout")); }, Math.max(1, milliseconds));
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
if (signal?.aborted) return await stopped;
|
|
42
|
+
return await Promise.race([stopped, action(controller.signal)]);
|
|
43
|
+
} finally {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
signal?.removeEventListener("abort", abort);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
function valueAtPath(value, path) {
|
|
40
50
|
let current = value;
|
|
41
51
|
for (const segment of path.split(".")) {
|
|
@@ -74,12 +84,14 @@ async function executeProtectedPoll(toolName, input, {
|
|
|
74
84
|
let unchanged = 0;
|
|
75
85
|
for (; attempts < input.max_attempts; attempts += 1) {
|
|
76
86
|
if (signal?.aborted) return result({ transitions, attempts, unchanged, finalState: previous ?? null, blocker: "cancelled", retryable: false, start, end: now() });
|
|
77
|
-
|
|
87
|
+
const remaining = input.timeout_ms - (now() - start);
|
|
88
|
+
if (remaining <= 0) return result({ transitions, attempts, unchanged, finalState: previous ?? null, blocker: "timeout", retryable: true, start, end: now() });
|
|
78
89
|
let response;
|
|
79
90
|
try {
|
|
80
|
-
response = await callTool(toolName, input.arguments);
|
|
81
|
-
} catch (
|
|
82
|
-
|
|
91
|
+
response = await withinBudget((requestSignal) => callTool(toolName, input.arguments, { timeoutMs: remaining, signal: requestSignal }), remaining, signal);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const blocker = signal?.aborted ? "cancelled" : ["timeout", "cancelled"].includes(error.message) ? error.message : "read-failed";
|
|
94
|
+
return result({ transitions, attempts: attempts + 1, unchanged, finalState: previous ?? null, blocker, retryable: blocker !== "cancelled", start, end: now() });
|
|
83
95
|
}
|
|
84
96
|
const state = valueAtPath(response, input.status_path);
|
|
85
97
|
if (typeof state !== "string" && typeof state !== "number" && typeof state !== "boolean") {
|
|
@@ -105,7 +117,12 @@ async function executeProtectedPoll(toolName, input, {
|
|
|
105
117
|
end: now(),
|
|
106
118
|
});
|
|
107
119
|
}
|
|
108
|
-
if (attempts + 1 < input.max_attempts && input.interval_ms > 0)
|
|
120
|
+
if (attempts + 1 < input.max_attempts && input.interval_ms > 0) {
|
|
121
|
+
const remainingSleep = input.timeout_ms - (now() - start);
|
|
122
|
+
if (remainingSleep <= 0) return result({ transitions, attempts: attempts + 1, unchanged, finalState: previous ?? null, blocker: "timeout", retryable: true, start, end: now() });
|
|
123
|
+
try { await withinBudget(() => sleep(Math.min(input.interval_ms, remainingSleep)), remainingSleep, signal); }
|
|
124
|
+
catch { return result({ transitions, attempts: attempts + 1, unchanged, finalState: previous ?? null, blocker: signal?.aborted ? "cancelled" : "timeout", retryable: !signal?.aborted, start, end: now() }); }
|
|
125
|
+
}
|
|
109
126
|
}
|
|
110
127
|
return result({ transitions, attempts, unchanged, finalState: previous ?? null, blocker: "attempt-budget-exhausted", retryable: true, start, end: now() });
|
|
111
128
|
}
|