aiscript 1.0.0__tar.gz → 1.2.0__tar.gz
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.
- {aiscript-1.0.0 → aiscript-1.2.0}/PKG-INFO +75 -10
- {aiscript-1.0.0 → aiscript-1.2.0}/README.md +73 -8
- {aiscript-1.0.0 → aiscript-1.2.0}/pyproject.toml +8 -2
- {aiscript-1.0.0 → aiscript-1.2.0}/pyproject.toml.orig +4 -2
- {aiscript-1.0.0 → aiscript-1.2.0}/src/aiscript/__init__.py +1 -1
- {aiscript-1.0.0 → aiscript-1.2.0}/src/aiscript/builder.py +30 -8
- {aiscript-1.0.0 → aiscript-1.2.0}/src/aiscript/cli.py +6 -3
- {aiscript-1.0.0 → aiscript-1.2.0}/src/aiscript/config.py +49 -3
- aiscript-1.2.0/src/aiscript/decisions.py +48 -0
- aiscript-1.2.0/src/aiscript/env.py +120 -0
- {aiscript-1.0.0 → aiscript-1.2.0}/src/aiscript/init_cmd.py +9 -1
- {aiscript-1.0.0 → aiscript-1.2.0}/src/aiscript/listing.py +28 -11
- {aiscript-1.0.0 → aiscript-1.2.0}/src/aiscript/llm.py +189 -36
- aiscript-1.2.0/src/aiscript/paths.py +131 -0
- aiscript-1.2.0/src/aiscript/pysetup.py +191 -0
- aiscript-1.2.0/src/aiscript/reviewer.py +1077 -0
- {aiscript-1.0.0 → aiscript-1.2.0}/src/aiscript/runner.py +11 -1
- aiscript-1.0.0/src/aiscript/paths.py +0 -77
- aiscript-1.0.0/src/aiscript/reviewer.py +0 -491
- {aiscript-1.0.0 → aiscript-1.2.0}/LICENSE +0 -0
- {aiscript-1.0.0 → aiscript-1.2.0}/src/aiscript/__main__.py +0 -0
- {aiscript-1.0.0 → aiscript-1.2.0}/src/aiscript/errors.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiscript
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: JIT-compile plain-English intents into cached, reviewed shell scripts via an LLM.
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: JIT-compile plain-English intents into cached, reviewed shell or Python scripts via an LLM.
|
|
5
5
|
Author: Daniel Illescas Romero
|
|
6
6
|
Author-email: Daniel Illescas Romero <illescas.daniel@protonmail.com>
|
|
7
7
|
License-Expression: MIT
|
|
@@ -17,11 +17,14 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
[](https://pypi.org/project/aiscript/)
|
|
18
18
|
[](https://pypi.org/project/aiscript/)
|
|
19
19
|
|
|
20
|
-
Describe what you want in plain English and `aiscript` asks an LLM to write a
|
|
21
|
-
|
|
20
|
+
Describe what you want in plain English and `aiscript` asks an LLM to write a script, caches
|
|
21
|
+
it, and runs it. The model picks the language: your native shell (Bash on Linux/macOS, PowerShell
|
|
22
|
+
on Windows) or Python when that fits better. Python scripts that need third-party packages get
|
|
23
|
+
their own virtual environment. The next run is instant:
|
|
22
24
|
no network, no config. Before anything risky is saved or run, every generated script is checked
|
|
23
25
|
by a built-in static scan and, optionally, by a decision model: [Jev](https://docs.typesafe.ai)
|
|
24
|
-
by TypeSafe AI (hosted)
|
|
26
|
+
by TypeSafe AI (hosted), [OpenAI Decisions](https://developers.openai.com/api/docs/guides/decisions),
|
|
27
|
+
or a local model served by [Ollaya](https://ollaya.dev). Risky scripts
|
|
25
28
|
are shown to you and need an explicit `[y/N]` approval.
|
|
26
29
|
|
|
27
30
|
## Install
|
|
@@ -34,7 +37,19 @@ pipx install aiscript
|
|
|
34
37
|
pip install aiscript
|
|
35
38
|
```
|
|
36
39
|
|
|
37
|
-
Requires Python 3.10+.
|
|
40
|
+
Requires Python 3.10+.
|
|
41
|
+
|
|
42
|
+
To upgrade to the latest version, use the same tool you installed with:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
uv tool upgrade aiscript
|
|
46
|
+
# or
|
|
47
|
+
pipx upgrade aiscript
|
|
48
|
+
# or
|
|
49
|
+
pip install --upgrade aiscript
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Your config, prompts and cached scripts in `~/.config/aiscript/` are kept. To work on aiscript itself, see [docs/development.md](docs/development.md).
|
|
38
53
|
|
|
39
54
|
## Quickstart
|
|
40
55
|
|
|
@@ -72,7 +87,7 @@ Every generated script is reviewed **before it is saved**:
|
|
|
72
87
|
- A **static regex scan** (always on) flags privilege escalation, recursive or system-wide file
|
|
73
88
|
changes, networking/firewall changes and remote code execution (`curl | sh`, `iex`).
|
|
74
89
|
- A **decision model** (`reviewer_provider`) gives a second opinion: **Jev** (hosted; key from
|
|
75
|
-
<https://console.typesafe.ai/keys>) or **Ollaya** (local and keyless; set
|
|
90
|
+
<https://console.typesafe.ai/keys>), **OpenAI Decisions** (see below), or **Ollaya** (local and keyless; set
|
|
76
91
|
`reviewer_provider: ollaya`, default model `jevk5:4b` on `localhost:11435`; see
|
|
77
92
|
<https://ollaya.dev>). Comment `reviewer_provider` out to use only the static scan.
|
|
78
93
|
|
|
@@ -87,7 +102,8 @@ a best-effort safety net, not a sandbox: read what you approve. Details: [docs/s
|
|
|
87
102
|
|
|
88
103
|
```yaml
|
|
89
104
|
# aiscript config. Contains API keys: keep it private (chmod 600) and out of git.
|
|
90
|
-
# Keys can also come from the environment: OPENAI_API_KEY,
|
|
105
|
+
# Keys can also come from the environment: OPENAI_API_KEY, OPENAI_DECISIONS_API_KEY,
|
|
106
|
+
# TYPESAFE_API_KEY, <PROVIDER>_API_KEY.
|
|
91
107
|
|
|
92
108
|
# Provider that writes the scripts (override with $AISCRIPT_PROVIDER).
|
|
93
109
|
default_provider: openai
|
|
@@ -103,6 +119,13 @@ providers:
|
|
|
103
119
|
model: "gpt-6-luna"
|
|
104
120
|
# reasoning_effort: low # none | low | medium | high | xhigh | max (model-dependent)
|
|
105
121
|
|
|
122
|
+
# OpenAI Decisions API reviewer provider.
|
|
123
|
+
# openai_decisions:
|
|
124
|
+
# kind: openai
|
|
125
|
+
# api: decisions
|
|
126
|
+
# api_key: "..."
|
|
127
|
+
# model: "gpt-6-luna"
|
|
128
|
+
|
|
106
129
|
# Jev (TypeSafe AI) safety reviewer. API key: https://console.typesafe.ai/keys
|
|
107
130
|
jev:
|
|
108
131
|
api_key: "..."
|
|
@@ -121,6 +144,22 @@ Placeholder keys (`"..."`) are ignored, so if you leave them, the environment va
|
|
|
121
144
|
All options (provider kinds, `api`, `temperature`, key and model resolution, every environment
|
|
122
145
|
variable, the legacy `secrets.yaml`): [docs/configuration.md](docs/configuration.md).
|
|
123
146
|
|
|
147
|
+
### OpenAI Decisions reviewer
|
|
148
|
+
|
|
149
|
+
To use OpenAI Decisions instead of Jev, select `reviewer_provider: openai_decisions` and
|
|
150
|
+
uncomment the corresponding provider block above. Supply its own `api_key`, or export
|
|
151
|
+
`OPENAI_DECISIONS_API_KEY`; this named reviewer does not fall back to `OPENAI_API_KEY`.
|
|
152
|
+
The default model is `gpt-6-luna`, and `model` remains configurable for future models.
|
|
153
|
+
|
|
154
|
+
Decisions uses the official OpenAI endpoint only. One request checks the same four hazards,
|
|
155
|
+
flagging probabilities **>= 0.5**. Requests use a 30-second transport timeout with zero retries.
|
|
156
|
+
Errors, refusals and malformed answers require the existing risk confirmation; they are refused
|
|
157
|
+
in noninteractive builds. Static findings and dependency warnings always remain effective.
|
|
158
|
+
|
|
159
|
+
Existing reviewers and cached scripts keep working. To roll back, restore your previous
|
|
160
|
+
`reviewer_provider` before downgrading aiscript. See [configuration](docs/configuration.md#openai-decisions)
|
|
161
|
+
and [safety](docs/safety.md#openai-decisions) for the full contract.
|
|
162
|
+
|
|
124
163
|
## Usage
|
|
125
164
|
|
|
126
165
|
| Command | Action |
|
|
@@ -135,17 +174,43 @@ variable, the legacy `secrets.yaml`): [docs/configuration.md](docs/configuration
|
|
|
135
174
|
|
|
136
175
|
Command names must match `[A-Za-z0-9][A-Za-z0-9_-]*`.
|
|
137
176
|
|
|
177
|
+
## Shell or Python
|
|
178
|
+
|
|
179
|
+
The model is told your OS, architecture, native shell, the Python version it will run under and
|
|
180
|
+
whether [uv](https://docs.astral.sh/uv/) is installed, and chooses the language: the shell for thin
|
|
181
|
+
wrappers around CLI tools and pipelines, Python for parsing JSON/CSV/HTML, data processing, HTTP
|
|
182
|
+
APIs and anything that would be fragile in shell. Say "in python" or "as a shell script" in your
|
|
183
|
+
description to ask for one; the model follows it unless it is clearly a poor fit (and then notes
|
|
184
|
+
why in the script header). `aiscript --list` shows each command's language.
|
|
185
|
+
|
|
186
|
+
- **Standard-library Python** is saved as `functions/NAME.py` and run with the system `python3`
|
|
187
|
+
(`py -3` on Windows), or with aiscript's own interpreter if there is none.
|
|
188
|
+
- **Python with dependencies** declares them in an inline
|
|
189
|
+
[PEP 723](https://peps.python.org/pep-0723/) block. After you approve (any dependency requires a
|
|
190
|
+
`[y/N]` approval that lists the packages), aiscript creates `functions/NAME/` with `main.py` and
|
|
191
|
+
a private `.venv`: with uv (`pyproject.toml` + `uv.lock`, `uv sync`) or otherwise with
|
|
192
|
+
`python -m venv` + `pip install -r requirements.txt`. The script always runs with that venv's
|
|
193
|
+
interpreter, so later runs are offline. A rebuild with the same dependencies reuses the venv; a
|
|
194
|
+
failed install keeps the previous version. Without uv, Debian/Ubuntu need `python3-venv`.
|
|
195
|
+
|
|
138
196
|
## How it works
|
|
139
197
|
|
|
140
198
|
```
|
|
141
199
|
~/.config/aiscript/ # or $XDG_CONFIG_HOME/aiscript, or $AISCRIPT_HOME
|
|
142
200
|
├── config.yaml # providers and keys
|
|
143
201
|
├── prompts/<name>.txt # your intent for each command
|
|
144
|
-
└── functions
|
|
202
|
+
└── functions/
|
|
203
|
+
├── <name>.sh # generated, reviewed scripts (.ps1 on Windows)
|
|
204
|
+
├── <name>.py # standard-library Python
|
|
205
|
+
└── <name>/ # Python with dependencies
|
|
206
|
+
├── main.py
|
|
207
|
+
├── .venv/
|
|
208
|
+
└── pyproject.toml + uv.lock (or requirements.txt)
|
|
145
209
|
```
|
|
146
210
|
|
|
147
211
|
The first run of `NAME` saves your description to `prompts/NAME.txt`, asks the LLM for a script,
|
|
148
|
-
reviews it, and stores it in `functions
|
|
212
|
+
reviews it, and stores it in `functions/` (only one build per command: switching language
|
|
213
|
+
replaces the old one). Later runs just execute the cached script. If the
|
|
149
214
|
prompt is newer than the script (after `--edit` or a manual change), the script is rebuilt on the
|
|
150
215
|
next run. A `--rebuild` that fails or is rejected keeps the previous script.
|
|
151
216
|
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
[](https://pypi.org/project/aiscript/)
|
|
4
4
|
[](https://pypi.org/project/aiscript/)
|
|
5
5
|
|
|
6
|
-
Describe what you want in plain English and `aiscript` asks an LLM to write a
|
|
7
|
-
|
|
6
|
+
Describe what you want in plain English and `aiscript` asks an LLM to write a script, caches
|
|
7
|
+
it, and runs it. The model picks the language: your native shell (Bash on Linux/macOS, PowerShell
|
|
8
|
+
on Windows) or Python when that fits better. Python scripts that need third-party packages get
|
|
9
|
+
their own virtual environment. The next run is instant:
|
|
8
10
|
no network, no config. Before anything risky is saved or run, every generated script is checked
|
|
9
11
|
by a built-in static scan and, optionally, by a decision model: [Jev](https://docs.typesafe.ai)
|
|
10
|
-
by TypeSafe AI (hosted)
|
|
12
|
+
by TypeSafe AI (hosted), [OpenAI Decisions](https://developers.openai.com/api/docs/guides/decisions),
|
|
13
|
+
or a local model served by [Ollaya](https://ollaya.dev). Risky scripts
|
|
11
14
|
are shown to you and need an explicit `[y/N]` approval.
|
|
12
15
|
|
|
13
16
|
## Install
|
|
@@ -20,7 +23,19 @@ pipx install aiscript
|
|
|
20
23
|
pip install aiscript
|
|
21
24
|
```
|
|
22
25
|
|
|
23
|
-
Requires Python 3.10+.
|
|
26
|
+
Requires Python 3.10+.
|
|
27
|
+
|
|
28
|
+
To upgrade to the latest version, use the same tool you installed with:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
uv tool upgrade aiscript
|
|
32
|
+
# or
|
|
33
|
+
pipx upgrade aiscript
|
|
34
|
+
# or
|
|
35
|
+
pip install --upgrade aiscript
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Your config, prompts and cached scripts in `~/.config/aiscript/` are kept. To work on aiscript itself, see [docs/development.md](docs/development.md).
|
|
24
39
|
|
|
25
40
|
## Quickstart
|
|
26
41
|
|
|
@@ -58,7 +73,7 @@ Every generated script is reviewed **before it is saved**:
|
|
|
58
73
|
- A **static regex scan** (always on) flags privilege escalation, recursive or system-wide file
|
|
59
74
|
changes, networking/firewall changes and remote code execution (`curl | sh`, `iex`).
|
|
60
75
|
- A **decision model** (`reviewer_provider`) gives a second opinion: **Jev** (hosted; key from
|
|
61
|
-
<https://console.typesafe.ai/keys>) or **Ollaya** (local and keyless; set
|
|
76
|
+
<https://console.typesafe.ai/keys>), **OpenAI Decisions** (see below), or **Ollaya** (local and keyless; set
|
|
62
77
|
`reviewer_provider: ollaya`, default model `jevk5:4b` on `localhost:11435`; see
|
|
63
78
|
<https://ollaya.dev>). Comment `reviewer_provider` out to use only the static scan.
|
|
64
79
|
|
|
@@ -73,7 +88,8 @@ a best-effort safety net, not a sandbox: read what you approve. Details: [docs/s
|
|
|
73
88
|
|
|
74
89
|
```yaml
|
|
75
90
|
# aiscript config. Contains API keys: keep it private (chmod 600) and out of git.
|
|
76
|
-
# Keys can also come from the environment: OPENAI_API_KEY,
|
|
91
|
+
# Keys can also come from the environment: OPENAI_API_KEY, OPENAI_DECISIONS_API_KEY,
|
|
92
|
+
# TYPESAFE_API_KEY, <PROVIDER>_API_KEY.
|
|
77
93
|
|
|
78
94
|
# Provider that writes the scripts (override with $AISCRIPT_PROVIDER).
|
|
79
95
|
default_provider: openai
|
|
@@ -89,6 +105,13 @@ providers:
|
|
|
89
105
|
model: "gpt-6-luna"
|
|
90
106
|
# reasoning_effort: low # none | low | medium | high | xhigh | max (model-dependent)
|
|
91
107
|
|
|
108
|
+
# OpenAI Decisions API reviewer provider.
|
|
109
|
+
# openai_decisions:
|
|
110
|
+
# kind: openai
|
|
111
|
+
# api: decisions
|
|
112
|
+
# api_key: "..."
|
|
113
|
+
# model: "gpt-6-luna"
|
|
114
|
+
|
|
92
115
|
# Jev (TypeSafe AI) safety reviewer. API key: https://console.typesafe.ai/keys
|
|
93
116
|
jev:
|
|
94
117
|
api_key: "..."
|
|
@@ -107,6 +130,22 @@ Placeholder keys (`"..."`) are ignored, so if you leave them, the environment va
|
|
|
107
130
|
All options (provider kinds, `api`, `temperature`, key and model resolution, every environment
|
|
108
131
|
variable, the legacy `secrets.yaml`): [docs/configuration.md](docs/configuration.md).
|
|
109
132
|
|
|
133
|
+
### OpenAI Decisions reviewer
|
|
134
|
+
|
|
135
|
+
To use OpenAI Decisions instead of Jev, select `reviewer_provider: openai_decisions` and
|
|
136
|
+
uncomment the corresponding provider block above. Supply its own `api_key`, or export
|
|
137
|
+
`OPENAI_DECISIONS_API_KEY`; this named reviewer does not fall back to `OPENAI_API_KEY`.
|
|
138
|
+
The default model is `gpt-6-luna`, and `model` remains configurable for future models.
|
|
139
|
+
|
|
140
|
+
Decisions uses the official OpenAI endpoint only. One request checks the same four hazards,
|
|
141
|
+
flagging probabilities **>= 0.5**. Requests use a 30-second transport timeout with zero retries.
|
|
142
|
+
Errors, refusals and malformed answers require the existing risk confirmation; they are refused
|
|
143
|
+
in noninteractive builds. Static findings and dependency warnings always remain effective.
|
|
144
|
+
|
|
145
|
+
Existing reviewers and cached scripts keep working. To roll back, restore your previous
|
|
146
|
+
`reviewer_provider` before downgrading aiscript. See [configuration](docs/configuration.md#openai-decisions)
|
|
147
|
+
and [safety](docs/safety.md#openai-decisions) for the full contract.
|
|
148
|
+
|
|
110
149
|
## Usage
|
|
111
150
|
|
|
112
151
|
| Command | Action |
|
|
@@ -121,17 +160,43 @@ variable, the legacy `secrets.yaml`): [docs/configuration.md](docs/configuration
|
|
|
121
160
|
|
|
122
161
|
Command names must match `[A-Za-z0-9][A-Za-z0-9_-]*`.
|
|
123
162
|
|
|
163
|
+
## Shell or Python
|
|
164
|
+
|
|
165
|
+
The model is told your OS, architecture, native shell, the Python version it will run under and
|
|
166
|
+
whether [uv](https://docs.astral.sh/uv/) is installed, and chooses the language: the shell for thin
|
|
167
|
+
wrappers around CLI tools and pipelines, Python for parsing JSON/CSV/HTML, data processing, HTTP
|
|
168
|
+
APIs and anything that would be fragile in shell. Say "in python" or "as a shell script" in your
|
|
169
|
+
description to ask for one; the model follows it unless it is clearly a poor fit (and then notes
|
|
170
|
+
why in the script header). `aiscript --list` shows each command's language.
|
|
171
|
+
|
|
172
|
+
- **Standard-library Python** is saved as `functions/NAME.py` and run with the system `python3`
|
|
173
|
+
(`py -3` on Windows), or with aiscript's own interpreter if there is none.
|
|
174
|
+
- **Python with dependencies** declares them in an inline
|
|
175
|
+
[PEP 723](https://peps.python.org/pep-0723/) block. After you approve (any dependency requires a
|
|
176
|
+
`[y/N]` approval that lists the packages), aiscript creates `functions/NAME/` with `main.py` and
|
|
177
|
+
a private `.venv`: with uv (`pyproject.toml` + `uv.lock`, `uv sync`) or otherwise with
|
|
178
|
+
`python -m venv` + `pip install -r requirements.txt`. The script always runs with that venv's
|
|
179
|
+
interpreter, so later runs are offline. A rebuild with the same dependencies reuses the venv; a
|
|
180
|
+
failed install keeps the previous version. Without uv, Debian/Ubuntu need `python3-venv`.
|
|
181
|
+
|
|
124
182
|
## How it works
|
|
125
183
|
|
|
126
184
|
```
|
|
127
185
|
~/.config/aiscript/ # or $XDG_CONFIG_HOME/aiscript, or $AISCRIPT_HOME
|
|
128
186
|
├── config.yaml # providers and keys
|
|
129
187
|
├── prompts/<name>.txt # your intent for each command
|
|
130
|
-
└── functions
|
|
188
|
+
└── functions/
|
|
189
|
+
├── <name>.sh # generated, reviewed scripts (.ps1 on Windows)
|
|
190
|
+
├── <name>.py # standard-library Python
|
|
191
|
+
└── <name>/ # Python with dependencies
|
|
192
|
+
├── main.py
|
|
193
|
+
├── .venv/
|
|
194
|
+
└── pyproject.toml + uv.lock (or requirements.txt)
|
|
131
195
|
```
|
|
132
196
|
|
|
133
197
|
The first run of `NAME` saves your description to `prompts/NAME.txt`, asks the LLM for a script,
|
|
134
|
-
reviews it, and stores it in `functions
|
|
198
|
+
reviews it, and stores it in `functions/` (only one build per command: switching language
|
|
199
|
+
replaces the old one). Later runs just execute the cached script. If the
|
|
135
200
|
prompt is newer than the script (after `--edit` or a manual change), the script is rebuilt on the
|
|
136
201
|
next run. A `--rebuild` that fails or is rejected keeps the previous script.
|
|
137
202
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aiscript"
|
|
3
|
-
version = "1.
|
|
4
|
-
description = "JIT-compile plain-English intents into cached, reviewed shell scripts via an LLM."
|
|
3
|
+
version = "1.2.0"
|
|
4
|
+
description = "JIT-compile plain-English intents into cached, reviewed shell or Python scripts via an LLM."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
@@ -33,6 +33,12 @@ dev = [
|
|
|
33
33
|
[tool.ruff]
|
|
34
34
|
line-length = 100
|
|
35
35
|
target-version = "py310"
|
|
36
|
+
extend-exclude = [
|
|
37
|
+
".agents/skills",
|
|
38
|
+
".claude/skills",
|
|
39
|
+
".cursor/skills",
|
|
40
|
+
".codex/skills",
|
|
41
|
+
]
|
|
36
42
|
|
|
37
43
|
[tool.ruff.lint]
|
|
38
44
|
select = [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aiscript"
|
|
3
|
-
version = "1.
|
|
4
|
-
description = "JIT-compile plain-English intents into cached, reviewed shell scripts via an LLM."
|
|
3
|
+
version = "1.2.0"
|
|
4
|
+
description = "JIT-compile plain-English intents into cached, reviewed shell or Python scripts via an LLM."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
@@ -32,6 +32,8 @@ dev = [
|
|
|
32
32
|
[tool.ruff]
|
|
33
33
|
line-length = 100
|
|
34
34
|
target-version = "py310"
|
|
35
|
+
# Installed skills carry supporting code with their own style conventions.
|
|
36
|
+
extend-exclude = [".agents/skills", ".claude/skills", ".cursor/skills", ".codex/skills"]
|
|
35
37
|
|
|
36
38
|
[tool.ruff.lint]
|
|
37
39
|
select = ["E", "F", "I", "UP", "B", "ANN"]
|
|
@@ -4,11 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import contextlib
|
|
6
6
|
import os
|
|
7
|
+
import shutil
|
|
7
8
|
import sys
|
|
8
9
|
import tempfile
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
|
-
from aiscript import llm, paths, reviewer
|
|
12
|
+
from aiscript import llm, paths, pysetup, reviewer
|
|
12
13
|
from aiscript.config import Settings
|
|
13
14
|
from aiscript.errors import AiscriptError
|
|
14
15
|
|
|
@@ -44,17 +45,26 @@ def build(name: str, settings: Settings) -> Path:
|
|
|
44
45
|
print(f"Generating {name!r} with {gen.name}/{gen.model}...", file=sys.stderr)
|
|
45
46
|
script = llm.generate_script(gen, name, intent)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if not reviewer.confirm(script, result):
|
|
48
|
+
result = reviewer.review(script.code, script.language, settings.reviewer, script.dependencies)
|
|
49
|
+
if not reviewer.confirm(script.code, result):
|
|
50
50
|
raise AiscriptError("Script rejected; nothing saved.")
|
|
51
51
|
|
|
52
52
|
paths.ensure_dirs()
|
|
53
|
-
|
|
53
|
+
if script.dependencies:
|
|
54
|
+
target = pysetup.install(name, script.code, script.dependencies)
|
|
55
|
+
else:
|
|
56
|
+
target = paths.function_target(name, script.language, has_deps=False)
|
|
57
|
+
_write_file(name, target, script.code)
|
|
58
|
+
_remove_other_artifacts(name, target)
|
|
59
|
+
print(f"Saved {target}", file=sys.stderr)
|
|
60
|
+
return target
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _write_file(name: str, target: Path, code: str) -> None:
|
|
54
64
|
fd, tmp_name = tempfile.mkstemp(dir=paths.functions_dir(), prefix=f".{name}.", suffix=".tmp")
|
|
55
65
|
try:
|
|
56
66
|
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as fh:
|
|
57
|
-
fh.write(
|
|
67
|
+
fh.write(code)
|
|
58
68
|
if not paths.is_windows():
|
|
59
69
|
os.chmod(tmp_name, 0o755)
|
|
60
70
|
os.replace(tmp_name, target)
|
|
@@ -62,5 +72,17 @@ def build(name: str, settings: Settings) -> Path:
|
|
|
62
72
|
with contextlib.suppress(OSError):
|
|
63
73
|
os.unlink(tmp_name)
|
|
64
74
|
raise
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _remove_other_artifacts(name: str, target: Path) -> None:
|
|
78
|
+
"""Drop builds of ``name`` in another language or layout, so only ``target`` remains."""
|
|
79
|
+
for path in paths.function_artifacts(name):
|
|
80
|
+
if path == target or path in target.parents:
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
if path.is_dir():
|
|
84
|
+
shutil.rmtree(path)
|
|
85
|
+
else:
|
|
86
|
+
path.unlink()
|
|
87
|
+
except OSError as exc:
|
|
88
|
+
print(f"aiscript: warning: could not remove old build {path}: {exc}", file=sys.stderr)
|
|
@@ -22,7 +22,7 @@ usage: aiscript NAME [args...] run a command (created/built on first use)
|
|
|
22
22
|
aiscript --list list commands
|
|
23
23
|
aiscript --edit NAME edit a command's prompt (rebuilds on next run)
|
|
24
24
|
aiscript --rebuild NAME regenerate a command's script (does not run it)
|
|
25
|
-
aiscript --remove NAME delete a command's prompt and script
|
|
25
|
+
aiscript --remove NAME delete a command's prompt and script(s)
|
|
26
26
|
aiscript --help | --version
|
|
27
27
|
|
|
28
28
|
Everything after NAME is passed untouched to the generated script.
|
|
@@ -92,12 +92,15 @@ def _rebuild(name: str) -> int:
|
|
|
92
92
|
|
|
93
93
|
def _remove(name: str) -> int:
|
|
94
94
|
paths.validate_command_name(name)
|
|
95
|
-
targets = [p for p in (paths.prompt_path(name), paths.
|
|
95
|
+
targets = [p for p in (paths.prompt_path(name), *paths.function_artifacts(name)) if p.exists()]
|
|
96
96
|
if not targets:
|
|
97
97
|
raise AiscriptError(f"No command named {name!r}.")
|
|
98
98
|
for path in targets:
|
|
99
99
|
try:
|
|
100
|
-
path.
|
|
100
|
+
if path.is_dir():
|
|
101
|
+
shutil.rmtree(path)
|
|
102
|
+
else:
|
|
103
|
+
path.unlink()
|
|
101
104
|
except OSError as exc:
|
|
102
105
|
raise AiscriptError(f"Could not remove {path}: {exc}") from exc
|
|
103
106
|
_err(f"Removed {path}")
|
|
@@ -21,7 +21,7 @@ class ProviderConfig:
|
|
|
21
21
|
base_url: str | None
|
|
22
22
|
model: str
|
|
23
23
|
kind: str = "openai" # "openai" (OpenAI-compatible) or "jev" (TypeSafe System One; Ollaya too)
|
|
24
|
-
api: str = "chat" # for kind "openai": "responses" or "
|
|
24
|
+
api: str = "chat" # for kind "openai": "responses", "chat", or reviewer-only "decisions"
|
|
25
25
|
reasoning_effort: str | None = None # sent only when set
|
|
26
26
|
temperature: float | None = None # sent only when set (GPT-6 rejects it unless effort=none)
|
|
27
27
|
|
|
@@ -34,11 +34,12 @@ class Settings:
|
|
|
34
34
|
|
|
35
35
|
DEFAULT_PROVIDER = "openai"
|
|
36
36
|
DEFAULT_MODEL = "gpt-6-luna"
|
|
37
|
+
DECISIONS_DEFAULT_MODEL = "gpt-6-luna"
|
|
37
38
|
JEV_DEFAULT_MODEL = "jev-latest"
|
|
38
39
|
OLLAYA_DEFAULT_MODEL = "jevk5:4b"
|
|
39
40
|
OLLAYA_DEFAULT_URL = "http://localhost:11435"
|
|
40
41
|
_KINDS = {"openai", "jev", "ollaya"}
|
|
41
|
-
_APIS = {"responses", "chat"}
|
|
42
|
+
_APIS = {"responses", "chat", "decisions"}
|
|
42
43
|
_OPENAI_HOSTS = {"api.openai.com"}
|
|
43
44
|
_JEV_NAMES = {"jev", "typesafe"}
|
|
44
45
|
_OLLAYA_NAMES = {"ollaya"}
|
|
@@ -157,6 +158,50 @@ def _resolve_provider(
|
|
|
157
158
|
f"Provider '{name}' in {path} has unknown kind '{kind}' "
|
|
158
159
|
"(expected: openai, jev, ollaya)."
|
|
159
160
|
)
|
|
161
|
+
if (_clean(entry.get("api")) or "").lower() == "decisions":
|
|
162
|
+
if generator:
|
|
163
|
+
raise ConfigError(
|
|
164
|
+
f"Provider '{name}' in {path}: Decisions is only supported as a reviewer. "
|
|
165
|
+
"Select it with `reviewer_provider` and use another generator."
|
|
166
|
+
)
|
|
167
|
+
if kind != "openai":
|
|
168
|
+
raise ConfigError(f"Provider '{name}' in {path}: Decisions requires kind 'openai'.")
|
|
169
|
+
for setting in ("temperature", "reasoning_effort"):
|
|
170
|
+
if setting in entry:
|
|
171
|
+
raise ConfigError(
|
|
172
|
+
f"Provider '{name}' in {path}: {setting} is not supported by Decisions. "
|
|
173
|
+
"Remove that setting."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
official_url = "https://api.openai.com/v1"
|
|
177
|
+
for alias in ("base_url", "endpoint"):
|
|
178
|
+
if alias in entry and _clean(entry[alias]) not in (official_url, official_url + "/"):
|
|
179
|
+
raise ConfigError(
|
|
180
|
+
f"Provider '{name}' in {path}: Decisions {alias} must be "
|
|
181
|
+
f"{official_url} (an optional trailing slash is accepted)."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
api_key = None
|
|
185
|
+
env_name = _env_key_name(name)
|
|
186
|
+
for candidate in (entry.get("api_key"), os.environ.get(env_name)):
|
|
187
|
+
cleaned_key = _clean(candidate)
|
|
188
|
+
if cleaned_key is not None and not cleaned_key.endswith("..."):
|
|
189
|
+
api_key = cleaned_key
|
|
190
|
+
break
|
|
191
|
+
if api_key is None:
|
|
192
|
+
raise ConfigError(
|
|
193
|
+
f"No API key for provider '{name}'. Set `api_key` for it in {path} "
|
|
194
|
+
f"or export {env_name}; Decisions requires a dedicated reviewer credential."
|
|
195
|
+
)
|
|
196
|
+
return ProviderConfig(
|
|
197
|
+
name=name,
|
|
198
|
+
api_key=api_key,
|
|
199
|
+
base_url=official_url,
|
|
200
|
+
model=_clean(entry.get("model")) or DECISIONS_DEFAULT_MODEL,
|
|
201
|
+
kind="openai",
|
|
202
|
+
api="decisions",
|
|
203
|
+
)
|
|
204
|
+
|
|
160
205
|
# Ollaya speaks the Jev wire protocol: only its defaults differ, so it becomes kind "jev".
|
|
161
206
|
ollaya = kind == "ollaya"
|
|
162
207
|
if ollaya:
|
|
@@ -222,7 +267,8 @@ def _resolve_provider(
|
|
|
222
267
|
)
|
|
223
268
|
if api not in _APIS:
|
|
224
269
|
raise ConfigError(
|
|
225
|
-
f"Provider '{name}' in {path} has unknown api '{api}'
|
|
270
|
+
f"Provider '{name}' in {path} has unknown api '{api}' "
|
|
271
|
+
"(expected: responses, chat, decisions)."
|
|
226
272
|
)
|
|
227
273
|
reasoning_effort = _clean(entry.get("reasoning_effort"))
|
|
228
274
|
temperature = entry.get("temperature")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Domain values and gateway contract for Decisions safety review."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal, Protocol
|
|
7
|
+
|
|
8
|
+
HazardName = Literal["privilege_escalation", "filesystem", "networking", "remote_code_execution"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ReviewInput:
|
|
13
|
+
"""Script and metadata supplied as untrusted review data."""
|
|
14
|
+
|
|
15
|
+
script: str
|
|
16
|
+
language: str
|
|
17
|
+
dependencies: tuple[str, ...]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class HazardQuestion:
|
|
22
|
+
"""Fixed hazard definition shared by reviewer adapters."""
|
|
23
|
+
|
|
24
|
+
name: HazardName
|
|
25
|
+
category: str
|
|
26
|
+
instructions: str
|
|
27
|
+
true_criteria: str
|
|
28
|
+
false_criteria: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class DecisionsCallError(Exception):
|
|
33
|
+
"""Bounded failure metadata without a provider payload."""
|
|
34
|
+
|
|
35
|
+
kind: Literal["client", "connection", "timeout", "http", "unexpected"]
|
|
36
|
+
status_code: int | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DecisionsGateway(Protocol):
|
|
40
|
+
"""Port returning untrusted decoded response data."""
|
|
41
|
+
|
|
42
|
+
def classify(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
model: str,
|
|
46
|
+
review_input: ReviewInput,
|
|
47
|
+
questions: tuple[HazardQuestion, ...],
|
|
48
|
+
) -> object: ...
|