shennian 0.3.85 → 0.3.86
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/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +28 -28
- package/dist/src/agents/adapter.js +19 -1
- package/dist/src/agents/claude-managed-permissions.js +248 -1
- package/dist/src/agents/claude.js +393 -8
- package/dist/src/agents/codex-managed-permissions.js +294 -1
- package/dist/src/agents/codex-managed-room-tools.js +249 -1
- package/dist/src/agents/codex-utils.js +200 -7
- package/dist/src/agents/codex.js +1352 -17
- package/dist/src/agents/config-status.js +226 -1
- package/dist/src/agents/cursor-managed-permissions.js +299 -2
- package/dist/src/agents/cursor.js +314 -1
- package/dist/src/agents/custom.js +418 -4
- package/dist/src/agents/external-channel-instructions.js +94 -11
- package/dist/src/agents/gemini.js +180 -1
- package/dist/src/agents/manager.d.ts +2 -0
- package/dist/src/agents/manager.js +250 -18
- package/dist/src/agents/model-registry/cache.js +37 -1
- package/dist/src/agents/model-registry/discovery.js +188 -2
- package/dist/src/agents/model-registry/parsers.js +521 -4
- package/dist/src/agents/model-registry/runner.js +30 -1
- package/dist/src/agents/model-registry/service.js +115 -1
- package/dist/src/agents/model-registry/types.js +8 -1
- package/dist/src/agents/model-registry.js +19 -1
- package/dist/src/agents/native-capabilities.js +237 -1
- package/dist/src/agents/openclaw.js +293 -2
- package/dist/src/agents/opencode-managed-permissions.js +187 -2
- package/dist/src/agents/opencode.js +298 -1
- package/dist/src/agents/platform-instructions.js +46 -17
- package/dist/src/agents/runtime-compatibility.js +44 -1
- package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
- package/dist/src/agents/workbuddy.js +444 -3
- package/dist/src/commands/room.js +2 -2
- package/dist/src/fs/boundary.js +185 -1
- package/dist/src/fs/handler.js +130 -1
- package/dist/src/fs/security.js +32 -1
- package/dist/src/fs/text-decoder.js +110 -1
- package/dist/src/index.js +587 -3
- package/dist/src/manager/prompt.js +34 -29
- package/dist/src/manager/registry.js +317 -1
- package/dist/src/manager/retired-external-channel-compat.js +40 -1
- package/dist/src/manager/runtime.d.ts +4 -0
- package/dist/src/manager/runtime.js +1514 -23
- package/dist/src/native-fusion/codex-parser.js +660 -4
- package/dist/src/native-fusion/codex-user-input.js +102 -5
- package/dist/src/native-fusion/config.js +5 -1
- package/dist/src/native-fusion/file-watcher.js +191 -1
- package/dist/src/native-fusion/local-repository.js +569 -2
- package/dist/src/native-fusion/opencode-parser.js +130 -3
- package/dist/src/native-fusion/parsers.js +140 -3
- package/dist/src/native-fusion/service.js +739 -2
- package/dist/src/native-fusion/session-header.js +101 -2
- package/dist/src/native-fusion/state.js +104 -2
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/personal-sync/machine-authority.js +34 -1
- package/dist/src/room/activation-audit.js +45 -2
- package/dist/src/room/activation-policy.js +30 -1
- package/dist/src/room/activation-service.d.ts +1 -0
- package/dist/src/room/activation-service.js +408 -8
- package/dist/src/room/invite-bootstrap.js +156 -1
- package/dist/src/room/managed-context.d.ts +1 -1
- package/dist/src/room/managed-context.js +1 -1
- package/dist/src/room/managed-prompt-composer.d.ts +1 -1
- package/dist/src/room/managed-prompt-composer.js +6 -5
- package/dist/src/room/managed-room-binding.js +167 -1
- package/dist/src/room/managed-room-ipc.d.ts +2 -2
- package/dist/src/room/managed-room-ipc.js +2 -2
- package/dist/src/room/mirror-sync-service.js +183 -1
- package/dist/src/room/room-action-client.d.ts +11 -0
- package/dist/src/room/room-action-client.js +1 -1
- package/dist/src/room/room-binding-client.d.ts +1 -0
- package/dist/src/room/room-binding-client.js +151 -1
- package/dist/src/security/managed-agent-policy.d.ts +3 -2
- package/dist/src/security/managed-agent-policy.js +1 -1
- package/dist/src/session/archive-zip.js +220 -1
- package/dist/src/session/deletion-sync-state.js +136 -2
- package/dist/src/session/handlers/agent-capabilities.js +14 -1
- package/dist/src/session/handlers/agent-config.js +150 -1
- package/dist/src/session/handlers/agents.js +55 -1
- package/dist/src/session/handlers/chat.d.ts +1 -1
- package/dist/src/session/handlers/chat.js +1030 -2
- package/dist/src/session/handlers/control.js +61 -1
- package/dist/src/session/handlers/fs.js +594 -1
- package/dist/src/session/handlers/message-status.js +15 -1
- package/dist/src/session/handlers/remote-access.js +43 -1
- package/dist/src/session/handlers/room-managed.js +101 -1
- package/dist/src/session/handlers/session-refresh.js +35 -1
- package/dist/src/session/handlers/title.js +79 -1
- package/dist/src/session/handlers/tool-detail.js +149 -1
- package/dist/src/session/handlers/voice-window.js +62 -1
- package/dist/src/session/history-subscriptions.js +470 -1
- package/dist/src/session/manager.js +658 -1
- package/dist/src/session/native-cleanup-outbox.js +287 -2
- package/dist/src/session/native-session-lifecycle.js +327 -6
- package/dist/src/session/queue.d.ts +5 -0
- package/dist/src/session/queue.js +8 -8
- package/dist/src/upgrade/scheduler.js +126 -1
- package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
- package/package.json +1 -1
|
@@ -1,2 +1,293 @@
|
|
|
1
|
-
import{randomUUID
|
|
2
|
-
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { AgentAdapter } from './adapter.js';
|
|
3
|
+
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
4
|
+
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
5
|
+
import { buildInlineChannelPreamble } from './platform-instructions.js';
|
|
6
|
+
/** Best-effort parse of `openclaw agent --json` stdout; shape may vary by version. */
|
|
7
|
+
function extractResponseText(parsed) {
|
|
8
|
+
if (parsed == null)
|
|
9
|
+
return '';
|
|
10
|
+
if (typeof parsed === 'string')
|
|
11
|
+
return parsed;
|
|
12
|
+
if (typeof parsed === 'object') {
|
|
13
|
+
const o = parsed;
|
|
14
|
+
// Primary format: { payloads: [{ text: "..." }] }
|
|
15
|
+
if (Array.isArray(o.payloads)) {
|
|
16
|
+
const parts = [];
|
|
17
|
+
for (const p of o.payloads) {
|
|
18
|
+
if (p && typeof p === 'object') {
|
|
19
|
+
const t = p.text;
|
|
20
|
+
if (typeof t === 'string')
|
|
21
|
+
parts.push(t);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (parts.join('').trim())
|
|
25
|
+
return parts.join('\n');
|
|
26
|
+
}
|
|
27
|
+
const direct = o.response ?? o.message ?? o.text ?? o.output ?? o.content ?? o.result;
|
|
28
|
+
if (typeof direct === 'string' && direct.trim())
|
|
29
|
+
return direct;
|
|
30
|
+
}
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
function extractSessionId(parsed) {
|
|
34
|
+
if (parsed == null || typeof parsed !== 'object')
|
|
35
|
+
return undefined;
|
|
36
|
+
const o = parsed;
|
|
37
|
+
const meta = o.meta;
|
|
38
|
+
const agentMeta = meta?.agentMeta;
|
|
39
|
+
const id = agentMeta?.sessionId ?? o.sessionId ?? o.session_id;
|
|
40
|
+
return typeof id === 'string' && id ? id : undefined;
|
|
41
|
+
}
|
|
42
|
+
function extractUsage(parsed) {
|
|
43
|
+
if (parsed == null || typeof parsed !== 'object')
|
|
44
|
+
return undefined;
|
|
45
|
+
const o = parsed;
|
|
46
|
+
// Primary: meta.agentMeta.usage (openclaw format)
|
|
47
|
+
const meta = o.meta;
|
|
48
|
+
const agentMeta = meta?.agentMeta;
|
|
49
|
+
const u = agentMeta?.usage ?? o.usage;
|
|
50
|
+
if (!u || typeof u !== 'object')
|
|
51
|
+
return undefined;
|
|
52
|
+
const rec = u;
|
|
53
|
+
const inTok = rec.input ?? rec.inputTokens ?? rec.input_tokens;
|
|
54
|
+
const outTok = rec.output ?? rec.outputTokens ?? rec.output_tokens;
|
|
55
|
+
if (typeof inTok !== 'number' && typeof outTok !== 'number')
|
|
56
|
+
return undefined;
|
|
57
|
+
return {
|
|
58
|
+
inputTokens: typeof inTok === 'number' ? inTok : 0,
|
|
59
|
+
outputTokens: typeof outTok === 'number' ? outTok : 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function extractErrorMessage(parsed) {
|
|
63
|
+
if (parsed == null || typeof parsed !== 'object')
|
|
64
|
+
return undefined;
|
|
65
|
+
const o = parsed;
|
|
66
|
+
const err = o.error ?? o.err ?? o.errorMessage;
|
|
67
|
+
if (typeof err === 'string' && err.trim())
|
|
68
|
+
return err;
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
export class OpenClawAdapter extends AgentAdapter {
|
|
72
|
+
type = 'openclaw';
|
|
73
|
+
process = null;
|
|
74
|
+
sessionId = null;
|
|
75
|
+
/** OpenClaw session id from JSON or `resume()`; falls back to app `sessionId`. */
|
|
76
|
+
agentSessionId = null;
|
|
77
|
+
currentModelId = null;
|
|
78
|
+
workDir = null;
|
|
79
|
+
seq = 0;
|
|
80
|
+
runId = '';
|
|
81
|
+
externalChannel = null;
|
|
82
|
+
shennianSessionId = null;
|
|
83
|
+
extraEnv = {};
|
|
84
|
+
configure(options) {
|
|
85
|
+
this.shennianSessionId = options.sessionId ?? null;
|
|
86
|
+
this.externalChannel = options.externalChannel ?? null;
|
|
87
|
+
this.extraEnv = options.env ?? {};
|
|
88
|
+
}
|
|
89
|
+
async start(sessionId, workDir, agentSessionId) {
|
|
90
|
+
this.sessionId = sessionId;
|
|
91
|
+
this.workDir = workDir;
|
|
92
|
+
this.seq = 0;
|
|
93
|
+
if (agentSessionId)
|
|
94
|
+
this.agentSessionId = agentSessionId;
|
|
95
|
+
}
|
|
96
|
+
async send(text, modelId, _reasoningEffort, _attachments, agentSystemPrompt) {
|
|
97
|
+
await this.killProcess();
|
|
98
|
+
this.runId = randomUUID();
|
|
99
|
+
this.seq = 0;
|
|
100
|
+
const sid = this.agentSessionId ?? this.sessionId;
|
|
101
|
+
if (!sid) {
|
|
102
|
+
this.emitEvent({ state: 'error', message: 'openclaw: session not started' });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// OpenClaw `openclaw agent` uses session-scoped model; `--model` on the same CLI may not apply here.
|
|
106
|
+
// Sending `/model <id>` as a message matches interactive `/model` and updates the session before the user turn.
|
|
107
|
+
if (modelId && modelId !== this.currentModelId) {
|
|
108
|
+
const modelSwitch = await this.runAgentCommand([
|
|
109
|
+
'--message',
|
|
110
|
+
`/model ${modelId}`,
|
|
111
|
+
'--session-id',
|
|
112
|
+
sid,
|
|
113
|
+
]);
|
|
114
|
+
if (!modelSwitch.ok) {
|
|
115
|
+
this.emitEvent({
|
|
116
|
+
state: 'error',
|
|
117
|
+
message: modelSwitch.error ?? 'openclaw model switch failed',
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.currentModelId = modelId;
|
|
122
|
+
}
|
|
123
|
+
const outgoing = `${buildInlineChannelPreamble(this.workDir ?? process.cwd(), this.externalChannel, this.shennianSessionId ?? undefined, agentSystemPrompt)}${text}`;
|
|
124
|
+
this.spawnAgent(['--message', outgoing, '--session-id', sid]);
|
|
125
|
+
}
|
|
126
|
+
async resume(agentSessionId) {
|
|
127
|
+
await this.killProcess();
|
|
128
|
+
this.agentSessionId = agentSessionId;
|
|
129
|
+
this.runId = randomUUID();
|
|
130
|
+
this.seq = 0;
|
|
131
|
+
// No new user turn; minimal body so the CLI accepts the invocation.
|
|
132
|
+
this.spawnAgent(['--message', '\u200b', '--session-id', agentSessionId]);
|
|
133
|
+
}
|
|
134
|
+
async stop() {
|
|
135
|
+
await this.killProcess();
|
|
136
|
+
}
|
|
137
|
+
spawnAgent(extraArgs) {
|
|
138
|
+
const spec = resolveBuiltinCommand('openclaw');
|
|
139
|
+
if (!spec) {
|
|
140
|
+
this.emit('error', new Error('Command "openclaw" not found. Is OpenClaw CLI installed?'));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const args = ['agent', '--local', '--json', ...extraArgs];
|
|
144
|
+
const proc = spawnResolvedCommand(spec, args, {
|
|
145
|
+
cwd: this.workDir ?? undefined,
|
|
146
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
147
|
+
env: buildAgentProcessEnv(this.extraEnv),
|
|
148
|
+
});
|
|
149
|
+
this.process = proc;
|
|
150
|
+
let stdoutBuf = '';
|
|
151
|
+
proc.stdout?.on('data', (chunk) => {
|
|
152
|
+
stdoutBuf += chunk.toString();
|
|
153
|
+
});
|
|
154
|
+
let stderrBuf = '';
|
|
155
|
+
proc.stderr?.on('data', (chunk) => {
|
|
156
|
+
stderrBuf += chunk.toString();
|
|
157
|
+
});
|
|
158
|
+
proc.on('close', (code) => {
|
|
159
|
+
if (this.process !== proc)
|
|
160
|
+
return;
|
|
161
|
+
this.process = null;
|
|
162
|
+
if (code !== 0 && code !== null) {
|
|
163
|
+
const msg = stderrBuf.trim() || `openclaw exited with code ${code}`;
|
|
164
|
+
this.emitEvent({ state: 'error', message: msg });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const raw = stdoutBuf.trim();
|
|
168
|
+
if (!raw) {
|
|
169
|
+
const msg = stderrBuf.trim() || 'openclaw returned empty stdout';
|
|
170
|
+
this.emitEvent({ state: 'error', message: msg });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let parsed;
|
|
174
|
+
try {
|
|
175
|
+
parsed = JSON.parse(raw);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
this.emitEvent({
|
|
179
|
+
state: 'error',
|
|
180
|
+
message: `openclaw stdout is not valid JSON: ${raw.slice(0, 200)}`,
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const rec = parsed;
|
|
185
|
+
const failed = rec.ok === false || rec.success === false;
|
|
186
|
+
if (failed) {
|
|
187
|
+
this.emitEvent({
|
|
188
|
+
state: 'error',
|
|
189
|
+
message: extractErrorMessage(parsed) ?? 'openclaw request failed',
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const nextSid = extractSessionId(parsed);
|
|
194
|
+
if (nextSid)
|
|
195
|
+
this.agentSessionId = nextSid;
|
|
196
|
+
const text = extractResponseText(parsed);
|
|
197
|
+
if (text) {
|
|
198
|
+
this.emitEvent({ state: 'delta', text });
|
|
199
|
+
}
|
|
200
|
+
const usage = extractUsage(parsed);
|
|
201
|
+
this.emitEvent({ state: 'final', agentSessionId: this.agentSessionId ?? undefined, usage });
|
|
202
|
+
});
|
|
203
|
+
proc.on('error', (err) => {
|
|
204
|
+
if (this.process !== proc)
|
|
205
|
+
return;
|
|
206
|
+
this.process = null;
|
|
207
|
+
if (err.code === 'ENOENT') {
|
|
208
|
+
this.emit('error', new Error('Command "openclaw" not found. Is OpenClaw CLI installed?'));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
this.emit('error', err);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async runAgentCommand(extraArgs) {
|
|
216
|
+
return new Promise((resolve) => {
|
|
217
|
+
const spec = resolveBuiltinCommand('openclaw');
|
|
218
|
+
if (!spec) {
|
|
219
|
+
resolve({ ok: false, error: 'Command "openclaw" not found. Is OpenClaw CLI installed?' });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const args = ['agent', '--local', '--json', ...extraArgs];
|
|
223
|
+
const proc = spawnResolvedCommand(spec, args, {
|
|
224
|
+
cwd: this.workDir ?? undefined,
|
|
225
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
226
|
+
env: buildAgentProcessEnv(this.extraEnv),
|
|
227
|
+
});
|
|
228
|
+
let stdoutBuf = '';
|
|
229
|
+
let stderrBuf = '';
|
|
230
|
+
proc.stdout?.on('data', (chunk) => {
|
|
231
|
+
stdoutBuf += chunk.toString();
|
|
232
|
+
});
|
|
233
|
+
proc.stderr?.on('data', (chunk) => {
|
|
234
|
+
stderrBuf += chunk.toString();
|
|
235
|
+
});
|
|
236
|
+
proc.on('close', (code) => {
|
|
237
|
+
if (code !== 0 && code !== null) {
|
|
238
|
+
resolve({ ok: false, error: stderrBuf.trim() || `openclaw exited with code ${code}` });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const raw = stdoutBuf.trim();
|
|
242
|
+
if (!raw) {
|
|
243
|
+
resolve({ ok: true });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(raw);
|
|
248
|
+
if (parsed.ok === false || parsed.success === false) {
|
|
249
|
+
resolve({
|
|
250
|
+
ok: false,
|
|
251
|
+
error: extractErrorMessage(parsed) ?? 'openclaw request failed',
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const nextSid = extractSessionId(parsed);
|
|
256
|
+
if (nextSid)
|
|
257
|
+
this.agentSessionId = nextSid;
|
|
258
|
+
resolve({ ok: true, parsed });
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
resolve({ ok: false, error: `openclaw stdout is not valid JSON: ${raw.slice(0, 200)}` });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
proc.on('error', (err) => {
|
|
265
|
+
resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
emitEvent(partial) {
|
|
270
|
+
const event = {
|
|
271
|
+
...partial,
|
|
272
|
+
runId: this.runId,
|
|
273
|
+
seq: this.seq++,
|
|
274
|
+
};
|
|
275
|
+
this.emit('agentEvent', event);
|
|
276
|
+
}
|
|
277
|
+
async killProcess() {
|
|
278
|
+
const proc = this.process;
|
|
279
|
+
if (!proc)
|
|
280
|
+
return;
|
|
281
|
+
this.process = null;
|
|
282
|
+
proc.kill('SIGTERM');
|
|
283
|
+
await new Promise((resolve) => {
|
|
284
|
+
proc.on('close', resolve);
|
|
285
|
+
setTimeout(() => {
|
|
286
|
+
proc.kill('SIGKILL');
|
|
287
|
+
resolve();
|
|
288
|
+
}, 3000);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// OpenClaw support is intentionally disabled.
|
|
293
|
+
// registerAgent('openclaw', () => new OpenClawAdapter())
|
|
@@ -1,2 +1,187 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// @arch docs/features/managed-agent-restricted-runtime.md
|
|
2
|
+
// @test src/__tests__/opencode-managed-permissions.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import fsPromises from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { resolveProtectedShennianDirectories } from '../security/managed-agent-credential-paths.js';
|
|
8
|
+
const OPENCODE_MANAGED_CONFIG_PREFIX = 'shennian-opencode-managed-';
|
|
9
|
+
const BASE_SENSITIVE_PATH_RULES = [
|
|
10
|
+
'.env',
|
|
11
|
+
'.env.*',
|
|
12
|
+
'**/.env',
|
|
13
|
+
'**/.env.*',
|
|
14
|
+
'*.key',
|
|
15
|
+
'**/*.key',
|
|
16
|
+
'*.pem',
|
|
17
|
+
'**/*.pem',
|
|
18
|
+
'~/.ssh/**',
|
|
19
|
+
'$HOME/.ssh/**',
|
|
20
|
+
'~/.aws/**',
|
|
21
|
+
'$HOME/.aws/**',
|
|
22
|
+
'~/.azure/**',
|
|
23
|
+
'$HOME/.azure/**',
|
|
24
|
+
'~/.config/gcloud/**',
|
|
25
|
+
'$HOME/.config/gcloud/**',
|
|
26
|
+
'~/.netrc',
|
|
27
|
+
'$HOME/.netrc',
|
|
28
|
+
'~/.npmrc',
|
|
29
|
+
'$HOME/.npmrc',
|
|
30
|
+
'~/.shennian/**',
|
|
31
|
+
'$HOME/.shennian/**',
|
|
32
|
+
];
|
|
33
|
+
const supportedExternalCapabilities = {
|
|
34
|
+
// OpenCode permissions can deny Bash completely, but they cannot constrain
|
|
35
|
+
// Bash or child processes to a filesystem/network sandbox. Enabling command
|
|
36
|
+
// networking is therefore unavailable in managed mode.
|
|
37
|
+
commandNetwork: { allow: false, deny: true },
|
|
38
|
+
webSearch: { allow: true, deny: true },
|
|
39
|
+
browser: { allow: false, deny: true },
|
|
40
|
+
computerUse: { allow: false, deny: true },
|
|
41
|
+
// MCP tool names are server-defined. There is no safe generic allow rule
|
|
42
|
+
// that excludes project/global custom tools, so managed mode keeps MCP off.
|
|
43
|
+
mcp: { allow: false, deny: true },
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Evidence is intentionally limited to the locally exercised 1.18 release
|
|
47
|
+
* line. The declaration is partial: it describes tool dispatch checks, not an
|
|
48
|
+
* OS sandbox. Shell execution is disabled rather than presented as isolated.
|
|
49
|
+
*/
|
|
50
|
+
export const OPENCODE_MANAGED_RUNTIME_SECURITY = {
|
|
51
|
+
minimumRuntimeVersion: '1.18.0',
|
|
52
|
+
maximumRuntimeVersionExclusive: '1.19.0',
|
|
53
|
+
platforms: Object.fromEntries(['darwin', 'linux', 'wsl', 'win32'].map((platform) => [
|
|
54
|
+
platform,
|
|
55
|
+
{
|
|
56
|
+
level: 'partial',
|
|
57
|
+
fileAccess: {
|
|
58
|
+
'workspace-read': true,
|
|
59
|
+
'workspace-write': true,
|
|
60
|
+
'host-full': true,
|
|
61
|
+
},
|
|
62
|
+
externalCapabilities: supportedExternalCapabilities,
|
|
63
|
+
knownGaps: [
|
|
64
|
+
'sandbox.toolPermissionOnly',
|
|
65
|
+
'shell.unavailableInManagedMode',
|
|
66
|
+
'config.globalAndAdministratorSourcesMayRemain',
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
])),
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Compiles the effective policy into an invocation-scoped OpenCode config.
|
|
73
|
+
* The global deny rule is deliberate: unknown/custom tools stay disabled.
|
|
74
|
+
*/
|
|
75
|
+
export function compileOpenCodeManagedPermissions(enforcement, resolvedWorkDir) {
|
|
76
|
+
validateResolvedWorkDir(resolvedWorkDir);
|
|
77
|
+
const policy = enforcement.effectivePolicy;
|
|
78
|
+
if (policy.externalCapabilities.commandNetwork) {
|
|
79
|
+
throw new Error('opencode_managed_command_network_unavailable');
|
|
80
|
+
}
|
|
81
|
+
if (policy.externalCapabilities.browser || policy.externalCapabilities.computerUse) {
|
|
82
|
+
throw new Error('opencode_managed_interactive_tools_unavailable');
|
|
83
|
+
}
|
|
84
|
+
if (policy.externalCapabilities.mcp) {
|
|
85
|
+
throw new Error('opencode_managed_mcp_allowlist_unavailable');
|
|
86
|
+
}
|
|
87
|
+
const hostFull = policy.fileAccess === 'host-full';
|
|
88
|
+
const workspaceWritable = policy.fileAccess === 'workspace-write';
|
|
89
|
+
const sensitivePathRules = resolveSensitivePathRules();
|
|
90
|
+
const permission = {
|
|
91
|
+
'*': 'deny',
|
|
92
|
+
read: pathPermission('allow', sensitivePathRules),
|
|
93
|
+
glob: 'allow',
|
|
94
|
+
grep: 'allow',
|
|
95
|
+
list: 'allow',
|
|
96
|
+
edit: workspaceWritable || hostFull ? pathPermission('allow', sensitivePathRules) : 'deny',
|
|
97
|
+
external_directory: hostFull ? pathPermission('allow', sensitivePathRules) : 'deny',
|
|
98
|
+
bash: 'deny',
|
|
99
|
+
task: 'deny',
|
|
100
|
+
skill: 'deny',
|
|
101
|
+
question: 'allow',
|
|
102
|
+
todoread: 'allow',
|
|
103
|
+
todowrite: 'allow',
|
|
104
|
+
webfetch: policy.externalCapabilities.webSearch ? 'allow' : 'deny',
|
|
105
|
+
websearch: policy.externalCapabilities.webSearch ? 'allow' : 'deny',
|
|
106
|
+
codesearch: policy.externalCapabilities.webSearch ? 'allow' : 'deny',
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
args: ['--pure', '--auto'],
|
|
110
|
+
config: {
|
|
111
|
+
$schema: 'https://opencode.ai/config.json',
|
|
112
|
+
default_agent: 'build',
|
|
113
|
+
share: 'disabled',
|
|
114
|
+
subagent_depth: 0,
|
|
115
|
+
autoupdate: false,
|
|
116
|
+
formatter: false,
|
|
117
|
+
lsp: false,
|
|
118
|
+
permission,
|
|
119
|
+
agent: {
|
|
120
|
+
build: { permission },
|
|
121
|
+
},
|
|
122
|
+
plugin: [],
|
|
123
|
+
mcp: {},
|
|
124
|
+
command: {},
|
|
125
|
+
instructions: [],
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function createOpenCodeManagedConfigDirectory(config) {
|
|
130
|
+
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), OPENCODE_MANAGED_CONFIG_PREFIX));
|
|
131
|
+
const configPath = path.join(directory, 'opencode.json');
|
|
132
|
+
try {
|
|
133
|
+
await fsPromises.chmod(directory, 0o700);
|
|
134
|
+
await fsPromises.writeFile(configPath, `${JSON.stringify(config.config)}\n`, {
|
|
135
|
+
encoding: 'utf8',
|
|
136
|
+
mode: 0o600,
|
|
137
|
+
flag: 'wx',
|
|
138
|
+
});
|
|
139
|
+
if (process.platform !== 'win32')
|
|
140
|
+
await fsPromises.chmod(configPath, 0o600);
|
|
141
|
+
return { directory, configPath };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
await disposeOpenCodeManagedConfigDirectory(directory).catch(() => { });
|
|
145
|
+
throw new Error('opencode_managed_config_initialization_failed', { cause: error });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function disposeOpenCodeManagedConfigDirectory(directory) {
|
|
149
|
+
const resolved = path.resolve(directory);
|
|
150
|
+
const tempRoot = path.resolve(os.tmpdir());
|
|
151
|
+
if (path.dirname(resolved) !== tempRoot ||
|
|
152
|
+
!path.basename(resolved).startsWith(OPENCODE_MANAGED_CONFIG_PREFIX)) {
|
|
153
|
+
throw new Error('opencode_managed_config_cleanup_refused');
|
|
154
|
+
}
|
|
155
|
+
await fsPromises.rm(resolved, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
function pathPermission(defaultEffect, sensitivePathRules) {
|
|
158
|
+
return {
|
|
159
|
+
'*': defaultEffect,
|
|
160
|
+
...Object.fromEntries(sensitivePathRules.map((pattern) => [pattern, 'deny'])),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function normalizeForOpenCode(value) {
|
|
164
|
+
return value.replaceAll('\\', '/');
|
|
165
|
+
}
|
|
166
|
+
function resolveSensitivePathRules() {
|
|
167
|
+
const shennianDirectories = resolveProtectedShennianDirectories()
|
|
168
|
+
.map((entry) => `${normalizeForOpenCode(path.normalize(entry))}/**`);
|
|
169
|
+
return [...new Set([...BASE_SENSITIVE_PATH_RULES, ...shennianDirectories])];
|
|
170
|
+
}
|
|
171
|
+
function validateResolvedWorkDir(value) {
|
|
172
|
+
const normalized = path.normalize(value.trim());
|
|
173
|
+
if (!normalized || !path.isAbsolute(normalized)) {
|
|
174
|
+
throw new Error('opencode_managed_workdir_invalid');
|
|
175
|
+
}
|
|
176
|
+
let realPath;
|
|
177
|
+
try {
|
|
178
|
+
realPath = fs.realpathSync(normalized);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
throw new Error('opencode_managed_workdir_unavailable');
|
|
182
|
+
}
|
|
183
|
+
if (path.normalize(realPath) !== normalized) {
|
|
184
|
+
throw new Error('opencode_managed_workdir_not_realpath');
|
|
185
|
+
}
|
|
186
|
+
return normalized;
|
|
187
|
+
}
|