@delta-agentcloud/cli 0.0.0-stage → 1.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +511 -2
- package/checksums.txt +5 -0
- package/package.json +47 -4
- package/scripts/install-wizard.js +683 -0
- package/scripts/install.js +435 -0
- package/scripts/run.js +65 -0
- package/skills/delta-protein/AGENTS.md +73 -0
- package/skills/delta-protein/SKILL.md +389 -0
- package/skills/delta-protein/agents/openai.yaml +4 -0
- package/skills/delta-protein/references/bfitter.md +64 -0
- package/skills/delta-protein/references/catapro_predict.md +63 -0
- package/skills/delta-protein/references/catpred_predict.md +65 -0
- package/skills/delta-protein/references/caver_tunnel_analyze.md +63 -0
- package/skills/delta-protein/references/consensus_mutation.md +61 -0
- package/skills/delta-protein/references/consurf_predict.md +67 -0
- package/skills/delta-protein/references/crop_sequence.md +69 -0
- package/skills/delta-protein/references/dock.md +82 -0
- package/skills/delta-protein/references/dock_score.md +70 -0
- package/skills/delta-protein/references/easifa_predict_active_site.md +70 -0
- package/skills/delta-protein/references/ephod_predict_mutation.md +66 -0
- package/skills/delta-protein/references/ephod_predict_protein.md +61 -0
- package/skills/delta-protein/references/esmfold2_predict.md +67 -0
- package/skills/delta-protein/references/evcouplings_mutation.md +64 -0
- package/skills/delta-protein/references/fasta_to_sequence.md +59 -0
- package/skills/delta-protein/references/foldseek.md +61 -0
- package/skills/delta-protein/references/generate_msa.md +61 -0
- package/skills/delta-protein/references/interpro_analyze.md +63 -0
- package/skills/delta-protein/references/mpnn_design_sequence.md +71 -0
- package/skills/delta-protein/references/multievolve.md +73 -0
- package/skills/delta-protein/references/pisa_query.md +70 -0
- package/skills/delta-protein/references/plip_analyze.md +65 -0
- package/skills/delta-protein/references/pocket_prediction.md +63 -0
- package/skills/delta-protein/references/protein_design.md +65 -0
- package/skills/delta-protein/references/protein_evolution.md +77 -0
- package/skills/delta-protein/references/protein_sol_predict_mutation.md +66 -0
- package/skills/delta-protein/references/protein_sol_predict_protein.md +61 -0
- package/skills/delta-protein/references/protenix.md +66 -0
- package/skills/delta-protein/references/protrek_search.md +90 -0
- package/skills/delta-protein/references/pubchem_search.md +68 -0
- package/skills/delta-protein/references/pubmed_search.md +79 -0
- package/skills/delta-protein/references/reap_design_primer.md +69 -0
- package/skills/delta-protein/references/rfd3_design.md +65 -0
- package/skills/delta-protein/references/structure_to_sequence.md +61 -0
- package/skills/delta-protein/references/summarize_result.md +68 -0
- package/skills/delta-protein/references/surface_charge.md +84 -0
- package/skills/delta-protein/references/uniprot_batch_query.md +63 -0
- package/skills/delta-protein/references/uniprot_query.md +64 -0
- package/skills/delta-protein/references/zero_shot_mutant_candidates.md +76 -0
- package/skills/delta-protein/scripts/gen_references.py +167 -0
- package/skills/delta-protein/scripts/output_metadata.json +275 -0
- package/skills/delta-sandbox/SKILL.md +388 -0
- package/skills/delta-sandbox/agents/openai.yaml +4 -0
- package/skills/delta-sandbox/references/commands.md +58 -0
- package/skills/delta-sandbox/references/lifecycle.md +100 -0
- package/skills/delta-sandbox/references/recipes.md +138 -0
- package/skills/delta-science/AGENTS.md +47 -0
- package/skills/delta-science/SKILL.md +227 -0
- package/skills/delta-science/agents/openai.yaml +4 -0
- package/skills/delta-science/references/antbo.md +105 -0
- package/skills/delta-science/references/delta-bo.md +92 -0
- package/skills/delta-science/references/gsasii.md +118 -0
- package/skills/delta-science/references/lammps.md +68 -0
- package/skills/delta-science/references/ldm-bo.md +132 -0
- package/skills/delta-science/references/materials-design.md +130 -0
- package/skills/delta-science/references/pubchem.md +128 -0
- package/skills/delta-science/references/pymatgen.md +69 -0
- package/skills/delta-science/references/qe.md +134 -0
- package/skills/delta-science/references/rdkit.md +141 -0
- package/skills/delta-science/references/strbo.md +95 -0
- package/skills/delta-science/references/synbo.md +201 -0
- package/skills/delta-science/references/xrd.md +704 -0
- package/skills/delta-science/scripts/catalog.py +92 -0
- package/skills/delta-science/scripts/generated_catalog.py +69 -0
- package/skills/delta-science/scripts/invoke.py +346 -0
- package/skills/delta-shared/SKILL.md +113 -0
- package/skills/delta-shared/agents/openai.yaml +4 -0
- package/skills/manifest.json +20 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read the authoritative Delta Science catalog through delta-cli."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from invoke import _parse_cli_error, _resolve_cli, _runtime_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parser() -> argparse.ArgumentParser:
|
|
17
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
18
|
+
action = parser.add_mutually_exclusive_group(required=True)
|
|
19
|
+
action.add_argument("--tools", action="store_true")
|
|
20
|
+
action.add_argument("--endpoints", metavar="TOOL")
|
|
21
|
+
parser.add_argument("--cli")
|
|
22
|
+
parser.add_argument("--science-base-url")
|
|
23
|
+
parser.add_argument("--timeout", type=float, default=30.0)
|
|
24
|
+
return parser
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> int:
|
|
28
|
+
args = _parser().parse_args()
|
|
29
|
+
started = time.perf_counter()
|
|
30
|
+
kind = "tools" if args.tools else "endpoints"
|
|
31
|
+
try:
|
|
32
|
+
runtime = _runtime_config()
|
|
33
|
+
cli = _resolve_cli(args.cli, runtime)
|
|
34
|
+
env = os.environ.copy()
|
|
35
|
+
base_url = args.science_base_url or runtime.get("science_base_url")
|
|
36
|
+
if base_url:
|
|
37
|
+
env["DELTA_INFRA_SCIENCE_BASE_URL"] = str(base_url)
|
|
38
|
+
|
|
39
|
+
command = [cli, "science", "list"]
|
|
40
|
+
if args.endpoints:
|
|
41
|
+
command = [cli, "science", "endpoints", "list", args.endpoints]
|
|
42
|
+
completed = subprocess.run(
|
|
43
|
+
command,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
encoding="utf-8",
|
|
47
|
+
errors="replace",
|
|
48
|
+
env=env,
|
|
49
|
+
timeout=args.timeout,
|
|
50
|
+
check=False,
|
|
51
|
+
)
|
|
52
|
+
if completed.returncode != 0:
|
|
53
|
+
error_type, detail = _parse_cli_error(completed)
|
|
54
|
+
raise RuntimeError(f"{error_type}: {detail}")
|
|
55
|
+
root = json.loads(completed.stdout)
|
|
56
|
+
if not isinstance(root, dict) or root.get("ok") is not True:
|
|
57
|
+
raise RuntimeError(json.dumps(root, ensure_ascii=False))
|
|
58
|
+
if "data" not in root:
|
|
59
|
+
raise RuntimeError("CLI success envelope is missing data")
|
|
60
|
+
print(json.dumps({
|
|
61
|
+
"ok": True,
|
|
62
|
+
"transport": "delta-cli",
|
|
63
|
+
"catalog": kind,
|
|
64
|
+
"tool": args.endpoints,
|
|
65
|
+
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
66
|
+
"native": root["data"],
|
|
67
|
+
}, ensure_ascii=False))
|
|
68
|
+
return 0
|
|
69
|
+
except subprocess.TimeoutExpired:
|
|
70
|
+
error_type = "timeout"
|
|
71
|
+
error = f"timeout after {args.timeout}s"
|
|
72
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
73
|
+
error_type = "catalog"
|
|
74
|
+
error = str(exc)
|
|
75
|
+
except RuntimeError as exc:
|
|
76
|
+
error_type = "cli"
|
|
77
|
+
error = str(exc)
|
|
78
|
+
|
|
79
|
+
print(json.dumps({
|
|
80
|
+
"ok": False,
|
|
81
|
+
"transport": "delta-cli",
|
|
82
|
+
"catalog": kind,
|
|
83
|
+
"tool": args.endpoints,
|
|
84
|
+
"error_type": error_type,
|
|
85
|
+
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
86
|
+
"error": error[:2000],
|
|
87
|
+
}, ensure_ascii=False))
|
|
88
|
+
return 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
sys.exit(main())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Generated Science catalog aliases. Do not edit by hand."""
|
|
2
|
+
|
|
3
|
+
LEGACY_TOOL_ALIASES = {
|
|
4
|
+
"synbo": "synbo-service",
|
|
5
|
+
"antbo": "antbo-service",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
LEGACY_ENDPOINTS = {
|
|
9
|
+
"pubchem": {
|
|
10
|
+
"health": "chem_pubchem_health",
|
|
11
|
+
"compound-resolve-cids": "chem_pubchem_compound_resolve-cids",
|
|
12
|
+
"compound-properties": "chem_pubchem_compound_properties",
|
|
13
|
+
"compound-synonyms": "chem_pubchem_compound_synonyms",
|
|
14
|
+
"compound-summary": "chem_pubchem_compound_summary",
|
|
15
|
+
"compound-batch-summary": "chem_pubchem_compound_batch-summary",
|
|
16
|
+
},
|
|
17
|
+
"rdkit": {
|
|
18
|
+
"health": "chem_rdkit_health",
|
|
19
|
+
"parse": "chem_rdkit_parse",
|
|
20
|
+
"descriptors": "chem_rdkit_descriptors",
|
|
21
|
+
"batch-descriptors": "chem_rdkit_batch-descriptors",
|
|
22
|
+
"batch-parse-describe": "chem_rdkit_batch-parse-describe",
|
|
23
|
+
"render": "chem_rdkit_render",
|
|
24
|
+
"fingerprint": "chem_rdkit_fingerprint",
|
|
25
|
+
"similarity": "chem_rdkit_similarity",
|
|
26
|
+
"similarity-matrix": "chem_rdkit_similarity-matrix",
|
|
27
|
+
"substructure": "chem_rdkit_substructure",
|
|
28
|
+
},
|
|
29
|
+
"pymatgen": {
|
|
30
|
+
"health": "chem_pymatgen_health",
|
|
31
|
+
"composition-parse": "chem_pymatgen_composition_parse",
|
|
32
|
+
"structure-parse": "chem_pymatgen_structure_parse",
|
|
33
|
+
"structure-summary": "chem_pymatgen_structure_summary",
|
|
34
|
+
"structure-convert": "chem_pymatgen_structure_convert",
|
|
35
|
+
"structure-symmetry": "chem_pymatgen_structure_symmetry",
|
|
36
|
+
},
|
|
37
|
+
"gsasii": {
|
|
38
|
+
"health": "chem_gsasii_health",
|
|
39
|
+
"powder-simulate": "chem_gsasii_powder_simulate",
|
|
40
|
+
"powder-refine": "chem_gsasii_powder_refine",
|
|
41
|
+
},
|
|
42
|
+
"lammps": {
|
|
43
|
+
"health": "chem_lammps_health",
|
|
44
|
+
"lj-melt-example": "chem_lammps_examples_lj-melt",
|
|
45
|
+
"run": "chem_lammps_run",
|
|
46
|
+
},
|
|
47
|
+
"delta-bo": {
|
|
48
|
+
"commands": "chem_delta-bo_commands",
|
|
49
|
+
"generate": "chem_delta-bo_generate",
|
|
50
|
+
},
|
|
51
|
+
"ldm-bo": {
|
|
52
|
+
"health": "chem_ldm-bo_health",
|
|
53
|
+
"recommend": "chem_ldm-bo_recommend",
|
|
54
|
+
"trajectory": "chem_ldm-bo_trajectory",
|
|
55
|
+
},
|
|
56
|
+
"synbo": {
|
|
57
|
+
"health": "chem_synbo_health",
|
|
58
|
+
"initialize": "chem_synbo_initialize",
|
|
59
|
+
"optimize": "chem_synbo_optimize",
|
|
60
|
+
},
|
|
61
|
+
"antbo": {
|
|
62
|
+
"health": "biology_antbo_health",
|
|
63
|
+
"run-default-job": "biology_antbo_run_default_job",
|
|
64
|
+
"run": "biology_antbo_run",
|
|
65
|
+
"log": "biology_antbo_log",
|
|
66
|
+
"jobs": "biology_antbo_jobs",
|
|
67
|
+
"stop": "biology_antbo_stop",
|
|
68
|
+
},
|
|
69
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Invoke one Delta Science operation strictly through delta-cli."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
SKILL_DIR = Path(__file__).resolve().parents[1]
|
|
17
|
+
_CLI_ERROR_TYPES = {
|
|
18
|
+
2: "validation",
|
|
19
|
+
3: "auth",
|
|
20
|
+
4: "permission",
|
|
21
|
+
5: "not_found",
|
|
22
|
+
6: "network",
|
|
23
|
+
7: "api",
|
|
24
|
+
10: "internal",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _json_object(raw: str | None, flag: str) -> dict[str, Any] | None:
|
|
29
|
+
if raw is None:
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
value = json.loads(raw)
|
|
33
|
+
except json.JSONDecodeError as exc:
|
|
34
|
+
raise ValueError(f"{flag} is not valid JSON: {exc}") from exc
|
|
35
|
+
if not isinstance(value, dict):
|
|
36
|
+
raise ValueError(f"{flag} must decode to a JSON object")
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize_synbo_payload(
|
|
41
|
+
tool: str,
|
|
42
|
+
endpoint: str,
|
|
43
|
+
data: dict[str, Any] | None,
|
|
44
|
+
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
45
|
+
"""Normalize SynBO's discrete condition contract before any remote request."""
|
|
46
|
+
canonical_tool = tool.removesuffix("-service")
|
|
47
|
+
if canonical_tool != "synbo" or endpoint not in {"initialize", "optimize"}:
|
|
48
|
+
return data, None
|
|
49
|
+
if data is None:
|
|
50
|
+
raise ValueError(f"synbo/{endpoint} requires --data-json")
|
|
51
|
+
|
|
52
|
+
condition_dict = data.get("condition_dict")
|
|
53
|
+
if not isinstance(condition_dict, dict) or not condition_dict:
|
|
54
|
+
raise ValueError("SynBO condition_dict must be a non-empty object")
|
|
55
|
+
|
|
56
|
+
normalized_conditions: dict[str, list[str]] = {}
|
|
57
|
+
for name, raw_values in condition_dict.items():
|
|
58
|
+
if not isinstance(raw_values, list) or not raw_values:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"SynBO condition_dict.{name} must be a non-empty explicit list; "
|
|
61
|
+
"range objects are not supported"
|
|
62
|
+
)
|
|
63
|
+
values = [str(value) for value in raw_values]
|
|
64
|
+
if any(value == "" for value in values):
|
|
65
|
+
raise ValueError(f"SynBO condition_dict.{name} contains an empty value")
|
|
66
|
+
if len(set(values)) != len(values):
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"SynBO condition_dict.{name} contains duplicate values after "
|
|
69
|
+
"string normalization"
|
|
70
|
+
)
|
|
71
|
+
normalized_conditions[str(name)] = values
|
|
72
|
+
|
|
73
|
+
normalized = dict(data)
|
|
74
|
+
normalized["condition_dict"] = normalized_conditions
|
|
75
|
+
normalization = {
|
|
76
|
+
"condition_values": "strings",
|
|
77
|
+
"condition_space_size": _condition_space_size(normalized_conditions),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if endpoint == "initialize":
|
|
81
|
+
return normalized, normalization
|
|
82
|
+
|
|
83
|
+
metrics = normalized.get("opt_metrics")
|
|
84
|
+
if not isinstance(metrics, list) or not metrics:
|
|
85
|
+
raise ValueError("SynBO optimize requires a non-empty opt_metrics list")
|
|
86
|
+
metric_names = [str(metric) for metric in metrics]
|
|
87
|
+
normalized["opt_metrics"] = metric_names
|
|
88
|
+
|
|
89
|
+
previous_results = normalized.get("previous_results")
|
|
90
|
+
if not isinstance(previous_results, list) or not previous_results:
|
|
91
|
+
raise ValueError("SynBO optimize requires non-empty previous_results")
|
|
92
|
+
|
|
93
|
+
normalized_rows: list[dict[str, Any]] = []
|
|
94
|
+
for index, raw_row in enumerate(previous_results):
|
|
95
|
+
if not isinstance(raw_row, dict):
|
|
96
|
+
raise ValueError(f"SynBO previous_results[{index}] must be an object")
|
|
97
|
+
row = dict(raw_row)
|
|
98
|
+
for condition_name, allowed_values in normalized_conditions.items():
|
|
99
|
+
if condition_name not in row:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"SynBO previous_results[{index}] is missing condition "
|
|
102
|
+
f"{condition_name!r}"
|
|
103
|
+
)
|
|
104
|
+
value = str(row[condition_name])
|
|
105
|
+
if value not in allowed_values:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"SynBO previous_results[{index}].{condition_name}={value!r} "
|
|
108
|
+
"is not present in condition_dict"
|
|
109
|
+
)
|
|
110
|
+
row[condition_name] = value
|
|
111
|
+
for metric_name in metric_names:
|
|
112
|
+
metric_value = row.get(metric_name)
|
|
113
|
+
if (
|
|
114
|
+
metric_name not in row
|
|
115
|
+
or isinstance(metric_value, bool)
|
|
116
|
+
or not isinstance(metric_value, (int, float))
|
|
117
|
+
):
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"SynBO previous_results[{index}].{metric_name} must be numeric"
|
|
120
|
+
)
|
|
121
|
+
normalized_rows.append(row)
|
|
122
|
+
|
|
123
|
+
normalized["previous_results"] = normalized_rows
|
|
124
|
+
normalized.setdefault("accuracy", "tiny")
|
|
125
|
+
normalized.setdefault("device", "cpu")
|
|
126
|
+
normalized.setdefault("surrogate_model", "RF")
|
|
127
|
+
normalized.setdefault("acq_func", "UCB")
|
|
128
|
+
normalization["defaults"] = {
|
|
129
|
+
key: normalized[key]
|
|
130
|
+
for key in ("accuracy", "device", "surrogate_model", "acq_func")
|
|
131
|
+
}
|
|
132
|
+
return normalized, normalization
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _condition_space_size(condition_dict: dict[str, list[str]]) -> int:
|
|
136
|
+
size = 1
|
|
137
|
+
for values in condition_dict.values():
|
|
138
|
+
size *= len(values)
|
|
139
|
+
return size
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _runtime_config() -> dict[str, Any]:
|
|
143
|
+
path = SKILL_DIR / "runtime.local.json"
|
|
144
|
+
if not path.is_file():
|
|
145
|
+
return {}
|
|
146
|
+
try:
|
|
147
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
148
|
+
except (OSError, json.JSONDecodeError):
|
|
149
|
+
return {}
|
|
150
|
+
return value if isinstance(value, dict) else {}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _resolve_cli(explicit: str | None, runtime: dict[str, Any]) -> str:
|
|
154
|
+
candidates: list[str | None] = [
|
|
155
|
+
explicit,
|
|
156
|
+
os.environ.get("DELTA_CLI_PATH"),
|
|
157
|
+
runtime.get("cli_path"),
|
|
158
|
+
shutil.which("delta-cli"),
|
|
159
|
+
shutil.which("delta-cli.exe"),
|
|
160
|
+
]
|
|
161
|
+
package_root = SKILL_DIR.parents[1]
|
|
162
|
+
exe_name = "delta-cli.exe" if os.name == "nt" else "delta-cli"
|
|
163
|
+
candidates.append(str(package_root / "bin" / exe_name))
|
|
164
|
+
for candidate in candidates:
|
|
165
|
+
if candidate and Path(candidate).is_file():
|
|
166
|
+
return str(Path(candidate).resolve())
|
|
167
|
+
raise FileNotFoundError(
|
|
168
|
+
"delta-cli not found; install @delta-agentcloud/cli, set DELTA_CLI_PATH, "
|
|
169
|
+
"or add cli_path to delta-science/runtime.local.json"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _unwrap(stdout: str) -> tuple[Any, int]:
|
|
174
|
+
try:
|
|
175
|
+
root = json.loads(stdout)
|
|
176
|
+
except json.JSONDecodeError as exc:
|
|
177
|
+
raise RuntimeError(f"CLI stdout is not JSON: {exc}") from exc
|
|
178
|
+
if not isinstance(root, dict):
|
|
179
|
+
raise RuntimeError("CLI stdout root is not an object")
|
|
180
|
+
if root.get("ok") is not True:
|
|
181
|
+
raise RuntimeError(json.dumps(root.get("error", root), ensure_ascii=False))
|
|
182
|
+
if "data" not in root:
|
|
183
|
+
raise RuntimeError("CLI success envelope is missing data")
|
|
184
|
+
|
|
185
|
+
value: Any = root["data"]
|
|
186
|
+
depth = 0
|
|
187
|
+
while isinstance(value, dict):
|
|
188
|
+
if "status_code" in value and "data" in value:
|
|
189
|
+
status_code = value.get("status_code")
|
|
190
|
+
if not isinstance(status_code, int) or not 200 <= status_code < 300:
|
|
191
|
+
raise RuntimeError(json.dumps(value, ensure_ascii=False))
|
|
192
|
+
value = value["data"]
|
|
193
|
+
elif (
|
|
194
|
+
"code" in value
|
|
195
|
+
and "data" in value
|
|
196
|
+
and set(value).intersection({"message", "msg"})
|
|
197
|
+
):
|
|
198
|
+
code = value.get("code")
|
|
199
|
+
if code not in (0, "0", None):
|
|
200
|
+
raise RuntimeError(json.dumps(value, ensure_ascii=False))
|
|
201
|
+
value = value["data"]
|
|
202
|
+
else:
|
|
203
|
+
break
|
|
204
|
+
depth += 1
|
|
205
|
+
if depth > 4:
|
|
206
|
+
raise RuntimeError("too many nested service envelopes")
|
|
207
|
+
if isinstance(value, dict) and value.get("valid") is False:
|
|
208
|
+
raise RuntimeError(json.dumps({
|
|
209
|
+
"valid": False,
|
|
210
|
+
"error": value.get("error"),
|
|
211
|
+
"warnings": value.get("warnings"),
|
|
212
|
+
}, ensure_ascii=False))
|
|
213
|
+
return value, depth
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _validate_native(tool: str, endpoint: str, native: Any) -> None:
|
|
217
|
+
canonical_tool = tool.removesuffix("-service")
|
|
218
|
+
if canonical_tool == "synbo" and endpoint == "optimize":
|
|
219
|
+
if not isinstance(native, dict):
|
|
220
|
+
raise RuntimeError("SynBO optimize returned a non-object result")
|
|
221
|
+
recommendations = native.get("recommendations")
|
|
222
|
+
if not isinstance(recommendations, list) or not recommendations:
|
|
223
|
+
raise RuntimeError("SynBO optimize returned no recommendations")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _parse_cli_error(completed: subprocess.CompletedProcess[str]) -> tuple[str, str]:
|
|
227
|
+
detail = completed.stderr.strip() or completed.stdout.strip()
|
|
228
|
+
error_type = _CLI_ERROR_TYPES.get(completed.returncode, "cli")
|
|
229
|
+
try:
|
|
230
|
+
envelope = json.loads(detail)
|
|
231
|
+
error = envelope.get("error", {}) if isinstance(envelope, dict) else {}
|
|
232
|
+
if isinstance(error, dict):
|
|
233
|
+
error_type = str(error.get("type") or error_type)
|
|
234
|
+
message = error.get("message")
|
|
235
|
+
if message:
|
|
236
|
+
detail = str(message)
|
|
237
|
+
except (AttributeError, json.JSONDecodeError):
|
|
238
|
+
pass
|
|
239
|
+
return error_type, detail
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _parser() -> argparse.ArgumentParser:
|
|
243
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
244
|
+
parser.add_argument("--tool", required=True)
|
|
245
|
+
parser.add_argument("--endpoint", required=True)
|
|
246
|
+
body = parser.add_mutually_exclusive_group()
|
|
247
|
+
body.add_argument("--data-json")
|
|
248
|
+
body.add_argument("--params-json")
|
|
249
|
+
parser.add_argument("--cli")
|
|
250
|
+
parser.add_argument("--science-base-url")
|
|
251
|
+
parser.add_argument("--timeout", type=float, default=120.0)
|
|
252
|
+
return parser
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def main() -> int:
|
|
256
|
+
args = _parser().parse_args()
|
|
257
|
+
started = time.perf_counter()
|
|
258
|
+
stage = "arguments"
|
|
259
|
+
cli_exit_code: int | None = None
|
|
260
|
+
error_type = "unknown"
|
|
261
|
+
normalization: dict[str, Any] | None = None
|
|
262
|
+
try:
|
|
263
|
+
data = _json_object(args.data_json, "--data-json")
|
|
264
|
+
params = _json_object(args.params_json, "--params-json")
|
|
265
|
+
data, normalization = _normalize_synbo_payload(
|
|
266
|
+
args.tool, args.endpoint, data
|
|
267
|
+
)
|
|
268
|
+
runtime = _runtime_config()
|
|
269
|
+
stage = "cli-resolution"
|
|
270
|
+
cli = _resolve_cli(args.cli, runtime)
|
|
271
|
+
|
|
272
|
+
env = os.environ.copy()
|
|
273
|
+
base_url = args.science_base_url or runtime.get("science_base_url")
|
|
274
|
+
if base_url:
|
|
275
|
+
env["DELTA_INFRA_SCIENCE_BASE_URL"] = str(base_url)
|
|
276
|
+
|
|
277
|
+
command = [
|
|
278
|
+
cli, "science", "invoke",
|
|
279
|
+
"--tool", args.tool,
|
|
280
|
+
"--endpoint", args.endpoint,
|
|
281
|
+
]
|
|
282
|
+
if data is not None:
|
|
283
|
+
command.extend(["--data", json.dumps(data, ensure_ascii=False, separators=(",", ":"))])
|
|
284
|
+
if params is not None:
|
|
285
|
+
command.extend(["--params", json.dumps(params, ensure_ascii=False, separators=(",", ":"))])
|
|
286
|
+
|
|
287
|
+
stage = "delta-cli"
|
|
288
|
+
completed = subprocess.run(
|
|
289
|
+
command,
|
|
290
|
+
capture_output=True,
|
|
291
|
+
text=True,
|
|
292
|
+
encoding="utf-8",
|
|
293
|
+
errors="replace",
|
|
294
|
+
env=env,
|
|
295
|
+
timeout=args.timeout,
|
|
296
|
+
check=False,
|
|
297
|
+
)
|
|
298
|
+
if completed.returncode != 0:
|
|
299
|
+
cli_exit_code = completed.returncode
|
|
300
|
+
error_type, detail = _parse_cli_error(completed)
|
|
301
|
+
raise RuntimeError(f"exit_code={completed.returncode}: {detail}")
|
|
302
|
+
stage = "response-validation"
|
|
303
|
+
native, depth = _unwrap(completed.stdout)
|
|
304
|
+
_validate_native(args.tool, args.endpoint, native)
|
|
305
|
+
result = {
|
|
306
|
+
"ok": True,
|
|
307
|
+
"transport": "delta-cli",
|
|
308
|
+
"tool": args.tool,
|
|
309
|
+
"endpoint": args.endpoint,
|
|
310
|
+
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
311
|
+
"envelope_depth": depth,
|
|
312
|
+
"native": native,
|
|
313
|
+
}
|
|
314
|
+
if normalization is not None:
|
|
315
|
+
result["request_normalization"] = normalization
|
|
316
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
317
|
+
return 0
|
|
318
|
+
except subprocess.TimeoutExpired:
|
|
319
|
+
error_type = "timeout"
|
|
320
|
+
error = f"timeout after {args.timeout}s"
|
|
321
|
+
except ValueError as exc:
|
|
322
|
+
error_type = "validation"
|
|
323
|
+
error = str(exc)
|
|
324
|
+
except OSError as exc:
|
|
325
|
+
error_type = "cli_resolution"
|
|
326
|
+
error = str(exc)
|
|
327
|
+
except RuntimeError as exc:
|
|
328
|
+
if error_type == "unknown":
|
|
329
|
+
error_type = "response_validation"
|
|
330
|
+
error = str(exc)
|
|
331
|
+
print(json.dumps({
|
|
332
|
+
"ok": False,
|
|
333
|
+
"transport": "delta-cli",
|
|
334
|
+
"tool": args.tool,
|
|
335
|
+
"endpoint": args.endpoint,
|
|
336
|
+
"stage": stage,
|
|
337
|
+
"error_type": error_type,
|
|
338
|
+
"cli_exit_code": cli_exit_code,
|
|
339
|
+
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
340
|
+
"error": error[:2000],
|
|
341
|
+
}, ensure_ascii=False))
|
|
342
|
+
return 1
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
if __name__ == "__main__":
|
|
346
|
+
sys.exit(main())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delta-shared
|
|
3
|
+
description: "delta-cli shared global rules: configuration and authentication checks, security boundaries, error handling, and sandbox destruction."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# delta-cli Shared Rules
|
|
7
|
+
|
|
8
|
+
General guidance for AI agents using `delta-cli`. Core principles: **check, do not guess or expose credentials**.
|
|
9
|
+
|
|
10
|
+
## Before any operation
|
|
11
|
+
|
|
12
|
+
1. Verify the server address:
|
|
13
|
+
```bash
|
|
14
|
+
delta-cli config show
|
|
15
|
+
```
|
|
16
|
+
2. Inspect the local authentication configuration:
|
|
17
|
+
```bash
|
|
18
|
+
delta-cli auth status
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- If `auth status` reports `method: token` or `method: apikey`, a credential is configured locally; **run the requested command directly without logging in or initialising configuration again**.
|
|
22
|
+
- `auth status` reads local configuration and environment overrides; it does not validate the credential with the remote server. If it reports `method: none`, follow “Complete configuration” below. If a requested command returns `error.type: auth` or `permission`, follow the matching recovery action below.
|
|
23
|
+
|
|
24
|
+
## Complete configuration
|
|
25
|
+
|
|
26
|
+
If the server address or credentials are missing, **ask the user once**, then initialise with the flag that matches the credential type:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
delta-cli config init --base-url <server-url> --token <token>
|
|
30
|
+
# For an API key:
|
|
31
|
+
delta-cli config init --base-url <server-url> --apikey <api-key>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Do not use interactive `config init` (without flags), as it may hang when stdin is unavailable.
|
|
35
|
+
|
|
36
|
+
### Priority of information sources
|
|
37
|
+
|
|
38
|
+
1. Environment variables: `DELTA_INFRA_BASE_URL`, `DELTA_INFRA_TOKEN`, `DELTA_INFRA_API_KEY`
|
|
39
|
+
2. `base_url`, `token`, and `apikey` in `~/.delta-infra/config.json` (environment values override file values)
|
|
40
|
+
3. Default value: only `base_url` has an internal default
|
|
41
|
+
4. If none of the above are available, **you must ask the user**; do not make anything up
|
|
42
|
+
|
|
43
|
+
### Runtime credentials
|
|
44
|
+
|
|
45
|
+
The CLI sends the effective credential in the `Authorization: Bearer <credential>` header; an API key takes precedence over a token when both are configured. It adds `X-User-Uid` when it can parse a `uid` from the credential as an unverified JWT.
|
|
46
|
+
|
|
47
|
+
## Querying user information and remaining credits
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
delta-cli usage
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This returns the current user's information and remaining credits (through the gateway `/infra/api/users/me`; the CLI automatically attaches the configured credentials). Keep only these three fields in the output:
|
|
54
|
+
|
|
55
|
+
- `data.display_name`: display name (fall back to `user_id` if missing)
|
|
56
|
+
- `data.user_id`: user ID
|
|
57
|
+
- `data.resources.credit`: remaining balance in credits
|
|
58
|
+
|
|
59
|
+
`username`/`id`/`email`/`role`/`quota`/`group`/`tier`/`status`/`created_at`/`is_admin`, `stats` and its `total_consumed_credits` (in the database's original quota units) are internal fields and must not be exposed externally.
|
|
60
|
+
|
|
61
|
+
## Security rules
|
|
62
|
+
|
|
63
|
+
- **Never output credentials**: token and API key values must not appear in logs, messages or the terminal.
|
|
64
|
+
- **Never construct URLs or make HTTP requests yourself**: route all requests through `delta-cli`.
|
|
65
|
+
- **Do not install the CLI proactively**: if the user says it is not installed, direct them to the README or `install.sh`; do not choose an installation method yourself.
|
|
66
|
+
- **Confirm intent before write or delete operations**: tell the user what commands such as `sandbox kill`, `sandbox finish` and `sandbox write` will affect.
|
|
67
|
+
- `~/.delta-infra/config.json` must have `0600` permissions; do not loosen them manually.
|
|
68
|
+
- **The sandbox must be destroyed**: after calling `sandbox create`, call `sandbox kill` before the current task ends. The server does not clean up automatically; failure to destroy a sandbox leaves GPU resources occupied indefinitely. Do not skip this, even if an error occurs.
|
|
69
|
+
|
|
70
|
+
## JSON output conventions
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{"ok":true,"data":{...}}
|
|
74
|
+
{"ok":false,"error":{"type":"auth","message":"...","hint":"..."}}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
AI agent parsing rules:
|
|
78
|
+
- Always check `ok`; do not rely on the exit code alone.
|
|
79
|
+
- `error.type` is the stable field for branching on: `auth`, `permission`, `not_found`, `validation`, `network`, `api`, `command_failed`, `internal`.
|
|
80
|
+
- `error.hint` may be shown to the user.
|
|
81
|
+
|
|
82
|
+
## Exit code conventions
|
|
83
|
+
|
|
84
|
+
| Type | Exit Code | Scenario |
|
|
85
|
+
|------|-----------|----------|
|
|
86
|
+
| validation | 2 | Invalid flag or argument |
|
|
87
|
+
| auth | 3 | Not authenticated or authentication failed |
|
|
88
|
+
| permission | 4 | Insufficient permissions |
|
|
89
|
+
| not_found | 5 | Sandbox or file does not exist |
|
|
90
|
+
| network | 6 | DNS, timeout or connection failure |
|
|
91
|
+
| api | 7 | Server returned a non-2xx response |
|
|
92
|
+
| command_failed | 8 | Command failed inside the sandbox (non-zero `exit_code` or an error); on the SSE path, an error envelope is appended after the `complete` frame, with the stderr tail in `message` for quick reference |
|
|
93
|
+
| internal | 10 | Internal client error |
|
|
94
|
+
|
|
95
|
+
## Upgrade check
|
|
96
|
+
|
|
97
|
+
After completing the user's request, if the JSON contains `_notice.update`, you may recommend:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
delta-cli upgrade
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Common recovery actions
|
|
104
|
+
|
|
105
|
+
| Error / symptom | Recovery action |
|
|
106
|
+
|-------------|----------|
|
|
107
|
+
| `error.type: auth` / `method: none` | Check the server address and local credential configuration. If no credential is configured or the server rejects it, ask the user once for a credential; use `--apikey` for an API key or `--token` for a bearer token, or set the matching `DELTA_INFRA_API_KEY` / `DELTA_INFRA_TOKEN` environment variable. |
|
|
108
|
+
| `error.type: permission` | Check whether the current credential has sufficient permissions; if needed, ask for a credential with the required permissions. |
|
|
109
|
+
| `error.type: not_found` | Check `sandbox_id` / path |
|
|
110
|
+
| `error.type: network` | Check `base_url` and network connectivity |
|
|
111
|
+
| `error.type: validation` | Check required flags and argument formats |
|
|
112
|
+
| `error.type: command_failed` | Command failed inside the sandbox: the envelope's `message` already includes the stderr tail to help locate the issue quickly; the full `stderr` / `result_summary` are in the `complete` frame. Retry or correct the command |
|
|
113
|
+
| Configuration file parsing failed | Check that `~/.delta-infra/config.json` contains valid JSON |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skills": [
|
|
3
|
+
"delta-sandbox",
|
|
4
|
+
"delta-shared",
|
|
5
|
+
"delta-science",
|
|
6
|
+
"delta-protein"
|
|
7
|
+
],
|
|
8
|
+
"retired_skills": [
|
|
9
|
+
"pubchem",
|
|
10
|
+
"rdkit",
|
|
11
|
+
"pymatgen",
|
|
12
|
+
"gsasii",
|
|
13
|
+
"lammps",
|
|
14
|
+
"delta-bo",
|
|
15
|
+
"ldm-bo",
|
|
16
|
+
"synbo-service",
|
|
17
|
+
"antbo-service",
|
|
18
|
+
"antbo-ldm-guard"
|
|
19
|
+
]
|
|
20
|
+
}
|