spl-lang 0.1.1 → 0.3.0
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 +73 -129
- package/dist/ast.d.ts +12 -19
- package/dist/cli.js +58 -13
- package/dist/copilot.d.ts +6 -3
- package/dist/copilot.js +23 -10
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/interpreter.js +106 -73
- package/dist/llm.d.ts +5 -10
- package/dist/llm.js +17 -26
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +7 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +108 -0
- package/dist/parser.d.ts +2 -12
- package/dist/parser.js +36 -105
- package/dist/recording.d.ts +17 -0
- package/dist/recording.js +93 -0
- package/dist/restrict.d.ts +3 -0
- package/dist/restrict.js +27 -1
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +40 -0
- package/dist/tools.d.ts +9 -1
- package/dist/tools.js +223 -28
- package/dist/values.d.ts +1 -1
- package/docs/syntax.ebnf +53 -69
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,147 +1,91 @@
|
|
|
1
|
-
# spl-lang
|
|
1
|
+
# spl-lang 0.3
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
calls go through the GitHub Copilot SDK. See [../LANG-PRD.md](../LANG-PRD.md) for the full spec,
|
|
5
|
-
[docs/syntax.ebnf](docs/syntax.ebnf) for the grammar and the [documentation site](../docs/index.html) (open it in a
|
|
6
|
-
browser, or publish `docs/` with GitHub Pages) for guides and reference.
|
|
3
|
+
The Semantic Prompt Language interpreter. Requires Node.js 20+.
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
See the [project guide](../README.md), [language specification](../LANG-PRD.md),
|
|
6
|
+
[EBNF](docs/syntax.ebnf), and [documentation site](../docs/index.html).
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
npm install && npm run build
|
|
12
|
-
node dist/cli.js login # once: authenticate with GitHub Copilot
|
|
13
|
-
node dist/cli.js examples/live.spl
|
|
14
|
-
npx file:./lang examples/live.spl # npx from a checkout (or `npx spl-lang` once published)
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Requires Node 20+ and an authenticated GitHub Copilot.
|
|
8
|
+
## Run from a checkout
|
|
18
9
|
|
|
10
|
+
```sh
|
|
11
|
+
npm install
|
|
12
|
+
node dist/cli.js login
|
|
13
|
+
node dist/cli.js examples/hi.spl
|
|
19
14
|
```
|
|
20
|
-
spl <script.spl> [--log-level error|info|debug] [--silent] [--model <id>]
|
|
21
|
-
[--store <file.json> | --store-module <module>]
|
|
22
|
-
[--timeout <s>] [--cwd <dir>] [--check]
|
|
23
|
-
spl login # GitHub device flow: prints a code, you confirm it in the browser
|
|
24
|
-
spl logout # removes the credentials saved by `spl login`
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
* stdout: the value of `success` or a top-level `return` (strings raw, everything else as JSON) or the failure payload.
|
|
28
|
-
* stderr: logs, including model and reasoning effort of every LLM call.
|
|
29
|
-
* Exit code: `0` success, `1` uncaught failure, `2` syntax/usage error.
|
|
30
|
-
|
|
31
|
-
`spl login` does nothing if the runtime is already authenticated (an earlier login, a Copilot CLI / `gh` login or
|
|
32
|
-
`COPILOT_GITHUB_TOKEN`/`GH_TOKEN`/`GITHUB_TOKEN`). Otherwise it runs the device flow and saves the token to
|
|
33
|
-
`~/.spl/credentials.json` (override the folder with `SPL_HOME`); scripts use that token automatically. A script that
|
|
34
|
-
needs an LLM and finds no credentials fails with `llm_error` and tells you to run `spl login`.
|
|
35
|
-
|
|
36
|
-
## Language at a glance
|
|
37
15
|
|
|
16
|
+
`npm install` runs the TypeScript build. Installed packages expose `spl` and `spl-lang`.
|
|
17
|
+
Authentication is needed only for live generation and explicit agents; raw tools and replay
|
|
18
|
+
work without Copilot. Existing Copilot/CLI credentials and supported GitHub token environment
|
|
19
|
+
variables are reused. `spl login` otherwise runs device authentication and saves credentials
|
|
20
|
+
under `~/.spl` (override with `SPL_HOME`); `spl logout` removes those saved credentials.
|
|
21
|
+
|
|
22
|
+
## Language contract
|
|
23
|
+
|
|
24
|
+
- **Exact tools:** `read_file(path)`, `write_file(path, content)`, `web_fetch(url)`,
|
|
25
|
+
`web_search(query)`, `shell(command)` never call a model.
|
|
26
|
+
- **Generation:** `?` and `think` have no tools or MCP access.
|
|
27
|
+
- **Agency:** `agent` explicitly enables model-directed runtime and configured MCP tools.
|
|
28
|
+
- **Validation:** expression-valued `schema` uses strict synchronous JSON Schema draft-07;
|
|
29
|
+
`fmt='json'` accepts any JSON value, `fmt='object'` requires an object/array.
|
|
30
|
+
Prose and code fences are not silently extracted.
|
|
31
|
+
- **Repairs:** `repairs=0..5` retries only malformed/invalid generation, not transport errors
|
|
32
|
+
or external actions. Agents require `repairs=0`. Use `assert` for business rules.
|
|
33
|
+
- **Guidance:** `instruct`, `avoid`, and `len` influence prompts, not runtime guarantees.
|
|
34
|
+
- **Capabilities:** nested `restrict fs/net/exec/mcp` clauses intersect. Shell command
|
|
35
|
+
inspection is best effort; MCP servers are trusted services, not OS-sandboxed.
|
|
36
|
+
- **Bindings:** `let` declares/redeclares function/script locals; `x = ...` and `x += ...`
|
|
37
|
+
update existing locals. Every block shares that scope. Enclosing names are readable but
|
|
38
|
+
cannot be rebound by a function; shared collections remain mutable.
|
|
39
|
+
|
|
40
|
+
Common generation modifiers are `len`, `fmt`, `reasoning`, `show`, `schema`, `repairs`,
|
|
41
|
+
and `model`. Values are evaluated once, then validated before calling the model.
|
|
42
|
+
`think` and `agent` default to medium reasoning; `?` uses the model default.
|
|
43
|
+
CLI `--model` overrides per-call `model`, which overrides the header directive.
|
|
44
|
+
|
|
45
|
+
## Record/replay
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
node dist/cli.js examples/triage-issues.spl --record triage.jsonl
|
|
49
|
+
node dist/cli.js examples/triage-issues.spl --replay triage.jsonl --silent
|
|
38
50
|
```
|
|
39
|
-
@{model='gpt-5.5'}
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
Record creates a new file and rejects agents. Replay performs exact, ordered request matching
|
|
53
|
+
without starting Copilot/MCP. Local reads are permitted; writes, shell, network, agents,
|
|
54
|
+
and persistent stores are blocked. Recordings include prompts and raw responses and must
|
|
55
|
+
be protected like logs. Mismatched, missing, or unused entries fail explicitly.
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
let why = think{level='high', show=True} 'Why is {joke.setup} funny?'
|
|
57
|
+
## CLI
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
```text
|
|
60
|
+
spl script.spl [--model id] [--mcp file.json] [--cwd dir] [--timeout seconds]
|
|
61
|
+
[--store file.json | --store-module module]
|
|
62
|
+
[--record file.jsonl | --replay file.jsonl]
|
|
63
|
+
[--log-level error|info|debug] [--silent] [--check]
|
|
64
|
+
```
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
Results are printed on stdout, logs on stderr. Full prompts/responses and statement traces
|
|
67
|
+
appear at every non-silent log level. Exit codes are `0` success, `1` script failure,
|
|
68
|
+
`2` syntax/usage error, and `70` internal error. `--check` does not execute modifier expressions
|
|
69
|
+
or compile schemas. See [CLI and MCP configuration](../docs/cli.html).
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
let f = write_file{path='./out/a.txt', content='{fib(10)}'} 'Confirm'
|
|
61
|
-
}
|
|
71
|
+
## Tests and embedding
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
```sh
|
|
74
|
+
npm test
|
|
65
75
|
```
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* **Blocks use braces**, statements end at a newline.
|
|
70
|
-
* **Every modification of a variable starts with `let`**: `let x = 1`, `let x += 1`, `let d['k'] = 2`, `let d.k = 3`.
|
|
71
|
-
A bare `x = 1` is a syntax error.
|
|
72
|
-
* **Scoping.** `let NAME = ...` binds in the current scope. Bare `{ ... }` blocks and function bodies open a new scope,
|
|
73
|
-
so `let` there shadows an outer variable and the outer one is untouched. `if`/`elif`/`else`, `for`, `while`,
|
|
74
|
-
`try`/`except`, `enforce`, `forbid` and `restrict` bodies share the enclosing scope, so `let` inside them updates the
|
|
75
|
-
enclosing variable: this is how the answer of an LLM call leaves an `enforce`/`forbid`/`restrict` block. A function
|
|
76
|
-
cannot rebind its caller's variables (use a list or dict to share state); names are read through all enclosing scopes.
|
|
77
|
-
* **`return` exits immediately** from any scope: the innermost function, or the whole script when used at the top level
|
|
78
|
-
(like `success`). `break`/`continue` still belong to loops.
|
|
79
|
-
* **`?` is a prefix operator**: `?'a' + b` is `(?'a') + b`, `-?'n'` is `-(?'n')`.
|
|
80
|
-
* **LLM commands have defaults and braces are optional**: `?'q'`, `think 'q'` (level `medium`), and tools without
|
|
81
|
-
parameters, e.g. `read_file 'Read notes.txt and summarize it'`.
|
|
82
|
-
* **Every string interpolates `{expr}`**. Write `{{`/`}}` or `\{`/`\}` for literal braces, or use a raw string
|
|
83
|
-
`r'{"a": 1}'`. Values are substituted as is: strings raw, `True`/`False`/`None`, lists and dicts as JSON.
|
|
84
|
-
* Tool calls (`read_file`, `write_file`, `web_fetch`, `web_search`, `shell`): when the script supplies every parameter
|
|
85
|
-
the tool needs, the runtime runs it first (restriction-checked), and the output plus your instructions go to the LLM
|
|
86
|
-
whose answer is the value. When a parameter is missing (`read_file 'summarize notes.txt'`,
|
|
87
|
-
`write_file{path='a.txt'} 'write a haiku'`, `web_search 'latest Node.js LTS'`) the LLM calls the tool itself with the
|
|
88
|
-
parameters you gave kept fixed. `web_search` queries DuckDuckGo and counts as network access for `restrict net`.
|
|
89
|
-
* Builtins: `len str int float bool type list range abs round min max sum sorted reversed enumerate zip any all keys
|
|
90
|
-
values items json_parse json_stringify log`, plus Python-style string/list/dict methods. `log(...)` writes to stderr.
|
|
91
|
-
* Unknown modifiers/values, `break` outside loops, `try` without `except`, a modification without `let`, etc. are
|
|
92
|
-
syntax errors (exit code 2).
|
|
93
|
-
|
|
94
|
-
## Restrictions
|
|
95
|
-
|
|
96
|
-
Two layers: the LLM is told about active restrictions in the prompt, and the runtime enforces them. The LLM only gets
|
|
97
|
-
the five runtime tools (`read_file`, `write_file`, `web_fetch`, `web_search`, `shell`); every call is checked against
|
|
98
|
-
the active `restrict` scopes and any other Copilot permission request is rejected. A violation raises a
|
|
99
|
-
`restriction_violation` failure (catchable with `try/except`).
|
|
100
|
-
|
|
101
|
-
* Paths are canonicalized (symlinks resolved, `..` removed) relative to `--cwd` before the check.
|
|
102
|
-
* `net` matches exact hosts (and `*.example.com` wildcards); redirects are re-checked on every hop. `web_search`
|
|
103
|
-
needs `html.duckduckgo.com` to be allowed.
|
|
104
|
-
* `exec` matches the executable name. While any restriction is active, `shell` accepts only simple commands (no
|
|
105
|
-
`&|;<>` redirects, substitution or chaining) so the check is exact; path-like and URL arguments are also checked
|
|
106
|
-
against `fs`/`net`. This is best effort for programs that touch files or the network on their own.
|
|
107
|
-
* Nested restrictions of one kind intersect and can never widen.
|
|
108
|
-
|
|
109
|
-
## Reproducibility
|
|
110
|
-
|
|
111
|
-
Control flow, arithmetic and interpolation are deterministic. The Copilot SDK has no `temperature` or `seed` setting,
|
|
112
|
-
so the language has none either. `think` levels map to the SDK reasoning effort; if the selected model does not support
|
|
113
|
-
it a warning is logged and the model default is used. Guard against variance with `assert` and `fmt='object'`.
|
|
114
|
-
|
|
115
|
-
## Variable store
|
|
116
|
-
|
|
117
|
-
`--store vars.json` loads top-level variables from the file at start and writes them back at exit (also on failure).
|
|
118
|
-
`--store-module ./mystore.mjs` plugs in your own store: export `createStore()` returning `{ load(): Record, save(vars) }`.
|
|
119
|
-
|
|
120
|
-
## Examples
|
|
121
|
-
|
|
122
|
-
All in [examples/](examples); run from the project root, `--cwd <dir>` points a script at another project. The example
|
|
123
|
-
syntax is checked by `tests/examples.test.ts`; `triage-issues` is also run on Copilot there.
|
|
124
|
-
|
|
125
|
-
| Script | Shows |
|
|
126
|
-
|--------|-------|
|
|
127
|
-
| `commit-message.spl` | `shell` under `restrict exec 'git'`, validation helper, retry loop |
|
|
128
|
-
| `triage-issues.spl` | `fmt='object'` + `assert` validation with feedback retries, aggregation, summary |
|
|
129
|
-
| `extract-invoice.spl` | structured extraction cross-checked with arithmetic before it is accepted |
|
|
130
|
-
| `code-review.spl` | `think` then structure, `restrict` on read and on write, `fail` as a CI gate |
|
|
131
|
-
| `release-notes.spl` | nested `forbid`/`enforce`, git history to a file inside `./out` |
|
|
132
|
-
| `web-digest.spl` | `restrict net`, `web_fetch`, catching a blocked second host |
|
|
133
|
-
| `fix-failing-tests.spl` | agent loop: run `npm test`, let the LLM fix the source, repeat |
|
|
134
|
-
| `translate-docs.spl` | `write_file` without `content` (LLM writes, path pinned), dict iteration |
|
|
135
|
-
| `journal.spl` | state across runs with `--store` |
|
|
136
|
-
| `basics.spl`, `live.spl`, `restrict-live.spl`, `tools-live.spl` | small feature tours |
|
|
137
|
-
|
|
138
|
-
## Develop
|
|
77
|
+
The default suite is offline. In PowerShell, opt into authenticated live checks with
|
|
78
|
+
`$env:SPL_LIVE_TESTS='1'; npm test`, then remove the environment variable.
|
|
139
79
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
80
|
+
The package exports `parse`, `run`, `Logger`, `CopilotBackend`, `RecordingBackend`,
|
|
81
|
+
`ReplayBackend`, variable stores, authentication helpers, and related types.
|
|
82
|
+
Call `close()` on the backend after running; replay also checks for unused entries there.
|
|
83
|
+
Custom backends must honor the request kind: only `agent` may execute tools.
|
|
84
|
+
See [embedding](../docs/internals.html#embedding).
|
|
143
85
|
|
|
144
|
-
|
|
145
|
-
are skipped automatically when the runtime is not authenticated: run `spl login` first to include them.
|
|
86
|
+
## Migration
|
|
146
87
|
|
|
147
|
-
|
|
88
|
+
0.3 intentionally removes `enforce`/`forbid`, `think{level=...}`, tool-command syntax,
|
|
89
|
+
and `let` before compound/member/index assignments. Use `instruct`/`avoid`,
|
|
90
|
+
`reasoning`, ordinary raw functions, and explicit agents instead.
|
|
91
|
+
Bare blocks no longer create scopes. See the [migration table](../README.md#breaking-changes-in-03).
|
package/dist/ast.d.ts
CHANGED
|
@@ -61,29 +61,16 @@ export type Expr = {
|
|
|
61
61
|
lo?: Expr;
|
|
62
62
|
hi?: Expr;
|
|
63
63
|
} | {
|
|
64
|
-
t: 'ask';
|
|
65
|
-
mods:
|
|
64
|
+
t: 'ask' | 'think' | 'agent';
|
|
65
|
+
mods: Record<string, Expr>;
|
|
66
66
|
prompt: Expr;
|
|
67
|
-
} | {
|
|
68
|
-
t: 'think';
|
|
69
|
-
mods: ThinkMods;
|
|
70
|
-
prompt: Expr;
|
|
71
|
-
} | {
|
|
72
|
-
t: 'tool';
|
|
73
|
-
tool: ToolName;
|
|
74
|
-
params: Record<string, Expr>;
|
|
75
|
-
instructions: Expr;
|
|
76
67
|
};
|
|
77
68
|
export interface AskMods {
|
|
78
69
|
len?: 'word' | 'sentence' | 'paragraph' | 'long';
|
|
79
70
|
fmt?: 'text' | 'object';
|
|
80
71
|
}
|
|
81
|
-
export interface ThinkMods {
|
|
82
|
-
level?: 'low' | 'medium' | 'high';
|
|
83
|
-
show?: boolean;
|
|
84
|
-
}
|
|
85
72
|
export type ToolName = 'read_file' | 'write_file' | 'web_fetch' | 'web_search' | 'shell';
|
|
86
|
-
export type RestrictKind = 'fs' | 'net' | 'exec';
|
|
73
|
+
export type RestrictKind = 'fs' | 'net' | 'exec' | 'mcp';
|
|
87
74
|
export type AssignTarget = {
|
|
88
75
|
t: 'name';
|
|
89
76
|
name: string;
|
|
@@ -101,6 +88,10 @@ interface Base {
|
|
|
101
88
|
}
|
|
102
89
|
export type Stmt = Base & ({
|
|
103
90
|
t: 'let';
|
|
91
|
+
name: string;
|
|
92
|
+
value: Expr;
|
|
93
|
+
} | {
|
|
94
|
+
t: 'assign';
|
|
104
95
|
target: AssignTarget;
|
|
105
96
|
op: '=' | '+=' | '-=' | '*=' | '/=' | '//=' | '%=';
|
|
106
97
|
value: Expr;
|
|
@@ -157,13 +148,15 @@ export type Stmt = Base & ({
|
|
|
157
148
|
value?: Expr;
|
|
158
149
|
} | {
|
|
159
150
|
t: 'constraint';
|
|
160
|
-
kind: '
|
|
151
|
+
kind: 'instruct' | 'avoid';
|
|
161
152
|
text: Expr;
|
|
162
153
|
body: Stmt[];
|
|
163
154
|
} | {
|
|
164
155
|
t: 'restrict';
|
|
165
|
-
|
|
166
|
-
|
|
156
|
+
clauses: {
|
|
157
|
+
kind: RestrictKind;
|
|
158
|
+
arg: Expr;
|
|
159
|
+
}[];
|
|
167
160
|
body: Stmt[];
|
|
168
161
|
});
|
|
169
162
|
export interface Directives {
|
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,15 @@ import * as path from 'node:path';
|
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { CopilotBackend } from './copilot.js';
|
|
6
6
|
import { AuthError, login, logout } from './auth.js';
|
|
7
|
-
import { SplSyntaxError } from './errors.js';
|
|
7
|
+
import { Failure, SplSyntaxError } from './errors.js';
|
|
8
|
+
import { LlmError } from './llm.js';
|
|
8
9
|
import { Logger } from './logger.js';
|
|
9
10
|
import { parse } from './parser.js';
|
|
10
11
|
import { run } from './interpreter.js';
|
|
11
12
|
import { JsonFileStore, MemoryStore, loadStoreModule } from './store.js';
|
|
12
13
|
import { toJson } from './values.js';
|
|
14
|
+
import { loadMcpConfig } from './mcp.js';
|
|
15
|
+
import { RecordingBackend, ReplayBackend } from './recording.js';
|
|
13
16
|
const HELP = `spl - Semantic Prompt Language interpreter
|
|
14
17
|
|
|
15
18
|
Usage: spl <script.spl> [options]
|
|
@@ -18,13 +21,17 @@ Usage: spl <script.spl> [options]
|
|
|
18
21
|
|
|
19
22
|
Options:
|
|
20
23
|
--log-level <level> error | info | debug (default: info). Logs go to stderr.
|
|
24
|
+
All levels include LLM transcripts and [deterministic] statement traces.
|
|
21
25
|
--silent suppress all logs; only the result is printed
|
|
22
26
|
--model <id> model override (takes precedence over @{model=...})
|
|
27
|
+
--mcp <file.json> MCP servers in VS Code mcp.json format
|
|
23
28
|
--store <file.json> persist top-level variables in a JSON file
|
|
24
29
|
--store-module <path> custom variable store: module exporting createStore() -> { load(), save(vars) }
|
|
25
30
|
--timeout <seconds> per LLM call timeout (default: 300)
|
|
31
|
+
--record <file.jsonl> record generation responses (new file; no agents)
|
|
32
|
+
--replay <file.jsonl> offline generation replay; only local reads allowed
|
|
26
33
|
--cwd <dir> base directory for relative paths and restrictions (default: current directory)
|
|
27
|
-
--check
|
|
34
|
+
--check validate script syntax and any --mcp config without executing
|
|
28
35
|
-v, --version print version
|
|
29
36
|
-h, --help print this help
|
|
30
37
|
|
|
@@ -72,9 +79,12 @@ async function main() {
|
|
|
72
79
|
'log-level': { type: 'string' },
|
|
73
80
|
silent: { type: 'boolean' },
|
|
74
81
|
model: { type: 'string' },
|
|
82
|
+
mcp: { type: 'string' },
|
|
75
83
|
store: { type: 'string' },
|
|
76
84
|
'store-module': { type: 'string' },
|
|
77
85
|
timeout: { type: 'string' },
|
|
86
|
+
record: { type: 'string' },
|
|
87
|
+
replay: { type: 'string' },
|
|
78
88
|
cwd: { type: 'string' },
|
|
79
89
|
check: { type: 'boolean' },
|
|
80
90
|
version: { type: 'boolean', short: 'v' },
|
|
@@ -110,6 +120,10 @@ async function main() {
|
|
|
110
120
|
usageError('--timeout must be a positive number');
|
|
111
121
|
if (values.store && values['store-module'])
|
|
112
122
|
usageError('--store and --store-module are mutually exclusive');
|
|
123
|
+
if (values.record && values.replay)
|
|
124
|
+
usageError('--record and --replay are mutually exclusive');
|
|
125
|
+
if (values.replay && (values.store || values['store-module']))
|
|
126
|
+
usageError('--replay cannot use a persistent variable store');
|
|
113
127
|
const cwd = path.resolve(values.cwd ?? process.cwd());
|
|
114
128
|
const file = positionals[0];
|
|
115
129
|
let source;
|
|
@@ -130,6 +144,13 @@ async function main() {
|
|
|
130
144
|
}
|
|
131
145
|
throw e;
|
|
132
146
|
}
|
|
147
|
+
let mcpServers;
|
|
148
|
+
try {
|
|
149
|
+
mcpServers = values.mcp ? loadMcpConfig(values.mcp, cwd) : undefined;
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return usageError(e.message);
|
|
153
|
+
}
|
|
133
154
|
if (values.check)
|
|
134
155
|
return 0;
|
|
135
156
|
let store;
|
|
@@ -139,22 +160,46 @@ async function main() {
|
|
|
139
160
|
catch (e) {
|
|
140
161
|
return usageError(e.message);
|
|
141
162
|
}
|
|
142
|
-
|
|
163
|
+
let backend;
|
|
143
164
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
165
|
+
if (values.replay)
|
|
166
|
+
backend = new ReplayBackend(values.replay);
|
|
167
|
+
else {
|
|
168
|
+
backend = new CopilotBackend({ logger, cwd, timeoutMs: timeoutSec * 1000, mcpServers });
|
|
169
|
+
if (values.record)
|
|
170
|
+
backend = new RecordingBackend(backend, values.record);
|
|
148
171
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
if (!(e instanceof Error))
|
|
175
|
+
throw e;
|
|
176
|
+
return usageError(`cannot open recording: ${e.message}`);
|
|
177
|
+
}
|
|
178
|
+
let result;
|
|
179
|
+
try {
|
|
180
|
+
result = await run(program, { backend, logger, store: values.replay ? undefined : store, cwd, model: values.model });
|
|
154
181
|
}
|
|
155
182
|
finally {
|
|
156
|
-
|
|
183
|
+
try {
|
|
184
|
+
await backend.close();
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
if (!(e instanceof LlmError))
|
|
188
|
+
throw e;
|
|
189
|
+
logger.error(e.message);
|
|
190
|
+
if (result?.status !== 'failure')
|
|
191
|
+
result = { status: 'failure', failure: new Failure('llm_error', e.message).payload() };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (result.status === 'failure') {
|
|
195
|
+
process.stdout.write(JSON.stringify(result.failure, null, 4) + '\n');
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
if (result.hasValue) {
|
|
199
|
+
const v = result.value;
|
|
200
|
+
process.stdout.write((typeof v === 'string' ? v : JSON.stringify(toJson(v), null, 2)) + '\n');
|
|
157
201
|
}
|
|
202
|
+
return 0;
|
|
158
203
|
}
|
|
159
204
|
main().then((code) => process.exit(code), (e) => {
|
|
160
205
|
process.stderr.write(`spl: internal error: ${e.stack ?? e}\n`);
|
package/dist/copilot.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { Failure } from './errors.js';
|
|
|
3
3
|
import { type LlmBackend, type LlmRequest, type LlmResponse } from './llm.js';
|
|
4
4
|
import type { Logger } from './logger.js';
|
|
5
5
|
import type { Restrictions } from './restrict.js';
|
|
6
|
+
import { type McpServers } from './mcp.js';
|
|
6
7
|
export interface CopilotBackendOptions {
|
|
7
8
|
logger: Logger;
|
|
8
9
|
cwd: string;
|
|
9
10
|
timeoutMs: number;
|
|
11
|
+
mcpServers?: McpServers;
|
|
10
12
|
}
|
|
11
13
|
export declare class CopilotBackend implements LlmBackend {
|
|
12
14
|
private readonly opts;
|
|
@@ -14,6 +16,7 @@ export declare class CopilotBackend implements LlmBackend {
|
|
|
14
16
|
private models?;
|
|
15
17
|
constructor(opts: CopilotBackendOptions);
|
|
16
18
|
private getClient;
|
|
19
|
+
private denyGenerationPermission;
|
|
17
20
|
private listModels;
|
|
18
21
|
/** Maps the `think` level to the SDK reasoning effort, warning when the model cannot honor it. */
|
|
19
22
|
private resolveEffort;
|
|
@@ -23,7 +26,7 @@ export declare class CopilotBackend implements LlmBackend {
|
|
|
23
26
|
close(): Promise<void>;
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
29
|
+
* Only runtime tools and explicitly configured MCP tools are approved.
|
|
30
|
+
* Requests that violate a restriction are additionally reported as violations.
|
|
28
31
|
*/
|
|
29
|
-
export declare function decidePermission(r: PermissionRequest, restrictions: Restrictions, block: (f: Failure) => void): PermissionRequestResult;
|
|
32
|
+
export declare function decidePermission(r: PermissionRequest, restrictions: Restrictions, block: (f: Failure) => void, mcpServers?: McpServers): PermissionRequestResult;
|
package/dist/copilot.js
CHANGED
|
@@ -3,10 +3,11 @@ import { loadToken } from './auth.js';
|
|
|
3
3
|
import { Failure } from './errors.js';
|
|
4
4
|
import { LlmError } from './llm.js';
|
|
5
5
|
import { TOOL_SPECS } from './tools.js';
|
|
6
|
+
import { configuredMcpTool, filterMcpServers } from './mcp.js';
|
|
6
7
|
const SYSTEM_MESSAGE = [
|
|
7
8
|
'You are the execution engine of an automated script. Your reply is stored in a variable and used by the program.',
|
|
8
9
|
'Reply with exactly what was asked for: no preamble, no sign-off, no commentary.',
|
|
9
|
-
'Use the provided tools
|
|
10
|
+
'Use the provided runtime tools and configured MCP tools only when the task needs them.',
|
|
10
11
|
].join(' ');
|
|
11
12
|
const TOOL_NAMES = Object.keys(TOOL_SPECS);
|
|
12
13
|
export class CopilotBackend {
|
|
@@ -35,6 +36,11 @@ export class CopilotBackend {
|
|
|
35
36
|
}
|
|
36
37
|
return this.client;
|
|
37
38
|
}
|
|
39
|
+
denyGenerationPermission(block) {
|
|
40
|
+
const failure = new Failure('restriction_violation', 'generation cannot use tools; use an explicit agent operation');
|
|
41
|
+
block(failure);
|
|
42
|
+
return { kind: 'reject', feedback: failure.reason };
|
|
43
|
+
}
|
|
38
44
|
listModels(client) {
|
|
39
45
|
this.models ??= client.listModels().catch((e) => {
|
|
40
46
|
this.opts.logger.debug(`could not list models: ${e.message}`);
|
|
@@ -75,7 +81,8 @@ export class CopilotBackend {
|
|
|
75
81
|
violation ??= failure;
|
|
76
82
|
void session?.abort().catch(() => undefined);
|
|
77
83
|
};
|
|
78
|
-
const
|
|
84
|
+
const agent = req.kind === 'agent';
|
|
85
|
+
const tools = (agent ? TOOL_NAMES : []).map((name) => {
|
|
79
86
|
const spec = TOOL_SPECS[name];
|
|
80
87
|
return defineTool(name, {
|
|
81
88
|
description: spec.description,
|
|
@@ -83,8 +90,6 @@ export class CopilotBackend {
|
|
|
83
90
|
parameters: { type: 'object', properties: spec.properties, required: spec.required, additionalProperties: false },
|
|
84
91
|
handler: async (args) => {
|
|
85
92
|
const params = { ...args };
|
|
86
|
-
if (req.pinned?.tool === name)
|
|
87
|
-
Object.assign(params, req.pinned.params);
|
|
88
93
|
try {
|
|
89
94
|
return await req.tools.invoke(name, params);
|
|
90
95
|
}
|
|
@@ -100,13 +105,15 @@ export class CopilotBackend {
|
|
|
100
105
|
},
|
|
101
106
|
});
|
|
102
107
|
});
|
|
108
|
+
const mcpServers = agent ? filterMcpServers(this.opts.mcpServers ?? {}, req.restrictions) : {};
|
|
103
109
|
const baseConfig = {
|
|
104
110
|
model: req.model,
|
|
105
111
|
tools,
|
|
106
|
-
availableTools: TOOL_NAMES,
|
|
112
|
+
availableTools: [...(agent ? TOOL_NAMES : []), ...Object.keys(mcpServers).map((name) => `mcp:${name}-*`)],
|
|
113
|
+
mcpServers,
|
|
107
114
|
systemMessage: { mode: 'append', content: SYSTEM_MESSAGE },
|
|
108
115
|
workingDirectory: this.opts.cwd,
|
|
109
|
-
onPermissionRequest: (r) => decidePermission(r, req.restrictions, block),
|
|
116
|
+
onPermissionRequest: (r) => agent ? decidePermission(r, req.restrictions, block, mcpServers) : this.denyGenerationPermission(block),
|
|
110
117
|
};
|
|
111
118
|
try {
|
|
112
119
|
try {
|
|
@@ -160,11 +167,17 @@ export class CopilotBackend {
|
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
169
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
170
|
+
* Only runtime tools and explicitly configured MCP tools are approved.
|
|
171
|
+
* Requests that violate a restriction are additionally reported as violations.
|
|
165
172
|
*/
|
|
166
|
-
export function decidePermission(r, restrictions, block) {
|
|
173
|
+
export function decidePermission(r, restrictions, block, mcpServers = {}) {
|
|
167
174
|
try {
|
|
175
|
+
if (r.kind === 'mcp') {
|
|
176
|
+
restrictions.checkMcp(r.serverName, r.toolName);
|
|
177
|
+
if (configuredMcpTool(mcpServers, r.serverName, r.toolName))
|
|
178
|
+
return { kind: 'approve-once' };
|
|
179
|
+
return { kind: 'reject', feedback: 'MCP server/tool is not enabled by --mcp configuration.' };
|
|
180
|
+
}
|
|
168
181
|
if (r.kind === 'custom-tool' && TOOL_NAMES.includes(r.toolName)) {
|
|
169
182
|
return { kind: 'approve-once' }; // the tool itself performs the restriction checks
|
|
170
183
|
}
|
|
@@ -188,5 +201,5 @@ export function decidePermission(r, restrictions, block) {
|
|
|
188
201
|
return { kind: 'reject', feedback: JSON.stringify(e.payload()) };
|
|
189
202
|
}
|
|
190
203
|
}
|
|
191
|
-
return { kind: 'reject', feedback: 'Only
|
|
204
|
+
return { kind: 'reject', feedback: 'Only runtime tools and configured MCP tools may be used.' };
|
|
192
205
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type StatusCode = 'assertion_failed' | 'explicit_failure' | 'invalid_format' | 'restriction_violation' | 'llm_error' | 'tool_error' | 'runtime_error';
|
|
1
|
+
export type StatusCode = 'assertion_failed' | 'explicit_failure' | 'invalid_format' | 'validation_failed' | 'restriction_violation' | 'llm_error' | 'tool_error' | 'runtime_error';
|
|
2
2
|
export interface FailurePayload {
|
|
3
3
|
status: 'failure';
|
|
4
4
|
reason: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { run, type RunOptions, type RunResult } from './interpreter.js';
|
|
|
3
3
|
export { Failure, SplSyntaxError, type FailurePayload, type StatusCode } from './errors.js';
|
|
4
4
|
export { Logger, type LogLevel } from './logger.js';
|
|
5
5
|
export { CopilotBackend } from './copilot.js';
|
|
6
|
+
export { RecordingBackend, ReplayBackend } from './recording.js';
|
|
7
|
+
export { loadMcpConfig, type McpServers } from './mcp.js';
|
|
6
8
|
export { login, logout, authStatus, AuthError } from './auth.js';
|
|
7
9
|
export { JsonFileStore, MemoryStore, loadStoreModule, type VariableStore } from './store.js';
|
|
8
10
|
export type { LlmBackend, LlmRequest, LlmResponse } from './llm.js';
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,7 @@ export { run } from './interpreter.js';
|
|
|
3
3
|
export { Failure, SplSyntaxError } from './errors.js';
|
|
4
4
|
export { Logger } from './logger.js';
|
|
5
5
|
export { CopilotBackend } from './copilot.js';
|
|
6
|
+
export { RecordingBackend, ReplayBackend } from './recording.js';
|
|
7
|
+
export { loadMcpConfig } from './mcp.js';
|
|
6
8
|
export { login, logout, authStatus, AuthError } from './auth.js';
|
|
7
9
|
export { JsonFileStore, MemoryStore, loadStoreModule } from './store.js';
|