rainskills 0.1.45 → 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.
Files changed (83) hide show
  1. package/SKILL.md +1 -1
  2. package/bin/bridge-errors.js +26 -0
  3. package/bin/component-recipe.js +66 -0
  4. package/bin/confirmed-batch.js +120 -0
  5. package/bin/protected-poll.js +33 -16
  6. package/bin/rainskills-tools.js +133 -58
  7. package/bin/rainskills.js +5 -0
  8. package/bin/read-snapshot.js +3 -11
  9. package/bin/runtime-integrity.js +27 -0
  10. package/bin/runtime-smoke.js +63 -0
  11. package/bin/tool-policy.js +21 -0
  12. package/contracts/component-recipe.schema.yaml +30 -0
  13. package/contracts/confirmed-batch.schema.yaml +42 -0
  14. package/contracts/runtime/cli-base.md +3 -1
  15. package/contracts/runtime/overlay.schema.json +109 -10
  16. package/contracts/runtime/skills/rainbond-app-assistant.yaml +2 -1
  17. package/contracts/runtime/skills/rainbond-fullstack-bootstrap.yaml +2 -1
  18. package/contracts/runtime/skills/rainbond-fullstack-troubleshooter.yaml +2 -1
  19. package/marketplace/rainskills/.claude-plugin/plugin.json +1 -1
  20. package/marketplace/rainskills/.codex-plugin/plugin.json +1 -1
  21. package/marketplace/rainskills/skills/rainskills/SKILL.md +2 -2
  22. package/package.json +2 -2
  23. package/rainbond-ai-assistant/references/generated/runtime-gate.md +5 -3
  24. package/rainbond-app-assistant/SKILL.md +9 -8
  25. package/rainbond-app-assistant/references/customer-result.md +43 -0
  26. package/rainbond-app-assistant/references/generated/runtime-gate.md +53 -5
  27. package/rainbond-app-assistant/references/generated/runtime-routing.md +1 -1
  28. package/rainbond-app-assistant/references/mainline.md +27 -0
  29. package/rainbond-app-assistant/references/objects/catalog-and-source.md +361 -0
  30. package/rainbond-app-assistant/references/objects/runtime-delivery-and-lifecycle.md +424 -0
  31. package/rainbond-app-assistant/references/objects/skill-boundaries-and-gaps.md +243 -0
  32. package/rainbond-app-assistant/references/objects/source-resolution-and-projection.md +515 -0
  33. package/rainbond-app-assistant/references/output-contract.md +2 -431
  34. package/rainbond-app-assistant/references/phases/bootstrap.md +17 -0
  35. package/rainbond-app-assistant/references/phases/customer-rules.md +13 -0
  36. package/rainbond-app-assistant/references/phases/deliver.md +13 -0
  37. package/rainbond-app-assistant/references/phases/identity.md +27 -0
  38. package/rainbond-app-assistant/references/phases/initialize.md +11 -0
  39. package/rainbond-app-assistant/references/phases/input-literals.md +21 -0
  40. package/rainbond-app-assistant/references/phases/local-inputs.md +23 -0
  41. package/rainbond-app-assistant/references/phases/mainline-details.md +89 -0
  42. package/rainbond-app-assistant/references/phases/narration.md +33 -0
  43. package/rainbond-app-assistant/references/phases/orchestration-details.md +178 -0
  44. package/rainbond-app-assistant/references/phases/promote.md +3 -0
  45. package/rainbond-app-assistant/references/phases/routing-details.md +128 -0
  46. package/rainbond-app-assistant/references/phases/source-build.md +27 -0
  47. package/rainbond-app-assistant/references/phases/source-create.md +13 -0
  48. package/rainbond-app-assistant/references/phases/source-failure.md +29 -0
  49. package/rainbond-app-assistant/references/phases/source-policy.md +18 -0
  50. package/rainbond-app-assistant/references/phases/source-proxy.md +22 -0
  51. package/rainbond-app-assistant/references/phases/topology-selection.md +42 -0
  52. package/rainbond-app-assistant/references/phases/troubleshoot.md +23 -0
  53. package/rainbond-app-assistant/references/product-object-model.md +6 -1536
  54. package/rainbond-app-assistant/references/results/structured-result-details-2.md +191 -0
  55. package/rainbond-app-assistant/references/results/structured-result-details.md +205 -0
  56. package/rainbond-app-assistant/references/rule-ownership.json +265 -0
  57. package/rainbond-app-assistant/references/structured-result.md +7 -0
  58. package/rainbond-app-assistant/references/workflow-rules.md +22 -709
  59. package/rainbond-app-assistant/schemas/generated/component-recipe.schema.yaml +32 -0
  60. package/rainbond-app-assistant/schemas/generated/confirmed-batch.schema.yaml +44 -0
  61. package/rainbond-app-assistant/scripts/validate_progressive_loading.py +8 -4
  62. package/rainbond-app-version-assistant/references/generated/runtime-gate.md +5 -3
  63. package/rainbond-delivery-verifier/references/generated/runtime-gate.md +5 -3
  64. package/rainbond-env-sync/references/generated/runtime-gate.md +5 -3
  65. package/rainbond-fullstack-bootstrap/SKILL.md +6 -2
  66. package/rainbond-fullstack-bootstrap/modules/50-workflow.md +1 -1
  67. package/rainbond-fullstack-bootstrap/modules/52-cli-batching.md +29 -0
  68. package/rainbond-fullstack-bootstrap/references/generated/runtime-gate.md +53 -5
  69. package/rainbond-fullstack-bootstrap/references/generated/runtime-routing.md +1 -1
  70. package/rainbond-fullstack-bootstrap/schemas/generated/component-recipe.schema.yaml +32 -0
  71. package/rainbond-fullstack-bootstrap/schemas/generated/confirmed-batch.schema.yaml +44 -0
  72. package/rainbond-fullstack-troubleshooter/references/generated/runtime-gate.md +52 -4
  73. package/rainbond-fullstack-troubleshooter/references/generated/runtime-routing.md +1 -1
  74. package/rainbond-fullstack-troubleshooter/schemas/generated/component-recipe.schema.yaml +32 -0
  75. package/rainbond-fullstack-troubleshooter/schemas/generated/confirmed-batch.schema.yaml +44 -0
  76. package/rainbond-opensource-app-deploy/references/generated/runtime-gate.md +5 -3
  77. package/rainbond-platform-installer/SKILL.md +1 -1
  78. package/rainbond-platform-installer/scripts/installed-version.js +1 -1
  79. package/rainbond-platform-plugin-manager/references/generated/runtime-gate.md +5 -3
  80. package/rainbond-platform-query/references/generated/runtime-gate.md +5 -3
  81. package/rainbond-project-init/references/generated/runtime-gate.md +5 -3
  82. package/rainbond-template-installer/references/generated/runtime-gate.md +5 -3
  83. package/scripts/install-local-cli.mjs +14 -1
package/SKILL.md CHANGED
@@ -56,7 +56,7 @@ Hermes Agent 中必须使用 `terminal` 以 `background=true` 启动授权命令
56
56
 
57
57
  DeepSeek Harness 和 WorkBuddy 也必须保持原命令附着直到退出;如果宿主把长命令转成持久终端或后台任务,只能轮询该原始命令句柄,不得另起 `runtime status` 推测授权结果。WorkBuddy 安装后若当前任务的 Skill 清单尚未刷新,在下一条业务动作前先刷新 Skill 列表或新建任务,不得退回内置 Sites 代替 Rainbond 部署。
58
58
 
59
- Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.45`):
59
+ Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.46`):
60
60
 
61
61
  - 状态:执行 `runtime status --json`。
62
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 };
@@ -1,21 +1,12 @@
1
1
  "use strict";
2
2
 
3
- const READ_PREFIXES = [
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 (!readOnly(toolName)) throw new Error("poll accepts only a read-only tool");
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
- if (now() - start > input.timeout_ms) return result({ transitions, attempts, unchanged, finalState: previous ?? null, blocker: "timeout", retryable: true, start, end: now() });
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 (_error) {
82
- return result({ transitions, attempts: attempts + 1, unchanged, finalState: previous ?? null, blocker: "read-failed", retryable: true, start, end: now() });
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) await sleep(input.interval_ms);
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
  }