@ravefox/space 0.2.1 → 0.4.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/LICENSE +21 -205
- package/README.md +104 -31
- package/bin/space.js +113 -0
- package/lib/agents.js +181 -0
- package/lib/boardTools.js +89 -0
- package/lib/credentials.js +95 -0
- package/lib/daemon.js +294 -0
- package/lib/harness.js +166 -0
- package/lib/http.js +101 -0
- package/lib/login.js +68 -0
- package/lib/mcp.js +39 -0
- package/lib/runtime.js +253 -0
- package/lib/store.js +96 -0
- package/lib/tools.js +93 -0
- package/lib/workspaceSandbox.js +77 -0
- package/lib/workspaceTools.js +160 -0
- package/lib/workspaceWorker.js +17 -0
- package/package.json +27 -46
- package/NOTICE +0 -55
- package/assets/webhook-sdk/python/bot.py +0 -63
- package/assets/webhook-sdk/python/commonly.py +0 -232
- package/node_modules/@commonlyai/seats/README.md +0 -25
- package/node_modules/@commonlyai/seats/package.json +0 -22
- package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
- package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
- package/node_modules/@commonlyai/seats/src/contract.js +0 -48
- package/node_modules/@commonlyai/seats/src/failures.js +0 -44
- package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
- package/node_modules/@commonlyai/seats/src/index.js +0 -22
- package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
- package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
- package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
- package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
- package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
- package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
- package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
- package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
- package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
- package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
- package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
- package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
- package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
- package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
- package/skills/commonly/SKILL.md +0 -233
- package/src/commands/agent.js +0 -6334
- package/src/commands/daemon.js +0 -933
- package/src/commands/dev.js +0 -153
- package/src/commands/doctor.js +0 -141
- package/src/commands/inbox.js +0 -330
- package/src/commands/login.js +0 -194
- package/src/commands/mcp.js +0 -56
- package/src/commands/pod.js +0 -120
- package/src/commands/task.js +0 -279
- package/src/commands/verify.js +0 -48
- package/src/index.js +0 -99
- package/src/lib/adapter-login.js +0 -76
- package/src/lib/adapters/broker-bridge-entry.js +0 -76
- package/src/lib/adapters/claude.js +0 -2207
- package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
- package/src/lib/adapters/codex.js +0 -1898
- package/src/lib/adapters/dsh.js +0 -513
- package/src/lib/adapters/goose.js +0 -510
- package/src/lib/adapters/hermes.js +0 -18
- package/src/lib/adapters/index.js +0 -68
- package/src/lib/adapters/opencode.js +0 -1681
- package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
- package/src/lib/adapters/pi-mcp-client.mjs +0 -617
- package/src/lib/adapters/pi-shell-env.mjs +0 -41
- package/src/lib/adapters/pi.js +0 -1025
- package/src/lib/adapters/qwen.js +0 -523
- package/src/lib/adapters/seat-model.js +0 -8
- package/src/lib/adapters/stub.js +0 -1
- package/src/lib/adapters/tested-version.js +0 -106
- package/src/lib/api.js +0 -205
- package/src/lib/claim-outcome.js +0 -128
- package/src/lib/claimed-work-gate.js +0 -73
- package/src/lib/cli-hint.js +0 -33
- package/src/lib/commit-attribution.js +0 -391
- package/src/lib/config.js +0 -155
- package/src/lib/content-refusal.js +0 -94
- package/src/lib/credential-file.js +0 -208
- package/src/lib/daemon-agent-states.js +0 -95
- package/src/lib/daemon-hint.js +0 -18
- package/src/lib/daemon-logs.js +0 -43
- package/src/lib/daemon-service-windows.js +0 -263
- package/src/lib/daemon-service.js +0 -486
- package/src/lib/daemon-state.js +0 -84
- package/src/lib/daemon-store.js +0 -119
- package/src/lib/daemon-supervisor.js +0 -629
- package/src/lib/daemon-watch.js +0 -115
- package/src/lib/declared-mcp-guard.js +0 -284
- package/src/lib/default-environment.js +0 -207
- package/src/lib/device-login.js +0 -126
- package/src/lib/digest-frame.js +0 -151
- package/src/lib/doctor.js +0 -291
- package/src/lib/egress-proxy.js +0 -160
- package/src/lib/enforcement.js +0 -946
- package/src/lib/environment.js +0 -681
- package/src/lib/first-output-watchdog.js +0 -191
- package/src/lib/git-readonly-server.js +0 -138
- package/src/lib/git-readonly.js +0 -463
- package/src/lib/git-remote-commonly.mjs +0 -100
- package/src/lib/grant-broker-guard.js +0 -345
- package/src/lib/hooks-config.js +0 -238
- package/src/lib/host-gh-broker.js +0 -152
- package/src/lib/host-gh-shim.mjs +0 -98
- package/src/lib/inbox.js +0 -483
- package/src/lib/instance-proxy.js +0 -76
- package/src/lib/machine-host.js +0 -29
- package/src/lib/mcp-credential-delivery.js +0 -121
- package/src/lib/mcp-first-run.js +0 -50
- package/src/lib/mcp-home.js +0 -644
- package/src/lib/mcp-ready.js +0 -68
- package/src/lib/mcp-server-version.js +0 -123
- package/src/lib/mcp-token.js +0 -36
- package/src/lib/mcp-warm-child.mjs +0 -412
- package/src/lib/memory-bridge.js +0 -191
- package/src/lib/memory-import.js +0 -156
- package/src/lib/missing-harness.js +0 -31
- package/src/lib/model-unavailable.js +0 -154
- package/src/lib/native-runtime-inventory.js +0 -40
- package/src/lib/outsider-grant-mcp.js +0 -82
- package/src/lib/own-key-launch.js +0 -42
- package/src/lib/pod-catch-up.js +0 -55
- package/src/lib/pod-focus.js +0 -224
- package/src/lib/pod-tail.js +0 -68
- package/src/lib/poll-liveness.js +0 -103
- package/src/lib/poll-retry.js +0 -81
- package/src/lib/poller.js +0 -200
- package/src/lib/provider-proxy.js +0 -42
- package/src/lib/public-seat-mcp-flag.js +0 -36
- package/src/lib/public-tool-error.js +0 -60
- package/src/lib/public-withheld-tools.js +0 -48
- package/src/lib/reply-receipt.js +0 -55
- package/src/lib/resolve-on-path.js +0 -22
- package/src/lib/runtime-inventory.js +0 -56
- package/src/lib/runtime-observation.js +0 -37
- package/src/lib/sandbox/bwrap.js +0 -275
- package/src/lib/sandbox/codex-perimeter.js +0 -16
- package/src/lib/sandbox/host-check.js +0 -268
- package/src/lib/sandbox/mode.js +0 -49
- package/src/lib/sandbox/opencode-loopback.mjs +0 -68
- package/src/lib/sandbox/seatbelt.js +0 -684
- package/src/lib/sandbox/turn-dir.js +0 -56
- package/src/lib/sandbox/windows-unsandboxed.js +0 -183
- package/src/lib/sandbox/workspace-git-config.js +0 -243
- package/src/lib/sandbox/workspace-pin.js +0 -158
- package/src/lib/seat-runtime-controls.js +0 -61
- package/src/lib/seat-stop.js +0 -97
- package/src/lib/server-skills.js +0 -32
- package/src/lib/session-recycle.js +0 -219
- package/src/lib/session-store.js +0 -215
- package/src/lib/skill-prompt.js +0 -74
- package/src/lib/skills-import.js +0 -113
- package/src/lib/spawn-credential.js +0 -402
- package/src/lib/spawn-retry.js +0 -311
- package/src/lib/stable-install.js +0 -176
- package/src/lib/supervised-turn.js +0 -352
- package/src/lib/task.js +0 -235
- package/src/lib/turn-failure-notice.js +0 -177
- package/src/lib/unconfined-seat-guard.js +0 -94
- package/src/lib/update-check.js +0 -192
- package/src/lib/upstream-refusal.js +0 -1
- package/src/lib/webhook-server.js +0 -109
- package/src/lib/windows-command.js +0 -393
- package/src/lib/workspace-git.js +0 -87
- package/src/lib/workspace-trust.js +0 -162
package/src/lib/adapters/dsh.js
DELETED
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
import { observeSpawn } from '../runtime-observation.js';
|
|
2
|
-
/**
|
|
3
|
-
* DeepSeek Harness 0.2.1-rc.2, private seats only. Configuration is per spawn.
|
|
4
|
-
*
|
|
5
|
-
* A seat runs plain, or confined: `sandbox: { mode: 'workspace', network:
|
|
6
|
-
* { policy: 'restricted' } }` runs dsh inside one Seatbelt profile that writes
|
|
7
|
-
* only the workspace and the seat's home, reaches only three loopback ports
|
|
8
|
-
* (the provider proxy, the Commonly instance relay and the GitHub egress
|
|
9
|
-
* proxy), and cannot reach the Security services the keychain sits behind.
|
|
10
|
-
* The provider proxy is pinned to the seat's model and checks the model each
|
|
11
|
-
* response names. dsh's own sandbox is turned off inside it, because a second
|
|
12
|
-
* Seatbelt cannot be applied under the first.
|
|
13
|
-
*/
|
|
14
|
-
import { spawnInOwnGroup, wantsOwnProcessGroup } from '@commonlyai/seats/process-group';
|
|
15
|
-
import { requireSeatModel } from './seat-model.js';
|
|
16
|
-
import { spawn as childSpawn, spawnSync } from 'child_process';
|
|
17
|
-
import { createHash } from 'crypto';
|
|
18
|
-
import { existsSync, realpathSync } from 'fs';
|
|
19
|
-
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'fs/promises';
|
|
20
|
-
import { homedir, tmpdir } from 'os';
|
|
21
|
-
import { basename, delimiter, isAbsolute, join, sep } from 'path';
|
|
22
|
-
import { startEgressProxy } from '../egress-proxy.js';
|
|
23
|
-
import { startInstanceProxy } from '../instance-proxy.js';
|
|
24
|
-
import { prepareMcpSpawn, selectedMcpVersionDir } from '../mcp-home.js';
|
|
25
|
-
import { detectSeatbelt, wrapArgvWithSeatbelt } from '../sandbox/seatbelt.js';
|
|
26
|
-
import { refuseWorkspaceHoldingTurnDirs } from '../sandbox/turn-dir.js';
|
|
27
|
-
import { buildMemoryPreamble } from '../memory-bridge.js';
|
|
28
|
-
import { commonlyMcpServer } from '../default-environment.js';
|
|
29
|
-
import { effectiveSandboxTrust, refuseWorkspaceTrust, validateEnvironmentSpec } from '../environment.js';
|
|
30
|
-
import { deliverSeatCredential } from '../mcp-credential-delivery.js';
|
|
31
|
-
import { describeMcpCommand } from '../mcp-server-version.js';
|
|
32
|
-
import { writeCredentialFile } from '../credential-file.js';
|
|
33
|
-
import { resolveProviderKeyFile, startProviderProxy } from '../provider-proxy.js';
|
|
34
|
-
import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
|
|
35
|
-
import { promptTokens } from '../session-recycle.js';
|
|
36
|
-
|
|
37
|
-
export const TESTED_DSH_VERSIONS = Object.freeze(['0.2.1-rc.2']);
|
|
38
|
-
const PROXY_KEY_ENV = 'COMMONLY_DSH_PROXY_KEY';
|
|
39
|
-
const MAX_TIMEOUT_MS = 15 * 60 * 1000;
|
|
40
|
-
const SAFE_ENV = /^(?:PATH|LANG|LC_[A-Z_]+|TERM|USER|LOGNAME|NO_COLOR|CI|SSL_CERT_FILE|SSL_CERT_DIR)$/;
|
|
41
|
-
|
|
42
|
-
/** True for the one confined shape this adapter enforces; throws on any other sandbox. */
|
|
43
|
-
export const isConfinedDsh = (environment = {}) => {
|
|
44
|
-
const sandbox = environment?.sandbox || {};
|
|
45
|
-
refuseWorkspaceTrust(sandbox, 'dsh');
|
|
46
|
-
if (effectiveSandboxTrust(sandbox.trust) === 'public') {
|
|
47
|
-
throw new Error('dsh adapter: public-trust seats are not supported; nested Seatbelt fails closed');
|
|
48
|
-
}
|
|
49
|
-
if (sandbox.mode === undefined) return false;
|
|
50
|
-
const { mode, network, ...rest } = sandbox;
|
|
51
|
-
const networkKeys = Object.keys(network || {});
|
|
52
|
-
if (mode === 'workspace' && network?.policy === 'restricted'
|
|
53
|
-
&& networkKeys.length === 1 && Object.keys(rest).length === 0) return true;
|
|
54
|
-
throw new Error(`dsh adapter: sandbox.mode=${mode} cannot be enforced by this adapter; `
|
|
55
|
-
+ "the one confined shape is { mode: 'workspace', network: { policy: 'restricted' } }");
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export const assertPrivateDsh = (environment = {}) => { isConfinedDsh(environment); };
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* The macOS developer tools a confined writer needs for git, granted read-only.
|
|
62
|
-
* /usr/bin/git is an xcrun shim: inside the sandbox it cannot read the tools it
|
|
63
|
-
* forwards to, so `git` fails with "No developer tools were found" and a writer
|
|
64
|
-
* cannot commit. Returns null when no tools are installed; the seat then
|
|
65
|
-
* runs without git rather than failing.
|
|
66
|
-
*
|
|
67
|
-
* DEVELOPER_DIR and xcode-select are both settable by whoever starts the seat,
|
|
68
|
-
* so the resolved path is granted only when it is one of the two places Apple
|
|
69
|
-
* installs developer tools. Anything else (/, a home directory) returns null
|
|
70
|
-
* rather than widening what the sandbox can read.
|
|
71
|
-
*/
|
|
72
|
-
const DEVELOPER_DIR_SHAPES = [
|
|
73
|
-
/^\/Library\/Developer\/CommandLineTools$/,
|
|
74
|
-
/^\/Applications\/[^/]+\.app\/Contents\/Developer$/,
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
export const developerToolsDir = ({
|
|
78
|
-
env = process.env, run = spawnSync, exists = existsSync, realpath = realpathSync,
|
|
79
|
-
} = {}) => {
|
|
80
|
-
const declared = env?.DEVELOPER_DIR;
|
|
81
|
-
const dir = declared || String(run('/usr/bin/xcode-select', ['-p'], { encoding: 'utf8' })?.stdout || '').trim();
|
|
82
|
-
if (!dir || !isAbsolute(dir) || !exists(dir)) return null;
|
|
83
|
-
const resolved = realpath(dir);
|
|
84
|
-
return DEVELOPER_DIR_SHAPES.some((shape) => shape.test(resolved)) ? resolved : null;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The confined child's PATH with /usr/bin first, so bare `git` is the xcrun
|
|
89
|
-
* shim the developer-tools grant serves. Seat runners put /opt/homebrew/bin
|
|
90
|
-
* ahead of it, and Homebrew git fails inside the sandbox reading its own
|
|
91
|
-
* /opt/homebrew/etc/gitconfig. Other entries keep their order, so tools
|
|
92
|
-
* /usr/bin lacks (node, npm) still resolve as before.
|
|
93
|
-
*/
|
|
94
|
-
export const systemToolsFirst = (pathValue) => ['/usr/bin', ...String(pathValue || '').split(delimiter)
|
|
95
|
-
.filter((dir) => dir && dir !== '/usr/bin')].join(delimiter);
|
|
96
|
-
|
|
97
|
-
const onPath = (name, env) => {
|
|
98
|
-
if (isAbsolute(name)) return existsSync(name) ? name : null;
|
|
99
|
-
for (const dir of String(env?.PATH || '').split(delimiter)) {
|
|
100
|
-
if (dir && existsSync(join(dir, name))) return join(dir, name);
|
|
101
|
-
}
|
|
102
|
-
return null;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* The script a dsh executable runs, and the outermost node_modules holding it:
|
|
107
|
-
* a confined run starts that script with this node, and reads that tree only.
|
|
108
|
-
*/
|
|
109
|
-
export const resolveDshEntry = (binary, env) => {
|
|
110
|
-
const found = onPath(binary, env);
|
|
111
|
-
if (!found) throw new Error(`dsh adapter: a confined seat could not find ${binary} on PATH`);
|
|
112
|
-
const entry = realpathSync(found);
|
|
113
|
-
const parts = entry.split(sep);
|
|
114
|
-
const root = parts.indexOf('node_modules');
|
|
115
|
-
if (root < 1 || !/\.(?:c|m)?js$/.test(entry)) {
|
|
116
|
-
throw new Error(`dsh adapter: a confined seat runs dsh from its node package; ${found} resolves to ${entry}`);
|
|
117
|
-
}
|
|
118
|
-
return { entry, packages: parts.slice(0, root + 1).join(sep) };
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
/** Fail a confined turn whose provider traffic broke the model pin. */
|
|
122
|
-
export const assertServedModel = (check, credentials) => {
|
|
123
|
-
if (!check.refused && !check.failures.length) return;
|
|
124
|
-
const parts = [
|
|
125
|
-
...(check.refused ? [`${check.refused} request(s) for a model other than ${check.pinned} refused`] : []),
|
|
126
|
-
...check.failures,
|
|
127
|
-
];
|
|
128
|
-
throw adapterFailure('dsh', `served model check failed: ${parts.join('; ')}`, { credentials, limit: 500 });
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
/** A stable home per agent and Commonly instance, never the operator's ~/.dsh. */
|
|
132
|
-
export const seatHome = (ctx) => {
|
|
133
|
-
if (ctx._dshHome) return ctx._dshHome;
|
|
134
|
-
const id = createHash('sha256').update(`${ctx.instanceUrl || ''}\0${ctx.agentName || ''}`).digest('hex').slice(0, 24);
|
|
135
|
-
return join(homedir(), '.commonly', 'dsh-homes', id);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
export const childEnvironment = (source, home, bearer) => {
|
|
139
|
-
const env = Object.fromEntries(Object.entries(source || {}).filter(([name]) => SAFE_ENV.test(name)));
|
|
140
|
-
env.HOME = home;
|
|
141
|
-
env.DSH_HOME = home;
|
|
142
|
-
env.DSH_PERMISSION_MODE = 'workspace-write';
|
|
143
|
-
env.DSH_TELEMETRY_MODE = 'DISABLED';
|
|
144
|
-
env.DSH_TELEMETRY_DISABLED = '1';
|
|
145
|
-
env[PROXY_KEY_ENV] = bearer;
|
|
146
|
-
return env;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const substitute = (value, { credentialFile, instanceUrl }) => String(value).replace(/\$\{(COMMONLY_[A-Z_]+)\}/g, (whole, name) => ({
|
|
150
|
-
COMMONLY_TOKEN_FILE: credentialFile,
|
|
151
|
-
COMMONLY_API_URL: instanceUrl,
|
|
152
|
-
COMMONLY_INSTANCE_URL: instanceUrl,
|
|
153
|
-
}[name] || whole));
|
|
154
|
-
|
|
155
|
-
const isCommonlyCommand = (command) => {
|
|
156
|
-
if (!Array.isArray(command)) return false;
|
|
157
|
-
if (command.length === 3 && command[0] === 'npx' && command[1] === '-y') {
|
|
158
|
-
return /^@commonlyai\/mcp(?:@(?:latest|\d+\.\d+\.\d+))?$/.test(command[2]);
|
|
159
|
-
}
|
|
160
|
-
return command.length === 2 && basename(command[0]) === 'node'
|
|
161
|
-
&& command[1].endsWith('/@commonlyai/mcp/src/index.js');
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
/** One stdio Commonly server only. No remote credential or arbitrary MCP child. */
|
|
165
|
-
export const resolveCommonlyMcp = (declared, ctx) => {
|
|
166
|
-
const servers = declared?.length ? declared : [commonlyMcpServer()];
|
|
167
|
-
if (servers.length !== 1 || servers[0]?.name !== 'commonly') {
|
|
168
|
-
throw new Error('dsh adapter: PR 1 supports only the Space MCP server');
|
|
169
|
-
}
|
|
170
|
-
const isCommonly = (entry) => (entry.transport || 'stdio') === 'stdio'
|
|
171
|
-
&& isCommonlyCommand(entry.command) && !!describeMcpCommand(entry.command)?.isCommonly;
|
|
172
|
-
if (!isCommonly(servers[0])) {
|
|
173
|
-
throw new Error('dsh adapter: Space MCP must be a recognized stdio @commonlyai/mcp command');
|
|
174
|
-
}
|
|
175
|
-
// A confined seat runs the warmed build it can read, never a package runner,
|
|
176
|
-
// and that build is held to the same check as the declaration.
|
|
177
|
-
const server = ctx.prepareCommand
|
|
178
|
-
? { ...servers[0], command: ctx.prepareCommand(servers[0].command) }
|
|
179
|
-
: servers[0];
|
|
180
|
-
if (!isCommonly(server)) {
|
|
181
|
-
throw new Error('dsh adapter: Space MCP must be a recognized stdio @commonlyai/mcp command');
|
|
182
|
-
}
|
|
183
|
-
const delivery = deliverSeatCredential(server, {
|
|
184
|
-
credentialFile: ctx.credentialFile, label: 'dsh',
|
|
185
|
-
});
|
|
186
|
-
if (delivery.delivered !== 'path') {
|
|
187
|
-
throw new Error(`dsh adapter: Space MCP requires file-backed credentials (got ${delivery.delivered})`);
|
|
188
|
-
}
|
|
189
|
-
const values = { credentialFile: ctx.credentialFile, instanceUrl: ctx.instanceUrl };
|
|
190
|
-
const env = Object.fromEntries(Object.entries(delivery.env)
|
|
191
|
-
.map(([name, value]) => [name, substitute(value, values)]));
|
|
192
|
-
if (Object.keys(env).some((name) => !['COMMONLY_API_URL', 'COMMONLY_TOKEN_FILE'].includes(name))
|
|
193
|
-
|| env.COMMONLY_TOKEN_FILE !== ctx.credentialFile || env.COMMONLY_API_URL !== ctx.instanceUrl) {
|
|
194
|
-
throw new Error('dsh adapter: Space MCP must use this spawn\'s credential file');
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
id: 'mcp-commonly', name: '@deepseek-ai/dsh-mcp-client',
|
|
198
|
-
config: {
|
|
199
|
-
serverName: 'commonly', transport: 'stdio',
|
|
200
|
-
command: substitute(server.command[0], values),
|
|
201
|
-
args: server.command.slice(1).map((arg) => substitute(arg, values)),
|
|
202
|
-
env, failOnStartupError: true,
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
/** JSON is valid YAML; a generated patch has no expression or quoting ambiguity. */
|
|
208
|
-
export const buildPatch = ({
|
|
209
|
-
proxyBaseURL, model, cwd, mcp, effort, deepseekOfficial = false, confined = false,
|
|
210
|
-
}) => ([
|
|
211
|
-
{ id: 'llm-pi-ai', config: { providers: {
|
|
212
|
-
commonly: {
|
|
213
|
-
apiKeyEnv: PROXY_KEY_ENV, api: 'openai-completions', baseURL: proxyBaseURL,
|
|
214
|
-
...(effort ? { reasoning: effort } : {}),
|
|
215
|
-
// The loopback proxy hides the upstream URL from pi-ai's compatibility detection.
|
|
216
|
-
// Use the conservative Chat Completions roles/fields for every provider.
|
|
217
|
-
compat: {
|
|
218
|
-
supportsDeveloperRole: false,
|
|
219
|
-
supportsStore: false,
|
|
220
|
-
...(deepseekOfficial ? { thinkingFormat: 'deepseek' } : {}),
|
|
221
|
-
},
|
|
222
|
-
models: [{ id: model, name: model, contextWindow: 128000,
|
|
223
|
-
...(effort ? { reasoningEfforts: {
|
|
224
|
-
low: 'low', medium: 'high', high: 'high', xhigh: 'high', max: 'max',
|
|
225
|
-
} } : {}),
|
|
226
|
-
}],
|
|
227
|
-
},
|
|
228
|
-
} } },
|
|
229
|
-
{ id: 'agent-default-model', config: { provider: 'commonly', model } },
|
|
230
|
-
{ id: 'llm-deepseek', disabled: true },
|
|
231
|
-
{ id: 'llm-deepseek-account', disabled: true },
|
|
232
|
-
{ id: 'session-log-deepseek', config: { enabled: false } },
|
|
233
|
-
// Confined, the outer Seatbelt is the sandbox: dsh's own would nest a second
|
|
234
|
-
// one, which macOS refuses, so it is off.
|
|
235
|
-
{ id: 'sandbox-policy', config: { mode: confined ? 'danger-full-access' : 'workspace-write', workspaceRoot: cwd } },
|
|
236
|
-
{ insert: [mcp] },
|
|
237
|
-
]);
|
|
238
|
-
|
|
239
|
-
export const assertPatchHasNoCredentials = (serialized, credentials) => {
|
|
240
|
-
if (serialized.includes('${COMMONLY_AGENT_TOKEN}')) {
|
|
241
|
-
throw new Error('dsh adapter: patch contains an unresolved seat token placeholder');
|
|
242
|
-
}
|
|
243
|
-
for (const credential of credentials) {
|
|
244
|
-
if (typeof credential === 'string' && credential && serialized.includes(credential)) {
|
|
245
|
-
throw new Error('dsh adapter: patch contains a credential value');
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
/** dsh writes sessions below DSH_HOME/sessions/<workspace>/<session-id>. */
|
|
251
|
-
export const sessionExists = async (home, sessionId) => {
|
|
252
|
-
if (!/^session-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(sessionId || '')) return false;
|
|
253
|
-
const root = join(home, 'sessions');
|
|
254
|
-
if (!existsSync(root)) return false;
|
|
255
|
-
for (const workspace of await readdir(root, { withFileTypes: true })) {
|
|
256
|
-
if (!workspace.isDirectory()) continue;
|
|
257
|
-
if (existsSync(join(root, workspace.name, sessionId, 'session.v4.jsonl.zstd'))) return true;
|
|
258
|
-
}
|
|
259
|
-
return false;
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const parseVersion = (output) => String(output || '').trim().match(/\b\d+\.\d+\.\d+-rc\.\d+\b/)?.[0] || null;
|
|
263
|
-
const readVersion = (binary, env) => {
|
|
264
|
-
const result = spawnSync(binary, ['--version'], { encoding: 'utf8', timeout: 5000, env, cwd: env.DSH_HOME });
|
|
265
|
-
return result.status === 0 && !result.error ? parseVersion(result.stdout) : null;
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const parseEvent = (line, state) => {
|
|
269
|
-
let event;
|
|
270
|
-
try { event = JSON.parse(line); } catch { throw new Error('dsh emitted invalid JSON event'); }
|
|
271
|
-
if (event.type === 'session' && typeof event.sessionId === 'string') state.sessionId = event.sessionId;
|
|
272
|
-
if (event.type === 'final' && typeof event.text === 'string') state.final = event.text;
|
|
273
|
-
// F404: each step_end carries that request's usage, input apart from the cache.
|
|
274
|
-
if (event.type === 'status' && event.phase === 'step_end' && event.usage && typeof event.usage === 'object') {
|
|
275
|
-
const tokens = promptTokens({
|
|
276
|
-
input: event.usage.inputTokens, cacheRead: event.usage.cacheReadTokens, cacheWrite: event.usage.cacheWriteTokens,
|
|
277
|
-
});
|
|
278
|
-
if (tokens !== null) state.contextTokens = tokens;
|
|
279
|
-
}
|
|
280
|
-
if (event.type === 'error') state.error = String(event.message || event.error || 'dsh error');
|
|
281
|
-
if (event.type === 'tool_result'
|
|
282
|
-
&& /SANDBOX_UNAVAILABLE|sandbox_apply: Operation not permitted/.test(JSON.stringify(event))) {
|
|
283
|
-
state.error = 'dsh sandbox runner unavailable';
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
export const runDsh = ({
|
|
288
|
-
binary, args, cwd, env, timeoutMs, credentials, spawnImpl = childSpawn, onSpawn, controls = {}, ownGroup = false,
|
|
289
|
-
}) => new Promise((resolve, reject) => {
|
|
290
|
-
const proc = spawnInOwnGroup(spawnImpl, binary, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] }, { ownGroup });
|
|
291
|
-
observeSpawn(proc, onSpawn, controls);
|
|
292
|
-
const state = {
|
|
293
|
-
sessionId: null, final: null, error: null, buffer: '', contextTokens: null,
|
|
294
|
-
};
|
|
295
|
-
let stderr = '';
|
|
296
|
-
let timedOut = false;
|
|
297
|
-
let parseFailure = null;
|
|
298
|
-
let settled = false;
|
|
299
|
-
let killTimer;
|
|
300
|
-
const finish = (error, result) => {
|
|
301
|
-
if (settled) return;
|
|
302
|
-
settled = true;
|
|
303
|
-
clearTimeout(timer);
|
|
304
|
-
clearTimeout(killTimer);
|
|
305
|
-
if (error) reject(error); else resolve(result);
|
|
306
|
-
};
|
|
307
|
-
const timer = setTimeout(() => {
|
|
308
|
-
timedOut = true;
|
|
309
|
-
proc.kill('SIGTERM');
|
|
310
|
-
killTimer = setTimeout(() => proc.kill('SIGKILL'), 5000);
|
|
311
|
-
}, timeoutMs);
|
|
312
|
-
proc.stdout?.on('data', (chunk) => {
|
|
313
|
-
state.buffer += chunk.toString();
|
|
314
|
-
if (state.buffer.length > 1024 * 1024) {
|
|
315
|
-
parseFailure = new Error('dsh event exceeded 1 MiB');
|
|
316
|
-
proc.kill('SIGTERM');
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
while (state.buffer.includes('\n')) {
|
|
320
|
-
const end = state.buffer.indexOf('\n');
|
|
321
|
-
const line = state.buffer.slice(0, end).trim();
|
|
322
|
-
state.buffer = state.buffer.slice(end + 1);
|
|
323
|
-
if (line && !parseFailure) {
|
|
324
|
-
try { parseEvent(line, state); } catch (error) { parseFailure = error; proc.kill('SIGTERM'); }
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
proc.stderr?.on('data', (chunk) => { stderr = (stderr + chunk.toString()).slice(-8192); });
|
|
329
|
-
proc.on('error', (error) => finish(error));
|
|
330
|
-
proc.on('close', (code) => {
|
|
331
|
-
if (state.buffer.trim() && !parseFailure) {
|
|
332
|
-
try { parseEvent(state.buffer.trim(), state); } catch (error) { parseFailure = error; }
|
|
333
|
-
}
|
|
334
|
-
const failure = parseFailure?.message || state.error || (timedOut ? `timeout after ${timeoutMs}ms` : null)
|
|
335
|
-
|| (/SANDBOX_UNAVAILABLE|sandbox_apply: Operation not permitted/.test(stderr) ? 'sandbox runner unavailable' : null)
|
|
336
|
-
|| (code !== 0 ? stderr.trim() || `exit ${code}` : null)
|
|
337
|
-
|| (state.final === null ? 'no final event' : null);
|
|
338
|
-
if (failure) return finish(adapterFailure('dsh', failure, { credentials, exitCode: code, limit: 500 }));
|
|
339
|
-
return finish(null, { text: state.final.trim(), newSessionId: state.sessionId, contextTokens: state.contextTokens });
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
export default {
|
|
344
|
-
requiresModel: true,
|
|
345
|
-
name: 'dsh', runtimeType: 'dsh', supportsProvider: true,
|
|
346
|
-
|
|
347
|
-
// The attach gate defers to this for dsh's one confined shape.
|
|
348
|
-
confinesOwnSandbox: (environment) => isConfinedDsh(environment),
|
|
349
|
-
|
|
350
|
-
async validateEnvironment(environment, workspacePath) {
|
|
351
|
-
const validation = validateEnvironmentSpec(environment);
|
|
352
|
-
if (!validation.ok) throw new Error(validation.errors.join('; '));
|
|
353
|
-
assertPrivateDsh(environment);
|
|
354
|
-
if (!environment?.provider || !environment?.model) {
|
|
355
|
-
throw new Error('dsh requires environment.provider and environment.model');
|
|
356
|
-
}
|
|
357
|
-
const detected = await this.detect();
|
|
358
|
-
if (!TESTED_DSH_VERSIONS.includes(detected?.version)) {
|
|
359
|
-
throw new Error(`dsh adapter: installed version ${detected?.version || 'unknown'} is not smoke-tested (${TESTED_DSH_VERSIONS.join(', ')})`);
|
|
360
|
-
}
|
|
361
|
-
// A confined dsh is a private seat under Seatbelt, so it is checked too.
|
|
362
|
-
return resolveProviderKeyFile(environment.provider, workspacePath, 'dsh', {
|
|
363
|
-
environment, sandboxed: isConfinedDsh(environment),
|
|
364
|
-
});
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
async detect() {
|
|
368
|
-
const home = await mkdtemp(join(tmpdir(), 'commonly-dsh-probe-'));
|
|
369
|
-
try {
|
|
370
|
-
const env = childEnvironment(process.env, home, '');
|
|
371
|
-
const version = readVersion('dsh', env);
|
|
372
|
-
return version ? { path: 'dsh', version } : null;
|
|
373
|
-
} finally {
|
|
374
|
-
await rm(home, { recursive: true, force: true });
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
|
|
378
|
-
async spawn(prompt, ctx = {}) {
|
|
379
|
-
const seatModel = requireSeatModel(ctx.environment, 'dsh', true);
|
|
380
|
-
const confined = isConfinedDsh(ctx.environment);
|
|
381
|
-
if (confined) {
|
|
382
|
-
if ((ctx._platform || process.platform) !== 'darwin') {
|
|
383
|
-
throw new Error('dsh adapter: a confined seat needs Seatbelt, which is macOS only');
|
|
384
|
-
}
|
|
385
|
-
const seatbelt = (ctx._detectSeatbelt || detectSeatbelt)();
|
|
386
|
-
if (!seatbelt.available) throw new Error(`dsh adapter: a confined seat needs Seatbelt: ${seatbelt.error}`);
|
|
387
|
-
await refuseWorkspaceHoldingTurnDirs({ workspace: ctx.cwd, seat: 'confined dsh agents' });
|
|
388
|
-
}
|
|
389
|
-
if (!ctx.environment?.provider || !ctx.environment?.model) {
|
|
390
|
-
throw new Error('dsh requires environment.provider and environment.model');
|
|
391
|
-
}
|
|
392
|
-
if (!ctx.runtimeToken || !ctx.instanceUrl) throw new Error('dsh requires a Space runtime token and instance URL');
|
|
393
|
-
const home = seatHome(ctx);
|
|
394
|
-
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
395
|
-
await chmod(home, 0o700);
|
|
396
|
-
const binary = ctx._dshBinary || 'dsh';
|
|
397
|
-
const sourceEnv = ctx.env || process.env;
|
|
398
|
-
const version = readVersion(binary, childEnvironment(sourceEnv, home, ''));
|
|
399
|
-
if (!TESTED_DSH_VERSIONS.includes(version)) {
|
|
400
|
-
throw new Error(`dsh adapter: installed version ${version || 'unknown'} is not smoke-tested (${TESTED_DSH_VERSIONS.join(', ')})`);
|
|
401
|
-
}
|
|
402
|
-
// The seat home is the confined run's statePath: stable and writable in the jail.
|
|
403
|
-
const keyFile = await resolveProviderKeyFile(ctx.environment.provider, ctx.cwd, 'dsh', {
|
|
404
|
-
environment: ctx.environment, platform: ctx._platform, sandboxed: confined, seatDirectories: [home],
|
|
405
|
-
});
|
|
406
|
-
const tempDir = await mkdtemp(join(tmpdir(), 'commonly-dsh-'));
|
|
407
|
-
await chmod(tempDir, 0o700);
|
|
408
|
-
let proxy;
|
|
409
|
-
let relay;
|
|
410
|
-
let egress;
|
|
411
|
-
try {
|
|
412
|
-
const credential = writeCredentialFile(ctx.runtimeToken, { agentName: 'commonly', root: tempDir });
|
|
413
|
-
proxy = await startProviderProxy({
|
|
414
|
-
provider: ctx.environment.provider, keyFile,
|
|
415
|
-
tokenFile: join(tempDir, 'provider-proxy-token'), adapterLabel: 'dsh',
|
|
416
|
-
...(confined ? { pinnedModel: seatModel } : {}),
|
|
417
|
-
});
|
|
418
|
-
const bearer = (await readFile(proxy.tokenFile, 'utf8')).trim();
|
|
419
|
-
const mcpDirectories = [];
|
|
420
|
-
if (confined) {
|
|
421
|
-
relay = await startInstanceProxy({ instanceUrl: ctx.instanceUrl });
|
|
422
|
-
egress = await (ctx._startEgressProxy || startEgressProxy)();
|
|
423
|
-
}
|
|
424
|
-
const mcp = resolveCommonlyMcp(ctx.environment?.mcp, {
|
|
425
|
-
credentialFile: credential.path,
|
|
426
|
-
instanceUrl: confined ? relay.url : ctx.instanceUrl,
|
|
427
|
-
...(confined ? {
|
|
428
|
-
prepareCommand: (command) => {
|
|
429
|
-
const warmed = (ctx._prepareMcpSpawn || prepareMcpSpawn)(command, { apiUrl: ctx.instanceUrl, publicSeat: true });
|
|
430
|
-
const directory = warmed && (ctx._selectedMcpVersionDir || selectedMcpVersionDir)(warmed);
|
|
431
|
-
if (!directory) {
|
|
432
|
-
throw new Error('dsh adapter: a confined seat runs only a warmed Space MCP build, and none is ready on this host');
|
|
433
|
-
}
|
|
434
|
-
mcpDirectories.push(directory);
|
|
435
|
-
return [process.execPath, warmed[1]];
|
|
436
|
-
},
|
|
437
|
-
} : {}),
|
|
438
|
-
});
|
|
439
|
-
const patchData = buildPatch({
|
|
440
|
-
proxyBaseURL: proxy.baseURL, model: seatModel,
|
|
441
|
-
cwd: ctx.cwd, mcp, effort: ctx.environment.effort, confined,
|
|
442
|
-
deepseekOfficial: new URL(ctx.environment.provider.baseURL).hostname === 'api.deepseek.com',
|
|
443
|
-
});
|
|
444
|
-
const patch = JSON.stringify(patchData, null, 2);
|
|
445
|
-
const passedProvider = patchData.find((entry) => entry.id === 'llm-pi-ai').config.providers.commonly;
|
|
446
|
-
const passedModel = patchData.find((entry) => entry.id === 'agent-default-model').config.model;
|
|
447
|
-
assertPatchHasNoCredentials(patch, [ctx.runtimeToken, bearer]);
|
|
448
|
-
const patchPath = join(tempDir, 'seat.patch.yml');
|
|
449
|
-
await writeFile(patchPath, `${patch}\n`, { mode: 0o600 });
|
|
450
|
-
await chmod(patchPath, 0o600);
|
|
451
|
-
const env = childEnvironment(sourceEnv, home, bearer);
|
|
452
|
-
const resume = await sessionExists(home, ctx.sessionId);
|
|
453
|
-
const fullPrompt = buildMemoryPreamble(prompt, ctx.memoryLongTerm, { freshSession: !resume });
|
|
454
|
-
const args = ['--profile', 'headless', '--patch', patchPath, '--json',
|
|
455
|
-
...(resume ? ['--session-id', ctx.sessionId] : []), fullPrompt];
|
|
456
|
-
const requested = Number(ctx.timeoutMs);
|
|
457
|
-
const timeoutMs = Number.isFinite(requested) && requested > 0
|
|
458
|
-
? Math.min(requested, MAX_TIMEOUT_MS) : MAX_TIMEOUT_MS;
|
|
459
|
-
let command = binary;
|
|
460
|
-
let commandArgs = args;
|
|
461
|
-
if (confined) {
|
|
462
|
-
const { entry, packages } = resolveDshEntry(binary, sourceEnv);
|
|
463
|
-
const tempWork = join(home, 'tmp');
|
|
464
|
-
await mkdir(tempWork, { recursive: true, mode: 0o700 });
|
|
465
|
-
env.TMPDIR = tempWork;
|
|
466
|
-
for (const name of ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy']) env[name] = egress.url;
|
|
467
|
-
env.NO_PROXY = '127.0.0.1,localhost';
|
|
468
|
-
env.no_proxy = env.NO_PROXY;
|
|
469
|
-
const developerDir = (ctx._developerToolsDir || developerToolsDir)({ env: sourceEnv });
|
|
470
|
-
if (developerDir) {
|
|
471
|
-
env.DEVELOPER_DIR = developerDir;
|
|
472
|
-
env.PATH = systemToolsFirst(env.PATH);
|
|
473
|
-
}
|
|
474
|
-
[command, ...commandArgs] = (ctx._wrapArgvWithSeatbelt || wrapArgvWithSeatbelt)([process.execPath, entry, ...args], {
|
|
475
|
-
workspacePath: ctx.cwd,
|
|
476
|
-
workspaceAccess: 'write',
|
|
477
|
-
executablePath: process.execPath,
|
|
478
|
-
statePath: home,
|
|
479
|
-
mcpConfigDir: tempDir,
|
|
480
|
-
network: 'loopback',
|
|
481
|
-
loopbackNetworkPorts: [proxy.port, relay.port, egress.port],
|
|
482
|
-
readOnlyDirectories: [packages, ...mcpDirectories, ...(developerDir ? [developerDir] : [])],
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
const credentials = [...spawnCredentials(ctx, []), bearer];
|
|
486
|
-
let result;
|
|
487
|
-
let failure;
|
|
488
|
-
try {
|
|
489
|
-
result = await runDsh({
|
|
490
|
-
binary: command, args: commandArgs, cwd: ctx.cwd, env, timeoutMs, credentials,
|
|
491
|
-
spawnImpl: ctx._spawnImpl, onSpawn: ctx.onSpawn, ownGroup: wantsOwnProcessGroup(ctx),
|
|
492
|
-
controls: { model: passedModel || null, effort: passedProvider.reasoning || null },
|
|
493
|
-
});
|
|
494
|
-
} catch (error) {
|
|
495
|
-
failure = error;
|
|
496
|
-
}
|
|
497
|
-
// A broken pin is the reason a turn failed, whatever dsh said about it.
|
|
498
|
-
if (confined) {
|
|
499
|
-
const { refusals } = egress.stats();
|
|
500
|
-
// eslint-disable-next-line no-console
|
|
501
|
-
if (refusals.length) console.warn(`[dsh] egress refused: ${refusals.map((r) => `${r.target} (${r.reason})`).join(', ')}`);
|
|
502
|
-
assertServedModel(proxy.modelCheck(), credentials);
|
|
503
|
-
}
|
|
504
|
-
if (failure) throw failure;
|
|
505
|
-
return result;
|
|
506
|
-
} finally {
|
|
507
|
-
try { await egress?.close(); } catch { /* best effort */ }
|
|
508
|
-
try { await relay?.close(); } catch { /* best effort */ }
|
|
509
|
-
try { await proxy?.close(); } catch { /* best effort */ }
|
|
510
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
511
|
-
}
|
|
512
|
-
},
|
|
513
|
-
};
|