@sagentlab/navarch-runtime 0.1.118 → 0.1.120
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/bin/worktree-guard-hook.cjs +72 -1
- package/dist/adapters/opencode.cjs +77 -30
- package/package.json +1 -1
|
@@ -214,6 +214,73 @@ function isInsideOpeningQuote(command, start) {
|
|
|
214
214
|
return beforeQuote === undefined || /[\s=(<>;|&]/.test(beforeQuote);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Returns [open, close, segmentStart] for the quoted word containing `index`,
|
|
219
|
+
* or null when it sits outside quotes or the quote state cannot be trusted.
|
|
220
|
+
* `segmentStart` follows the last unquoted, unescaped `;`, `|`, `&` or newline
|
|
221
|
+
* before the word, so a separator inside quotes never starts a segment. Only
|
|
222
|
+
* plain single and double quotes are tracked. Constructs that nest or hide
|
|
223
|
+
* quotes (backticks, `$(`, `$'`, heredocs, shell comments) make the scan bail
|
|
224
|
+
* out, so a misread quote never exempts a token.
|
|
225
|
+
*/
|
|
226
|
+
function quotedSpanAt(command, index) {
|
|
227
|
+
let quote = "";
|
|
228
|
+
let open = -1;
|
|
229
|
+
let segmentStart = 0;
|
|
230
|
+
for (let i = 0; i < command.length; i++) {
|
|
231
|
+
const ch = command[i];
|
|
232
|
+
if (quote === "'") {
|
|
233
|
+
if (ch === "'") {
|
|
234
|
+
if (index > open && index < i) return [open, i, segmentStart];
|
|
235
|
+
quote = "";
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (ch === "`" || (ch === "$" && (command[i + 1] === "(" || command[i + 1] === "'"))) return null;
|
|
240
|
+
if (ch === "\\") {
|
|
241
|
+
i++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (quote === '"') {
|
|
245
|
+
if (ch === '"') {
|
|
246
|
+
if (index > open && index < i) return [open, i, segmentStart];
|
|
247
|
+
quote = "";
|
|
248
|
+
}
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (/[;|&\n]/.test(ch)) segmentStart = i + 1;
|
|
252
|
+
if (ch === "<" && command[i + 1] === "<") return null;
|
|
253
|
+
if (ch === "#" && (i === 0 || /[\s;|&(]/.test(command[i - 1]))) return null;
|
|
254
|
+
if (ch === "'" || ch === '"') {
|
|
255
|
+
quote = ch;
|
|
256
|
+
open = i;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* True for the jq alternative operator (`//`, `//=`) used as its own word inside
|
|
264
|
+
* a quoted jq program: `jq '.a // "x"' f.json`, `gh ... --jq '(.x) // "none"'`.
|
|
265
|
+
* Narrow on purpose. The token must be whitespace-delimited, sit inside a
|
|
266
|
+
* plain-quoted word in a shell segment whose command word is `jq`, or is `gh`
|
|
267
|
+
* with the word given to `--jq`/`-q`. Unquoted tokens (`find // -x`),
|
|
268
|
+
* quoted bare roots (`ls "//"`) and `//` in other quoted text stay screened.
|
|
269
|
+
*/
|
|
270
|
+
function isJqAlternativeOperator(command, start, token) {
|
|
271
|
+
if (token !== "//" && token !== "//=") return false;
|
|
272
|
+
const next = command[start + token.length];
|
|
273
|
+
if (!/\s/.test(command[start - 1] ?? "") || next === undefined || !/\s/.test(next)) return false;
|
|
274
|
+
const span = quotedSpanAt(command, start);
|
|
275
|
+
if (!span) return false;
|
|
276
|
+
const segment = command.slice(span[2], span[0]);
|
|
277
|
+
const commandWord = /^\s*\(?\s*(?:[A-Za-z_]\w*=\S*\s+)*/;
|
|
278
|
+
if (new RegExp(commandWord.source + "jq\\s").test(segment)) return true;
|
|
279
|
+
return (
|
|
280
|
+
new RegExp(commandWord.source + "gh\\s").test(segment) && /\s(?:--jq|-q)(?:=|\s+)$/.test(segment)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
217
284
|
/**
|
|
218
285
|
* True when a "/"-leading token is comment or markup text rather than a path:
|
|
219
286
|
* a closing tag (`</p>`), a block-comment opener (`/**`, `/*`), or a line
|
|
@@ -222,12 +289,16 @@ function isInsideOpeningQuote(command, start) {
|
|
|
222
289
|
* checked, `/*` and `//` only count at a comment position (line start, right
|
|
223
290
|
* after an opening quote, or after `;`) so `rm -rf /*`, `find // -name x`
|
|
224
291
|
* and `rm -rf "" /*` stay blocked, and a block comment needs a closing
|
|
225
|
-
* `*` + `/` later in the text.
|
|
292
|
+
* `*` + `/` later in the text. The jq alternative operator inside a quoted jq
|
|
293
|
+
* program is exempt under the separate, narrower rule in
|
|
294
|
+
* isJqAlternativeOperator.
|
|
226
295
|
*/
|
|
227
296
|
function isCommentOrMarkupToken(command, start, token) {
|
|
228
297
|
const end = start + token.length;
|
|
229
298
|
const next = command[end];
|
|
230
299
|
|
|
300
|
+
if (isJqAlternativeOperator(command, start, token)) return true;
|
|
301
|
+
|
|
231
302
|
if (command[start - 1] === "<" && next === ">" && /^\/[A-Za-z][\w:.-]*$/.test(token)) {
|
|
232
303
|
return !fs.existsSync(token);
|
|
233
304
|
}
|
|
@@ -13,11 +13,18 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
13
13
|
const config_cjs_1 = require("../config.cjs");
|
|
14
14
|
const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
15
15
|
/**
|
|
16
|
-
* Headless OpenCode adapter, fixture-pinned to the OpenCode JSON
|
|
17
|
-
* contract shared by V1 and V2 (verified against 2.0.6):
|
|
16
|
+
* Headless OpenCode adapter, fixture-pinned to the OpenCode JSON event
|
|
17
|
+
* contract shared by V1 and V2 (verified against 1.19.1, 2.0.6 and 2.0.17):
|
|
18
18
|
*
|
|
19
19
|
* opencode run --format json [--model provider/model[#variant]] < prompt
|
|
20
20
|
*
|
|
21
|
+
* V1 (still npm's `latest`) differs in two ways the adapter must follow: its
|
|
22
|
+
* config takes V1 `permission` rules and flat `mcp` entries, and it rejects
|
|
23
|
+
* V2's `permissions`/`mcp.servers` outright ("V2 permissions are not
|
|
24
|
+
* supported by OpenCode V1"); and it takes the reasoning variant as
|
|
25
|
+
* `--variant`, reporting `model#variant` as an unknown model. A `--version`
|
|
26
|
+
* probe picks the dialect per session, defaulting to V2.
|
|
27
|
+
*
|
|
21
28
|
* The prompt goes in on stdin, which `opencode run` reads as the message when
|
|
22
29
|
* none is given (verified against 2.0.17). Linux rejects any single argv
|
|
23
30
|
* string over 128 KiB (`spawn E2BIG`), and an argv prompt also let an
|
|
@@ -45,16 +52,20 @@ async function runOpenCodeAdapter(options) {
|
|
|
45
52
|
// supervisor's PWD in the inherited environment (often the home directory).
|
|
46
53
|
if (!options.dockerExec && options.cwd)
|
|
47
54
|
env.PWD = node_path_1.default.resolve(options.cwd);
|
|
48
|
-
|
|
55
|
+
const probe = (probeArgs) => {
|
|
56
|
+
const probeOptions = { ...options, env, timeoutMs: 10_000, onProcessStarted: undefined };
|
|
57
|
+
return options.dockerExec ? runViaDocker(probeOptions, probeArgs) : runOnHost(probeOptions, probeArgs);
|
|
58
|
+
};
|
|
59
|
+
// Neither generation loads config for --version, so the probe runs before
|
|
60
|
+
// the config is written in the matching dialect.
|
|
61
|
+
const generation = openCodeGeneration(await probe(["--version"]));
|
|
62
|
+
await prepareOpenCodeConfig(options.mcpConfigPath, env, Boolean(options.dockerExec), generation);
|
|
49
63
|
// V2's default background service owns its own cwd, environment and MCP
|
|
50
64
|
// configuration. A client process's isolated config does not isolate that
|
|
51
65
|
// service. Probe help without starting a turn, so older in-process CLIs
|
|
52
66
|
// remain supported and upgrades are detected on the next session.
|
|
53
67
|
const runOptions = { ...options, env };
|
|
54
|
-
const
|
|
55
|
-
const help = options.dockerExec
|
|
56
|
-
? await runViaDocker(probeOptions, ["run", "--help"])
|
|
57
|
-
: await runOnHost(probeOptions, ["run", "--help"]);
|
|
68
|
+
const help = await probe(["run", "--help"]);
|
|
58
69
|
if (help.exitCode !== 0 || help.timedOut || help.killedByLeaseLoss) {
|
|
59
70
|
return { ...help, stderr: `OpenCode CLI capability check failed. ${help.stderr}` };
|
|
60
71
|
}
|
|
@@ -76,10 +87,18 @@ async function runOpenCodeAdapter(options) {
|
|
|
76
87
|
if (options.model && options.model !== "default" && !hasArg(options.extraArgs, "--model", "-m")) {
|
|
77
88
|
// OpenCode V2 folds the reasoning variant into the model reference
|
|
78
89
|
// (`provider/model#variant`) and hard-fails on a variant the model does
|
|
79
|
-
// not advertise
|
|
80
|
-
// advertised variants, so a present value is
|
|
81
|
-
// use the model's own default.
|
|
82
|
-
|
|
90
|
+
// not advertise; V1 takes it as `--variant`. The claim route clamps the
|
|
91
|
+
// effort to the model's advertised variants, so a present value is
|
|
92
|
+
// always valid; absent means use the model's own default.
|
|
93
|
+
if (generation === 1) {
|
|
94
|
+
args.push("--model", options.model);
|
|
95
|
+
if (options.reasoningEffort && !hasArg(options.extraArgs, "--variant")) {
|
|
96
|
+
args.push("--variant", options.reasoningEffort);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
args.push("--model", options.reasoningEffort ? `${options.model}#${options.reasoningEffort}` : options.model);
|
|
101
|
+
}
|
|
83
102
|
}
|
|
84
103
|
const run = (runArgs) => runOptions.dockerExec
|
|
85
104
|
? runViaDocker(runOptions, runArgs, options.prompt)
|
|
@@ -110,10 +129,20 @@ function isLogLevelRejection(stderr) {
|
|
|
110
129
|
function resetOpenCodeLogLevelCache() {
|
|
111
130
|
openCodeLogLevels.clear();
|
|
112
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* V1 prints a bare `1.19.1`, V2 `opencode v2.0.17`. Anything unrecognized
|
|
134
|
+
* keeps the V2 dialect the adapter has always written.
|
|
135
|
+
*/
|
|
136
|
+
function openCodeGeneration(version) {
|
|
137
|
+
if (version.exitCode !== 0)
|
|
138
|
+
return 2;
|
|
139
|
+
const major = version.stdout.match(/^\s*(?:opencode\s+)?v?(\d+)\.\d+\.\d+/i)?.[1];
|
|
140
|
+
return major !== undefined && Number(major) < 2 ? 1 : 2;
|
|
141
|
+
}
|
|
113
142
|
function hasArg(args, ...flags) {
|
|
114
143
|
return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
115
144
|
}
|
|
116
|
-
async function prepareOpenCodeConfig(mcpConfigPath, env, inDocker) {
|
|
145
|
+
async function prepareOpenCodeConfig(mcpConfigPath, env, inDocker, generation) {
|
|
117
146
|
if (!mcpConfigPath) {
|
|
118
147
|
throw new Error("OpenCode requires a per-session MCP config path for isolated execution.");
|
|
119
148
|
}
|
|
@@ -143,28 +172,46 @@ async function prepareOpenCodeConfig(mcpConfigPath, env, inDocker) {
|
|
|
143
172
|
mcp[name] = {
|
|
144
173
|
type: "remote",
|
|
145
174
|
url,
|
|
146
|
-
disabled: false,
|
|
175
|
+
...(generation === 1 ? { enabled: true } : { disabled: false }),
|
|
147
176
|
oauth: false,
|
|
148
177
|
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
149
178
|
};
|
|
150
179
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
const managedDirectories = await openCodeManagedDirectories(env, inDocker);
|
|
181
|
+
// Same policy in both dialects; in each the last matching rule wins.
|
|
182
|
+
const config = generation === 1
|
|
183
|
+
? {
|
|
184
|
+
$schema: "https://opencode.ai/config.json",
|
|
185
|
+
autoupdate: false,
|
|
186
|
+
share: "disabled",
|
|
187
|
+
plugin: [],
|
|
188
|
+
mcp,
|
|
189
|
+
permission: {
|
|
190
|
+
"*": "allow",
|
|
191
|
+
external_directory: {
|
|
192
|
+
"*": "deny",
|
|
193
|
+
...Object.fromEntries(managedDirectories.map((resource) => [resource, "allow"])),
|
|
194
|
+
},
|
|
195
|
+
question: "deny",
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
: {
|
|
199
|
+
$schema: "https://opencode.ai/config.json",
|
|
200
|
+
update: "disable",
|
|
201
|
+
share: "disabled",
|
|
202
|
+
plugins: [],
|
|
203
|
+
mcp: { servers: mcp },
|
|
204
|
+
permissions: [
|
|
205
|
+
{ action: "*", resource: "*", effect: "allow" },
|
|
206
|
+
{ action: "external_directory", resource: "*", effect: "deny" },
|
|
207
|
+
...managedDirectories.map((resource) => ({
|
|
208
|
+
action: "external_directory",
|
|
209
|
+
resource,
|
|
210
|
+
effect: "allow",
|
|
211
|
+
})),
|
|
212
|
+
{ action: "question", resource: "*", effect: "deny" },
|
|
213
|
+
],
|
|
214
|
+
};
|
|
168
215
|
const configPath = `${mcpConfigPath}.opencode.json`;
|
|
169
216
|
const configDir = `${mcpConfigPath}.opencode-config`;
|
|
170
217
|
const xdgConfigHome = `${mcpConfigPath}.opencode-xdg`;
|
package/package.json
CHANGED