starmux 0.13.1 → 0.15.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/CHANGELOG.md +27 -0
- package/LICENSE +1 -1
- package/README.md +20 -28
- package/dist/src/access-control.js +1 -2
- package/dist/src/access-dashboard.js +2 -3
- package/dist/src/account-access-policy.js +54 -0
- package/dist/src/authorized-workspace-roots.js +0 -1
- package/dist/src/batch-read-policy.js +30 -0
- package/dist/src/cli/instances.js +27 -187
- package/dist/src/cli/oauth.js +0 -1
- package/dist/src/cli/starmux-status.js +25 -18
- package/dist/src/cli/starmux-supervisor.js +0 -1
- package/dist/src/cli/starmux.js +47 -37
- package/dist/src/config-schemas.js +1 -10
- package/dist/src/configured-mcp-upstreams.js +0 -1
- package/dist/src/contracts.js +0 -1
- package/dist/src/current-permission-probe.js +0 -1
- package/dist/src/dc-workspace-http.js +57 -33
- package/dist/src/deny-dashboard.js +2 -3
- package/dist/src/desktop-commander-facade.js +4 -4
- package/dist/src/explicit-deny-rules.js +2 -3
- package/dist/src/global-core-control.js +108 -56
- package/dist/src/home-write-sandbox.js +0 -1
- package/dist/src/http-host-policy.js +20 -0
- package/dist/src/instance-control-protocol.js +27 -0
- package/dist/src/instance-runtime.js +215 -76
- package/dist/src/instance-store.js +28 -22
- package/dist/src/instance-worker.js +2 -3
- package/dist/src/invocation.js +1 -2
- package/dist/src/local-desktop-read-probe.js +5 -88
- package/dist/src/managed-workspace-policy.js +38 -4
- package/dist/src/mcp-results.js +0 -1
- package/dist/src/mcp-upstream-router.js +0 -1
- package/dist/src/oauth-keycloak.js +0 -1
- package/dist/src/oauth-resource.js +0 -1
- package/dist/src/ops-dashboard.js +2 -179
- package/dist/src/permissions-dashboard.js +3 -4
- package/dist/src/platform.js +6 -4
- package/dist/src/privacy-diagnostics.js +0 -1
- package/dist/src/private-mcp-activity.js +0 -1
- package/dist/src/private-store-lock.js +0 -1
- package/dist/src/project-dev-presets.js +0 -1
- package/dist/src/project-git-workflow.js +0 -1
- package/dist/src/project-workspace-policy.js +10 -6
- package/dist/src/runtime-identity.js +0 -14
- package/dist/src/runtime-paths.js +0 -3
- package/dist/src/tool-dispatch.js +45 -46
- package/dist/src/validation.js +0 -1
- package/dist/src/workspace-grants.js +1 -2
- package/package.json +7 -15
- package/scripts/prepare.mjs +1 -1
- package/CONTRIBUTING.md +0 -18
- package/SECURITY.md +0 -7
- package/app/README.md +0 -29
- package/dist/src/access-control.js.map +0 -1
- package/dist/src/access-dashboard.js.map +0 -1
- package/dist/src/authorized-workspace-roots.js.map +0 -1
- package/dist/src/cli/access.js +0 -53
- package/dist/src/cli/access.js.map +0 -1
- package/dist/src/cli/community-remote-revoke.js +0 -32
- package/dist/src/cli/community-remote-revoke.js.map +0 -1
- package/dist/src/cli/instances.js.map +0 -1
- package/dist/src/cli/live-home-settings.js +0 -35
- package/dist/src/cli/live-home-settings.js.map +0 -1
- package/dist/src/cli/mac-mcp-gateway.js +0 -257
- package/dist/src/cli/mac-mcp-gateway.js.map +0 -1
- package/dist/src/cli/oauth.js.map +0 -1
- package/dist/src/cli/permissions-owner.js +0 -27
- package/dist/src/cli/permissions-owner.js.map +0 -1
- package/dist/src/cli/service-setup.js +0 -26
- package/dist/src/cli/service-setup.js.map +0 -1
- package/dist/src/cli/setup-wizard.js +0 -149
- package/dist/src/cli/setup-wizard.js.map +0 -1
- package/dist/src/cli/starmux-status.js.map +0 -1
- package/dist/src/cli/starmux-supervisor.js.map +0 -1
- package/dist/src/cli/starmux.js.map +0 -1
- package/dist/src/cli/verify-setup.js +0 -53
- package/dist/src/cli/verify-setup.js.map +0 -1
- package/dist/src/community-remote-revocations.js +0 -135
- package/dist/src/community-remote-revocations.js.map +0 -1
- package/dist/src/config-schemas.js.map +0 -1
- package/dist/src/configured-mcp-upstreams.js.map +0 -1
- package/dist/src/contracts.js.map +0 -1
- package/dist/src/current-permission-probe.js.map +0 -1
- package/dist/src/dc-workspace-http.js.map +0 -1
- package/dist/src/deny-dashboard.js.map +0 -1
- package/dist/src/desktop-commander-facade.js.map +0 -1
- package/dist/src/explicit-deny-rules.js.map +0 -1
- package/dist/src/gateway-status-format.js +0 -66
- package/dist/src/gateway-status-format.js.map +0 -1
- package/dist/src/global-core-control.js.map +0 -1
- package/dist/src/home-write-sandbox.js.map +0 -1
- package/dist/src/instance-migration.js +0 -138
- package/dist/src/instance-migration.js.map +0 -1
- package/dist/src/instance-runtime.js.map +0 -1
- package/dist/src/instance-store.js.map +0 -1
- package/dist/src/instance-worker.js.map +0 -1
- package/dist/src/invocation.js.map +0 -1
- package/dist/src/live-profile-home-settings.js +0 -80
- package/dist/src/live-profile-home-settings.js.map +0 -1
- package/dist/src/local-desktop-read-probe.js.map +0 -1
- package/dist/src/managed-workspace-policy.js.map +0 -1
- package/dist/src/mcp-results.js.map +0 -1
- package/dist/src/mcp-upstream-router.js.map +0 -1
- package/dist/src/oauth-keycloak.js.map +0 -1
- package/dist/src/oauth-resource.js.map +0 -1
- package/dist/src/operator-cli.js +0 -559
- package/dist/src/operator-cli.js.map +0 -1
- package/dist/src/ops-dashboard.js.map +0 -1
- package/dist/src/permissions-dashboard.js.map +0 -1
- package/dist/src/platform.js.map +0 -1
- package/dist/src/privacy-diagnostics.js.map +0 -1
- package/dist/src/private-mcp-activity.js.map +0 -1
- package/dist/src/private-store-lock.js.map +0 -1
- package/dist/src/project-dev-presets.js.map +0 -1
- package/dist/src/project-git-workflow.js.map +0 -1
- package/dist/src/project-workspace-policy.js.map +0 -1
- package/dist/src/read-scope.js +0 -44
- package/dist/src/read-scope.js.map +0 -1
- package/dist/src/release1-onboarding.js +0 -120
- package/dist/src/release1-onboarding.js.map +0 -1
- package/dist/src/release1-service.js +0 -131
- package/dist/src/release1-service.js.map +0 -1
- package/dist/src/remote-auth.js +0 -47
- package/dist/src/remote-auth.js.map +0 -1
- package/dist/src/runtime-identity.js.map +0 -1
- package/dist/src/runtime-paths.js.map +0 -1
- package/dist/src/starmux-migration.js +0 -172
- package/dist/src/starmux-migration.js.map +0 -1
- package/dist/src/tool-dispatch.js.map +0 -1
- package/dist/src/validation.js.map +0 -1
- package/dist/src/workspace-grants.js.map +0 -1
- package/docs/ACCESS_CONTROL.md +0 -51
- package/docs/CLIENTS.md +0 -22
- package/docs/CONNECT_CHATGPT.md +0 -148
- package/docs/INSTALL.md +0 -62
- package/docs/INSTANCES.md +0 -83
- package/docs/OAUTH.md +0 -116
- package/docs/PLATFORMS.md +0 -29
- package/docs/README.md +0 -16
- package/docs/REMOTE_SETUP.md +0 -280
- package/docs/SECURITY.md +0 -15
- package/src/dc-workspace-http.mjs +0 -3
- package/test/fixtures/p1-fixture.txt +0 -2
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { createServer, createConnection } from 'node:net';
|
|
3
3
|
import { fork } from 'node:child_process';
|
|
4
|
-
import { mkdir, rm,
|
|
4
|
+
import { mkdir, rm, open } from 'node:fs/promises';
|
|
5
|
+
import { setTimeout as pause } from 'node:timers/promises';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { homedir } from 'node:os';
|
|
8
|
-
import {
|
|
9
|
+
import { INSTANCE_CONTROL_VERSION, instanceOwnerSchema, activeConfigSchema, runtimeStateSchema, controlRequestSchema, controlReplySchema, checkInstanceProtocol } from './instance-control-protocol.js';
|
|
9
10
|
import { controlRoot, createPrivateDirectory, childEnvKeys, windows } from './platform.js';
|
|
10
|
-
import { acquireControl } from './global-core-control.js';
|
|
11
|
+
import { acquireControl, inspectControlOwner } from './global-core-control.js';
|
|
11
12
|
import { runtimeIdentity } from './runtime-identity.js';
|
|
12
13
|
import { atomicPrivate, changeRegistry, availablePort, ensureAccessReady, exists, instanceEnv, instancePaths, loadInstanceConfig, prepareUpstreams, readTrusted, resolveInstance } from './instance-store.js';
|
|
13
|
-
|
|
14
|
-
function runtimePaths(id) {
|
|
14
|
+
export function runtimePaths(id) {
|
|
15
15
|
const root = path.join(controlRoot(), 'i-' + id);
|
|
16
16
|
const socket = windows ? '\\\\.\\pipe\\gateway-instance-' + createHash('sha256').update(root).digest('hex').slice(0, 28) : path.join(root, 'control.sock');
|
|
17
17
|
return { root, socket, meta: path.join(root, 'owner.json') };
|
|
@@ -29,21 +29,21 @@ async function ownerFor(instance) {
|
|
|
29
29
|
const p = runtimePaths(instance.id);
|
|
30
30
|
if (!await exists(p.root))
|
|
31
31
|
return null;
|
|
32
|
-
const
|
|
32
|
+
const raw = JSON.parse(await readTrusted(p.meta));
|
|
33
|
+
checkInstanceProtocol(raw);
|
|
34
|
+
const owner = instanceOwnerSchema.parse(raw);
|
|
33
35
|
if (owner.instanceId !== instance.id)
|
|
34
36
|
throw Error('INSTANCE_CONTROL_IDENTITY_INVALID');
|
|
35
37
|
return owner;
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
const owner = await ownerFor(instance);
|
|
39
|
-
if (!owner || !alive(owner.pid))
|
|
40
|
-
return null;
|
|
39
|
+
async function exchange(instance, owner, method, revision, timeoutMs = method === 'apply' ? 65000 : 2000) {
|
|
41
40
|
const nonce = randomBytes(24).toString('hex');
|
|
42
|
-
const body = JSON.stringify({ method, revision: revision ?? null, runId: owner.runId, nonce });
|
|
41
|
+
const body = JSON.stringify({ protocolVersion: INSTANCE_CONTROL_VERSION, method, revision: revision ?? null, runId: owner.runId, nonce });
|
|
43
42
|
return new Promise((resolve, reject) => {
|
|
44
43
|
const socket = createConnection(runtimePaths(instance.id).socket);
|
|
45
44
|
let raw = '';
|
|
46
|
-
|
|
45
|
+
const timer = setTimeout(() => socket.destroy(Error('INSTANCE_CONTROL_TIMEOUT')), timeoutMs);
|
|
46
|
+
socket.once('close', () => clearTimeout(timer));
|
|
47
47
|
socket.on('error', reject);
|
|
48
48
|
socket.on('connect', () => socket.end(JSON.stringify({ body, proof: signature(owner.token, body) }) + '\n'));
|
|
49
49
|
socket.on('data', data => { raw += String(data); if (raw.length > 524288)
|
|
@@ -51,14 +51,18 @@ export async function instanceRequest(instance, method, revision) {
|
|
|
51
51
|
socket.on('end', () => {
|
|
52
52
|
try {
|
|
53
53
|
const reply = JSON.parse(raw);
|
|
54
|
-
if (!equal(reply.proof, signature(owner.token, nonce + reply.body)))
|
|
54
|
+
if (typeof reply.body !== 'string' || !equal(reply.proof, signature(owner.token, nonce + reply.body)))
|
|
55
55
|
throw Error('INSTANCE_CONTROL_REPLY_UNTRUSTED');
|
|
56
|
-
const
|
|
56
|
+
const parsed = JSON.parse(reply.body);
|
|
57
|
+
checkInstanceProtocol(parsed);
|
|
58
|
+
const data = controlReplySchema.parse(parsed);
|
|
57
59
|
if (data.error)
|
|
58
60
|
throw Error(data.error);
|
|
59
61
|
if (data.state.instanceId !== instance.id || data.state.runId !== owner.runId)
|
|
60
62
|
throw Error('INSTANCE_CONTROL_IDENTITY_CHANGED');
|
|
61
|
-
|
|
63
|
+
if (data.result !== (method === 'stop' ? 'STOP_ACCEPTED' : 'STATE'))
|
|
64
|
+
throw Error('INSTANCE_CONTROL_REPLY_INVALID');
|
|
65
|
+
resolve(data);
|
|
62
66
|
}
|
|
63
67
|
catch (e) {
|
|
64
68
|
reject(e);
|
|
@@ -66,64 +70,139 @@ export async function instanceRequest(instance, method, revision) {
|
|
|
66
70
|
});
|
|
67
71
|
});
|
|
68
72
|
}
|
|
73
|
+
export async function instanceRequest(instance, method, revision) {
|
|
74
|
+
const owner = await ownerFor(instance);
|
|
75
|
+
if (!owner || !alive(owner.pid))
|
|
76
|
+
return null;
|
|
77
|
+
return (await exchange(instance, owner, method, revision)).state;
|
|
78
|
+
}
|
|
79
|
+
async function snapshot(file, schema) {
|
|
80
|
+
try {
|
|
81
|
+
return schema.parse(JSON.parse(await readTrusted(file)));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
69
87
|
export async function instanceStatus(instance, home = homedir()) {
|
|
70
88
|
let runtime = null;
|
|
71
89
|
let runtimeProblem = null;
|
|
90
|
+
let owner = null;
|
|
72
91
|
try {
|
|
73
|
-
|
|
92
|
+
owner = await ownerFor(instance);
|
|
93
|
+
if (owner && alive(owner.pid))
|
|
94
|
+
runtime = (await exchange(instance, owner, 'status')).state;
|
|
74
95
|
}
|
|
75
96
|
catch (e) {
|
|
76
97
|
runtimeProblem = e.message;
|
|
77
98
|
}
|
|
78
|
-
let
|
|
99
|
+
let loaded = null;
|
|
79
100
|
let configProblem = null;
|
|
80
101
|
try {
|
|
81
|
-
|
|
102
|
+
loaded = await loadInstanceConfig(instance);
|
|
82
103
|
}
|
|
83
104
|
catch (e) {
|
|
84
105
|
configProblem = e.message;
|
|
85
106
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
catch { }
|
|
92
|
-
if (!runtime && lastRuntime?.problem)
|
|
93
|
-
runtimeProblem ??= lastRuntime.problem;
|
|
107
|
+
const saved = loaded ? { config: loaded.config, revision: loaded.revision, effectiveRevision: loaded.effectiveRevision, sourcePath: loaded.source.path } : null;
|
|
108
|
+
const snapshots = instancePaths(instance, home).snapshots;
|
|
109
|
+
const lastRun = await snapshot(path.join(snapshots, 'last-runtime.json'), runtimeStateSchema);
|
|
110
|
+
const lastActive = await snapshot(path.join(snapshots, 'active.json'), activeConfigSchema);
|
|
94
111
|
let health = 'not-running';
|
|
95
112
|
let accessMode = null;
|
|
96
113
|
let lastToolCallAt = null;
|
|
114
|
+
if (!runtime && !runtimeProblem) {
|
|
115
|
+
// Historical ports are hints for detecting a lost control channel, never
|
|
116
|
+
// authority to stop a process or claim a configuration is currently active.
|
|
117
|
+
const ports = new Set([owner?.port, saved?.config.port, lastActive?.config.port].filter((p) => p !== undefined));
|
|
118
|
+
for (const port of ports) {
|
|
119
|
+
if (!await availablePort(port)) {
|
|
120
|
+
runtimeProblem = 'INSTANCE_CONTROL_UNAVAILABLE: port ' + port + ' is in use; ownership is unconfirmed';
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const lease = inspectControlOwner(port);
|
|
125
|
+
if (lease && alive(lease.pid)) {
|
|
126
|
+
runtimeProblem = 'INSTANCE_CONTROL_UNAVAILABLE: live port lease ' + port;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
runtimeProblem = e.message;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
97
136
|
if (runtime?.phase === 'running' && runtime.port) {
|
|
98
137
|
try {
|
|
99
138
|
const response = await fetch(`http://127.0.0.1:${runtime.port}/healthz`, { signal: AbortSignal.timeout(1500) });
|
|
100
139
|
const data = await response.json();
|
|
101
140
|
health = response.ok && data.status === 'ok' && data.instanceId === instance.id ? 'ok' : 'unrecognized';
|
|
102
141
|
accessMode = data.accessMode ?? null;
|
|
103
|
-
|
|
104
|
-
|
|
142
|
+
if (health === 'ok')
|
|
143
|
+
try {
|
|
144
|
+
const activity = await fetch(`http://127.0.0.1:${runtime.port}/activity.json`, { signal: AbortSignal.timeout(1000) }).then(r => r.json());
|
|
145
|
+
lastToolCallAt = typeof activity.lastToolCallAt === 'string' ? activity.lastToolCallAt : null;
|
|
146
|
+
}
|
|
147
|
+
catch { /* Activity is optional; it must not turn a healthy service into an outage. */ }
|
|
105
148
|
}
|
|
106
149
|
catch {
|
|
107
150
|
health = 'unreachable';
|
|
108
151
|
}
|
|
109
152
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
catch { }
|
|
153
|
+
const active = runtime?.active ?? null;
|
|
154
|
+
const phase = runtime?.phase ?? (runtimeProblem ? 'unknown' : 'stopped');
|
|
155
|
+
if (phase === 'unknown')
|
|
156
|
+
health = 'unknown';
|
|
116
157
|
const configState = configProblem ? (configProblem.includes('ENOENT') ? 'missing' : 'invalid') : runtime?.phase === 'running' ? (saved?.effectiveRevision === active?.effectiveRevision ? 'applied' : 'pending') : 'on-next-start';
|
|
117
158
|
return { schemaVersion: 1, event: 'GATEWAY_STATUS', instanceId: instance.id, name: instance.name, archived: instance.archived, source: instance.source,
|
|
118
|
-
phase
|
|
159
|
+
phase, configState, configProblem, runtimeProblem, runtime, saved, lastActive, lastRun,
|
|
119
160
|
port: runtime?.port ?? saved?.config.port ?? null, configuredPort: saved?.config.port ?? null, active, revision: saved?.revision ?? null,
|
|
120
161
|
health, accessMode, lastToolCallAt, profile: instance.legacy ?? 'setup', installed: false, registered: false,
|
|
121
|
-
label: instance.name, workspaceMode:
|
|
122
|
-
writeScope:
|
|
123
|
-
coreLauncher: runtime?.launcher ?? 'unknown', coreOwnerPid: runtime?.pid ?? null,
|
|
124
|
-
executionMode: runtime
|
|
162
|
+
label: instance.name, workspaceMode: active?.config.mode ?? (phase === 'stopped' ? saved?.config.mode : null) ?? null,
|
|
163
|
+
writeScope: active?.config.writeScope ?? (phase === 'stopped' ? saved?.config.writeScope : null) ?? null, coreControl: runtime?.phase === 'running' ? 'RUNNING' : 'unavailable',
|
|
164
|
+
coreLauncher: runtime?.launcher ?? 'unknown', coreOwnerPid: runtime?.pid ?? null,
|
|
165
|
+
executionMode: runtime ? (runtime.executionMode === 'background' ? 'background' : 'foreground-owned') : phase === 'stopped' ? 'stopped' : 'unknown', loginAutostart: 'disabled',
|
|
125
166
|
connectionState: health === 'ok' ? (lastToolCallAt ? 'local-ready-activity-recorded' : 'local-ready-remote-unverified') : health, requiredAccess: instance.requiredAccess };
|
|
126
167
|
}
|
|
168
|
+
export async function stopInstance(instance, home = homedir(), options = {}) {
|
|
169
|
+
const timeoutMs = options.timeoutMs ?? 65000;
|
|
170
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)
|
|
171
|
+
throw Error('INSTANCE_STOP_TIMEOUT_INVALID');
|
|
172
|
+
const deadline = Date.now() + timeoutMs;
|
|
173
|
+
const owner = await ownerFor(instance);
|
|
174
|
+
if (!owner || !alive(owner.pid)) {
|
|
175
|
+
const status = await instanceStatus(instance, home);
|
|
176
|
+
if (status.phase !== 'stopped')
|
|
177
|
+
throw Error(status.runtimeProblem ?? 'INSTANCE_STOP_UNVERIFIED');
|
|
178
|
+
return { result: 'ALREADY_STOPPED', instanceId: instance.id, runId: null, restarted: false };
|
|
179
|
+
}
|
|
180
|
+
await exchange(instance, owner, 'stop', undefined, Math.max(1, Math.min(2000, deadline - Date.now())));
|
|
181
|
+
const lastFile = path.join(instancePaths(instance, home).snapshots, 'last-runtime.json');
|
|
182
|
+
// The terminal snapshot is committed only after worker/lease/socket cleanup.
|
|
183
|
+
// Do not use a free port or a dead PID alone as proof of successful shutdown.
|
|
184
|
+
while (Date.now() < deadline) {
|
|
185
|
+
let current;
|
|
186
|
+
try {
|
|
187
|
+
current = await ownerFor(instance);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
if (e.code === 'ENOENT') {
|
|
191
|
+
await pause(25);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
throw e;
|
|
195
|
+
}
|
|
196
|
+
const lastRun = await snapshot(lastFile, runtimeStateSchema);
|
|
197
|
+
if (lastRun?.runId === owner.runId && lastRun.phase === 'stopped' && current?.runId !== owner.runId) {
|
|
198
|
+
return { result: 'STOPPED', instanceId: instance.id, runId: owner.runId, restarted: current !== null };
|
|
199
|
+
}
|
|
200
|
+
if (lastRun?.runId === owner.runId && lastRun.phase === 'error')
|
|
201
|
+
throw Error('INSTANCE_STOP_FAILED: ' + lastRun.problem);
|
|
202
|
+
await pause(Math.min(50, Math.max(1, deadline - Date.now())));
|
|
203
|
+
}
|
|
204
|
+
throw Error('INSTANCE_STOP_TIMEOUT: cleanup was not confirmed; run starmux doctor');
|
|
205
|
+
}
|
|
127
206
|
export async function runInstance(instance, options = {}) {
|
|
128
207
|
const home = options.home ?? homedir();
|
|
129
208
|
const parent = options.appParentPid;
|
|
@@ -133,9 +212,10 @@ export async function runInstance(instance, options = {}) {
|
|
|
133
212
|
throw Error('INSTANCE_ARCHIVED');
|
|
134
213
|
const initial = await loadInstanceConfig(instance);
|
|
135
214
|
await ensureAccessReady(await resolveInstance(instance.id, home), home);
|
|
215
|
+
const identity = await runtimeIdentity();
|
|
136
216
|
const p = runtimePaths(instance.id);
|
|
137
217
|
createPrivateDirectory(controlRoot());
|
|
138
|
-
const owner = { instanceId: instance.id, runId: randomBytes(16).toString('hex'), pid: process.pid, token: randomBytes(32).toString('hex'), launcher: parent ? 'app' : 'cli', appParentPid: parent ?? null };
|
|
218
|
+
const owner = { protocolVersion: INSTANCE_CONTROL_VERSION, port: initial.config.port, instanceId: instance.id, runId: randomBytes(16).toString('hex'), pid: process.pid, token: randomBytes(32).toString('hex'), launcher: parent ? 'app' : 'cli', appParentPid: parent ?? null };
|
|
139
219
|
await changeRegistry(async (registry) => {
|
|
140
220
|
const registered = registry.instances.find(i => i.id === instance.id);
|
|
141
221
|
if (!registered || registered.archived)
|
|
@@ -165,14 +245,15 @@ export async function runInstance(instance, options = {}) {
|
|
|
165
245
|
}
|
|
166
246
|
await atomicPrivate(p.meta, JSON.stringify(owner));
|
|
167
247
|
}, home);
|
|
168
|
-
const
|
|
169
|
-
const state = { instanceId: instance.id, runId: owner.runId, pid: process.pid, launcher: owner.launcher, appParentPid: parent ?? null, executionMode: options.background ? 'background' : 'foreground', phase: 'starting', port: null, active: null, problem: null, ...identity };
|
|
248
|
+
const state = { protocolVersion: INSTANCE_CONTROL_VERSION, instanceId: instance.id, runId: owner.runId, pid: process.pid, launcher: owner.launcher, appParentPid: parent ?? null, executionMode: options.background ? 'background' : 'foreground', phase: 'starting', port: null, active: null, problem: null, ...identity };
|
|
170
249
|
let worker = null;
|
|
171
250
|
let portLease = null;
|
|
172
251
|
let server = null;
|
|
173
252
|
let ending = false;
|
|
174
253
|
let expectedWorkerExit = false;
|
|
175
254
|
let sequence = Promise.resolve();
|
|
255
|
+
let stopRequest = null;
|
|
256
|
+
const clients = new Set();
|
|
176
257
|
let finish;
|
|
177
258
|
const finished = new Promise(r => { finish = r; });
|
|
178
259
|
const serialize = (fn) => { const next = sequence.then(fn); sequence = next.catch(() => { }); return next; };
|
|
@@ -189,22 +270,40 @@ export async function runInstance(instance, options = {}) {
|
|
|
189
270
|
if (!child)
|
|
190
271
|
return;
|
|
191
272
|
expectedWorkerExit = true;
|
|
192
|
-
await new Promise(resolve => {
|
|
273
|
+
await new Promise((resolve, reject) => {
|
|
193
274
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
194
275
|
resolve();
|
|
195
276
|
return;
|
|
196
277
|
}
|
|
197
278
|
let forced = false;
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
state.problem = 'WORKER_STOP_FORCED';
|
|
279
|
+
const finish = (error) => { clearTimeout(grace); clearTimeout(deadline); child.off('exit', exited); child.off('error', failed); error ? reject(error) : resolve(); };
|
|
280
|
+
const exited = () => { if (forced)
|
|
281
|
+
state.problem = 'WORKER_STOP_FORCED'; finish(); };
|
|
282
|
+
const failed = (error) => finish(error);
|
|
283
|
+
const grace = setTimeout(() => { forced = true; try {
|
|
284
|
+
child.kill('SIGKILL');
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
finish(e);
|
|
288
|
+
} }, 8000);
|
|
289
|
+
const deadline = setTimeout(() => finish(Error('INSTANCE_WORKER_STOP_TIMEOUT')), 12000);
|
|
290
|
+
child.once('exit', exited);
|
|
291
|
+
child.once('error', failed);
|
|
201
292
|
if (child.connected)
|
|
202
|
-
child.send({ type: 'gateway-stop' })
|
|
293
|
+
child.send({ type: 'gateway-stop' }, error => { if (error && child.exitCode === null && child.signalCode === null)
|
|
294
|
+
try {
|
|
295
|
+
child.kill('SIGTERM');
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
finish(e);
|
|
299
|
+
} });
|
|
203
300
|
else
|
|
204
301
|
child.kill('SIGTERM');
|
|
205
302
|
});
|
|
206
303
|
worker = null;
|
|
207
304
|
expectedWorkerExit = false;
|
|
305
|
+
state.active = null;
|
|
306
|
+
state.port = null;
|
|
208
307
|
}
|
|
209
308
|
async function shutdown(reason) {
|
|
210
309
|
if (ending)
|
|
@@ -215,11 +314,24 @@ export async function runInstance(instance, options = {}) {
|
|
|
215
314
|
await stopWorker();
|
|
216
315
|
await portLease?.release();
|
|
217
316
|
portLease = null;
|
|
218
|
-
state.phase = 'stopped';
|
|
219
317
|
state.port = null;
|
|
220
|
-
emit('INSTANCE_STOPPED');
|
|
221
318
|
finish();
|
|
222
319
|
}
|
|
320
|
+
function requestStop(reason) {
|
|
321
|
+
if (!stopRequest) {
|
|
322
|
+
stopRequest = serialize(() => shutdown(reason));
|
|
323
|
+
void stopRequest.catch(async (error) => {
|
|
324
|
+
state.phase = 'error';
|
|
325
|
+
state.problem = 'INSTANCE_STOP_FAILED: ' + error.message;
|
|
326
|
+
emit('INSTANCE_ERROR');
|
|
327
|
+
try {
|
|
328
|
+
await atomicPrivate(path.join(instancePaths(instance, home).snapshots, 'last-runtime.json'), JSON.stringify(state));
|
|
329
|
+
}
|
|
330
|
+
catch { }
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return stopRequest;
|
|
334
|
+
}
|
|
223
335
|
async function startWorker(config, lease) {
|
|
224
336
|
await ensureAccessReady(await resolveInstance(instance.id, home), home);
|
|
225
337
|
if (!await availablePort(config.config.port))
|
|
@@ -228,10 +340,10 @@ export async function runInstance(instance, options = {}) {
|
|
|
228
340
|
for (const key of childEnvKeys)
|
|
229
341
|
if (process.env[key])
|
|
230
342
|
env[key] = process.env[key];
|
|
231
|
-
Object.assign(env, instanceEnv(instance, config.config, home), {
|
|
343
|
+
Object.assign(env, instanceEnv(instance, config.config, home), { STARMUX_INTERNAL_SETUP_OWNER_PID: String(process.pid), STARMUX_INTERNAL_SETUP_RUN_ID: lease.owner.runId, STARMUX_INTERNAL_SETUP_TOKEN: lease.owner.token });
|
|
232
344
|
const upstreamFile = await prepareUpstreams(instance, config.config, home);
|
|
233
345
|
if (upstreamFile)
|
|
234
|
-
env.
|
|
346
|
+
env.STARMUX_UPSTREAMS_FILE = upstreamFile;
|
|
235
347
|
const log = path.join(instancePaths(instance, home).logs, 'core.log');
|
|
236
348
|
if (await exists(log))
|
|
237
349
|
await readTrusted(log, true, 32 * 1024 * 1024);
|
|
@@ -254,6 +366,7 @@ export async function runInstance(instance, options = {}) {
|
|
|
254
366
|
child.on('message', ready);
|
|
255
367
|
});
|
|
256
368
|
state.active = config;
|
|
369
|
+
state.permissionMode = env.STARMUX_PERMISSION_MODE === 'global' ? 'global' : 'workspace';
|
|
257
370
|
state.port = config.config.port;
|
|
258
371
|
state.phase = 'running';
|
|
259
372
|
lease.markRunning();
|
|
@@ -262,13 +375,13 @@ export async function runInstance(instance, options = {}) {
|
|
|
262
375
|
state.phase = 'error';
|
|
263
376
|
state.problem = 'INSTANCE_WORKER_EXITED';
|
|
264
377
|
emit('INSTANCE_ERROR');
|
|
265
|
-
void
|
|
378
|
+
void requestStop('INSTANCE_WORKER_EXITED');
|
|
266
379
|
} });
|
|
267
380
|
emit('INSTANCE_RUNNING');
|
|
268
381
|
}
|
|
269
382
|
const active = (loaded) => ({ config: loaded.config, revision: loaded.revision, effectiveRevision: loaded.effectiveRevision, sourcePath: loaded.source.path });
|
|
270
383
|
async function apply(revision) {
|
|
271
|
-
if (ending)
|
|
384
|
+
if (ending || stopRequest)
|
|
272
385
|
throw Error('INSTANCE_STOPPING');
|
|
273
386
|
const current = await resolveInstance(instance.id, home);
|
|
274
387
|
const next = await loadInstanceConfig(current);
|
|
@@ -285,7 +398,7 @@ export async function runInstance(instance, options = {}) {
|
|
|
285
398
|
if (!await availablePort(next.config.port))
|
|
286
399
|
throw Error('PORT_IN_USE: ' + next.config.port);
|
|
287
400
|
candidateLease = await acquireControl(next.config.port, { launcher: owner.launcher });
|
|
288
|
-
candidateLease.onStop(
|
|
401
|
+
candidateLease.onStop(() => requestStop());
|
|
289
402
|
}
|
|
290
403
|
state.phase = 'starting';
|
|
291
404
|
await stopWorker();
|
|
@@ -314,7 +427,7 @@ export async function runInstance(instance, options = {}) {
|
|
|
314
427
|
throw Error(state.problem);
|
|
315
428
|
}
|
|
316
429
|
}
|
|
317
|
-
const signal = () => { void
|
|
430
|
+
const signal = () => { void requestStop(); };
|
|
318
431
|
const parentTimer = parent ? setInterval(() => { if (process.ppid !== parent)
|
|
319
432
|
signal(); }, 400) : null;
|
|
320
433
|
parentTimer?.unref();
|
|
@@ -322,6 +435,8 @@ export async function runInstance(instance, options = {}) {
|
|
|
322
435
|
process.on('SIGINT', signal);
|
|
323
436
|
try {
|
|
324
437
|
server = createServer({ allowHalfOpen: true }, socket => {
|
|
438
|
+
clients.add(socket);
|
|
439
|
+
socket.once('close', () => clients.delete(socket));
|
|
325
440
|
socket.setTimeout(65000, () => socket.destroy());
|
|
326
441
|
let raw = '';
|
|
327
442
|
let handled = false;
|
|
@@ -343,21 +458,25 @@ export async function runInstance(instance, options = {}) {
|
|
|
343
458
|
const envelope = JSON.parse(raw);
|
|
344
459
|
if (typeof envelope.body !== 'string' || !equal(envelope.proof, signature(owner.token, envelope.body)))
|
|
345
460
|
throw Error('CONTROL_DENIED');
|
|
346
|
-
const
|
|
347
|
-
nonce
|
|
348
|
-
|
|
461
|
+
const parsed = JSON.parse(envelope.body);
|
|
462
|
+
// Recover the nonce before validating the version so an authenticated
|
|
463
|
+
// caller receives an explicit protocol error rather than a bad proof.
|
|
464
|
+
if (parsed && typeof parsed === 'object' && 'nonce' in parsed && typeof parsed.nonce === 'string')
|
|
465
|
+
nonce = parsed.nonce;
|
|
466
|
+
checkInstanceProtocol(parsed);
|
|
467
|
+
const req = controlRequestSchema.parse(parsed);
|
|
468
|
+
if (req.runId !== owner.runId)
|
|
349
469
|
throw Error('CONTROL_DENIED');
|
|
350
|
-
if (req.method === '
|
|
351
|
-
await serialize(shutdown);
|
|
352
|
-
else if (req.method === 'apply')
|
|
470
|
+
if (req.method === 'apply')
|
|
353
471
|
await serialize(() => apply(req.revision));
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
472
|
+
const body = JSON.stringify({ protocolVersion: INSTANCE_CONTROL_VERSION, state, result: req.method === 'stop' ? 'STOP_ACCEPTED' : 'STATE' });
|
|
473
|
+
socket.end(JSON.stringify({ body, proof: signature(owner.token, nonce + body) }) + '\n', () => {
|
|
474
|
+
if (req.method === 'stop')
|
|
475
|
+
void requestStop();
|
|
476
|
+
});
|
|
358
477
|
}
|
|
359
478
|
catch (error) {
|
|
360
|
-
const body = JSON.stringify({ state, error: error.message });
|
|
479
|
+
const body = JSON.stringify({ protocolVersion: INSTANCE_CONTROL_VERSION, state, result: 'STATE', error: error.message });
|
|
361
480
|
socket.end(JSON.stringify({ body, proof: signature(owner.token, nonce + body) }) + '\n');
|
|
362
481
|
}
|
|
363
482
|
})();
|
|
@@ -365,7 +484,7 @@ export async function runInstance(instance, options = {}) {
|
|
|
365
484
|
});
|
|
366
485
|
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(p.socket, () => resolve()); });
|
|
367
486
|
portLease = await acquireControl(initial.config.port, { launcher: owner.launcher });
|
|
368
|
-
portLease.onStop(
|
|
487
|
+
portLease.onStop(() => requestStop());
|
|
369
488
|
await serialize(() => startWorker(active(initial), portLease));
|
|
370
489
|
if (options.onReady)
|
|
371
490
|
await options.onReady();
|
|
@@ -383,14 +502,34 @@ export async function runInstance(instance, options = {}) {
|
|
|
383
502
|
clearInterval(parentTimer);
|
|
384
503
|
process.off('SIGTERM', signal);
|
|
385
504
|
process.off('SIGINT', signal);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
505
|
+
try {
|
|
506
|
+
await stopWorker();
|
|
507
|
+
await portLease?.release();
|
|
508
|
+
if (server)
|
|
509
|
+
await new Promise((resolve, reject) => {
|
|
510
|
+
const timer = setTimeout(() => { for (const client of clients)
|
|
511
|
+
client.destroy(); }, 1500);
|
|
512
|
+
server.close(error => { clearTimeout(timer); error ? reject(error) : resolve(); });
|
|
513
|
+
});
|
|
514
|
+
if (ending)
|
|
515
|
+
state.phase = 'stopped';
|
|
516
|
+
// Commit completion last. A stop acknowledgement only means accepted;
|
|
517
|
+
// readers require this snapshot AND release of this run's owner record.
|
|
518
|
+
await atomicPrivate(path.join(instancePaths(instance, home).snapshots, 'last-runtime.json'), JSON.stringify(state));
|
|
519
|
+
const current = await ownerFor(instance);
|
|
520
|
+
if (current?.runId === owner.runId)
|
|
521
|
+
await rm(p.root, { recursive: true, force: true });
|
|
522
|
+
if (state.phase === 'stopped')
|
|
523
|
+
emit('INSTANCE_STOPPED');
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
state.phase = 'error';
|
|
527
|
+
state.problem = 'INSTANCE_CLEANUP_FAILED: ' + error.message;
|
|
528
|
+
try {
|
|
529
|
+
await atomicPrivate(path.join(instancePaths(instance, home).snapshots, 'last-runtime.json'), JSON.stringify(state));
|
|
530
|
+
}
|
|
531
|
+
catch { }
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
394
534
|
}
|
|
395
535
|
}
|
|
396
|
-
//# sourceMappingURL=instance-runtime.js.map
|
|
@@ -10,8 +10,10 @@ import { z } from 'zod';
|
|
|
10
10
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
11
11
|
import { validateConfiguredUpstreams } from './configured-mcp-upstreams.js';
|
|
12
12
|
import { createPrivateDirectory, ownedByUser } from './platform.js';
|
|
13
|
-
import {
|
|
13
|
+
import { supportedHost } from './platform.js';
|
|
14
|
+
import { createProjectPolicy } from './project-workspace-policy.js';
|
|
14
15
|
import { createAccessControl } from './access-control.js';
|
|
16
|
+
import { createAccountAccessPolicy } from './account-access-policy.js';
|
|
15
17
|
const nameSchema = z.string().trim().min(1).max(80).regex(/^[^\x00-\x1f\x7f/\\]+$/);
|
|
16
18
|
const sourceSchema = z.object({ kind: z.enum(['managed', 'linked']), path: z.string() }).strict();
|
|
17
19
|
export const instanceSchema = z.object({ id: z.string().uuid(), name: nameSchema, source: sourceSchema, archived: z.boolean().default(false),
|
|
@@ -118,17 +120,6 @@ export function parseConfig(text) {
|
|
|
118
120
|
const prefix = text.slice(0, e.offset);
|
|
119
121
|
throw Error(`CONFIG_JSON_INVALID line ${prefix.split('\n').length}, column ${e.offset - (prefix.lastIndexOf('\n') + 1) + 1}: ${printParseErrorCode(e.error)}`);
|
|
120
122
|
}
|
|
121
|
-
// Import the three historical onboarding formats without changing their authority.
|
|
122
|
-
if (value && typeof value === 'object' && 'version' in value) {
|
|
123
|
-
const legacy = value;
|
|
124
|
-
if (![1, 2, 3].includes(Number(legacy.version)))
|
|
125
|
-
throw Error('CONFIG_VERSION_UNSUPPORTED');
|
|
126
|
-
const allowed = new Set(['version', 'projectRoot', 'mode', 'connection', 'port', 'accessRole', 'writeScope']);
|
|
127
|
-
if (Object.keys(legacy).some(k => !allowed.has(k)))
|
|
128
|
-
throw Error('CONFIG_UNKNOWN_LEGACY_FIELD');
|
|
129
|
-
value = { schemaVersion: 1, projectRoot: legacy.projectRoot, mode: legacy.mode, connection: legacy.connection, port: legacy.port,
|
|
130
|
-
accessRole: legacy.version === 1 ? 'restricted' : legacy.accessRole, writeScope: legacy.version === 3 ? 'home-only' : 'unrestricted' };
|
|
131
|
-
}
|
|
132
123
|
const result = instanceConfigSchema.safeParse(value);
|
|
133
124
|
if (!result.success)
|
|
134
125
|
throw Error('CONFIG_INVALID: ' + result.error.issues.map(i => `${i.path.join('.') || 'config'}: ${i.message}`).join('; '));
|
|
@@ -138,8 +129,19 @@ export async function validateConfig(config) {
|
|
|
138
129
|
const { accessRole, writeScope } = config;
|
|
139
130
|
if (writeScope === 'home-only' && (accessRole !== 'trusted' || config.upstreams.length || config.gitMode !== 'off'))
|
|
140
131
|
throw Error('CONFIG_INVALID writeScope: home-only requires trusted mode without extra upstreams or Gateway Git');
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
supportedHost();
|
|
133
|
+
instanceConfigSchema.parse(config);
|
|
134
|
+
if (!path.isAbsolute(config.projectRoot))
|
|
135
|
+
throw Error('CONFIG_INVALID projectRoot: absolute path required');
|
|
136
|
+
if (accessRole === 'trusted' && config.mode !== 'read-write')
|
|
137
|
+
throw Error('CONFIG_ACCESS_ROLE_MISMATCH');
|
|
138
|
+
if (writeScope === 'home-only' && process.platform !== 'darwin')
|
|
139
|
+
throw Error('HOME_WRITE_SANDBOX_PLATFORM_UNSUPPORTED');
|
|
140
|
+
if (usesGlobalPermissions(config))
|
|
141
|
+
await createAccountAccessPolicy(config.projectRoot, config.mode);
|
|
142
|
+
else
|
|
143
|
+
await createProjectPolicy({ root: config.projectRoot, mode: config.mode,
|
|
144
|
+
developerCommands: accessRole === 'trusted' ? 'trusted-project' : 'presets', allowTrustedHomeRoot: accessRole === 'trusted' });
|
|
143
145
|
validateConfiguredUpstreams({ version: 1, upstreams: config.upstreams.map(({ envRefs, ...u }) => ({ ...u, env: Object.fromEntries(Object.keys(envRefs).map(k => [k, 'reference'])) })) });
|
|
144
146
|
return config;
|
|
145
147
|
}
|
|
@@ -185,7 +187,7 @@ export async function createInstance(name, options = {}, home = homedir()) {
|
|
|
185
187
|
const id = randomUUID();
|
|
186
188
|
const root = path.join(instancesRoot(home), 'instances', id);
|
|
187
189
|
const instance = instanceSchema.parse({ id, name, archived: false, requiredAccess: options.requiredAccess ?? 'ingress',
|
|
188
|
-
source: { kind: options.link ? 'linked' : 'managed', path: options.link ? input : path.join(root, 'config.jsonc') }
|
|
190
|
+
source: { kind: options.link ? 'linked' : 'managed', path: options.link ? input : path.join(root, 'config.jsonc') } });
|
|
189
191
|
createPrivateDirectory(path.join(instancesRoot(home), 'instances'));
|
|
190
192
|
createPrivateDirectory(root);
|
|
191
193
|
for (const dir of ['state', 'logs', 'snapshots'])
|
|
@@ -193,7 +195,7 @@ export async function createInstance(name, options = {}, home = homedir()) {
|
|
|
193
195
|
try {
|
|
194
196
|
await atomicPrivate(path.join(instancesRoot(home), 'gateway.schema.json'), JSON.stringify(configJSONSchema(), null, 2) + '\n');
|
|
195
197
|
if (!options.link) {
|
|
196
|
-
const original = text
|
|
198
|
+
const original = text ?? configTemplate(config);
|
|
197
199
|
await atomicPrivate(instance.source.path, applyEdits(original, modify(original, ['$schema'], '../../gateway.schema.json', { formattingOptions: { insertSpaces: true, tabSize: 2 } })));
|
|
198
200
|
}
|
|
199
201
|
registry.instances.push(instance);
|
|
@@ -234,13 +236,18 @@ export async function patchInstanceConfig(instance, patch, revision, home = home
|
|
|
234
236
|
text = applyEdits(text, modify(text, [key], value, { formattingOptions: { insertSpaces: true, tabSize: 2 } }));
|
|
235
237
|
return saveInstanceConfig(instance, text, revision, home);
|
|
236
238
|
}
|
|
239
|
+
export function usesGlobalPermissions(config) {
|
|
240
|
+
return config.writeScope === 'unrestricted' &&
|
|
241
|
+
(config.accessRole === 'restricted' && config.mode === 'read-only' || config.accessRole === 'trusted' && config.mode === 'read-write');
|
|
242
|
+
}
|
|
237
243
|
export function instanceEnv(instance, config, home = homedir()) {
|
|
238
|
-
const legacy = { projectRoot: config.projectRoot, mode: config.mode, port: config.port, connection: config.connection === 'local' ? 'https' : config.connection,
|
|
239
|
-
...(config.accessRole === 'restricted' && config.mode === 'read-write' ? { version: 1 } : config.writeScope === 'home-only' ? { version: 3, accessRole: config.accessRole, writeScope: 'home-only' } : { version: 2, accessRole: config.accessRole }) };
|
|
240
244
|
const p = instancePaths(instance, home);
|
|
241
|
-
return {
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
return { STARMUX_PROJECT_ROOT: config.projectRoot, STARMUX_PROJECT_PORT: String(config.port),
|
|
246
|
+
STARMUX_WORKSPACE_MODE: config.mode, STARMUX_DEV_COMMANDS: config.accessRole === 'trusted' ? 'trusted-project' : 'presets',
|
|
247
|
+
STARMUX_DC_ACCESS_MODE: config.accessRole === 'trusted' ? 'native' : 'workspace', STARMUX_DC_WRITE_SCOPE: config.writeScope,
|
|
248
|
+
STARMUX_ACCESS_FILE: p.access, STARMUX_GRANTS_FILE: p.grants, STARMUX_DENY_FILE: p.deny,
|
|
249
|
+
STARMUX_GIT_MODE: config.gitMode, STARMUX_WORKTREE_MODE: config.worktreeMode, STARMUX_INSTANCE_ID: instance.id, STARMUX_INSTANCE_NAME: instance.name,
|
|
250
|
+
STARMUX_PRIVATE_ACTIVITY_OBSERVER: '1', STARMUX_PERMISSION_MODE: usesGlobalPermissions(config) ? 'global' : 'workspace' };
|
|
244
251
|
}
|
|
245
252
|
export async function ensureAccessReady(instance, home = homedir()) {
|
|
246
253
|
const access = await createAccessControl(instancePaths(instance, home).access);
|
|
@@ -266,4 +273,3 @@ export async function prepareUpstreams(instance, config, home = homedir()) {
|
|
|
266
273
|
await atomicPrivate(file, JSON.stringify({ version: 1, upstreams: definitions }));
|
|
267
274
|
return file;
|
|
268
275
|
}
|
|
269
|
-
//# sourceMappingURL=instance-store.js.map
|
|
@@ -2,8 +2,7 @@ import { verifyInternalSetupWorker } from './global-core-control.js';
|
|
|
2
2
|
import { runGateway } from './dc-workspace-http.js';
|
|
3
3
|
if (!process.send)
|
|
4
4
|
throw Error('INSTANCE_WORKER_REQUIRES_SUPERVISOR');
|
|
5
|
-
verifyInternalSetupWorker({ port: Number(process.env.
|
|
6
|
-
runId: process.env.
|
|
5
|
+
verifyInternalSetupWorker({ port: Number(process.env.STARMUX_PROJECT_PORT), ownerPid: Number(process.env.STARMUX_INTERNAL_SETUP_OWNER_PID),
|
|
6
|
+
runId: process.env.STARMUX_INTERNAL_SETUP_RUN_ID, token: process.env.STARMUX_INTERNAL_SETUP_TOKEN });
|
|
7
7
|
await runGateway();
|
|
8
8
|
process.send({ type: 'gateway-ready' });
|
|
9
|
-
//# sourceMappingURL=instance-worker.js.map
|
package/dist/src/invocation.js
CHANGED
|
@@ -61,7 +61,7 @@ export function createInvocationPipeline(options) {
|
|
|
61
61
|
catch (error) {
|
|
62
62
|
const reason = errorCode(error) || 'DENY_RULE_CHECK_FAILED';
|
|
63
63
|
audit({ event: 'TOOL_EXPLICITLY_DENIED', requestId: request.requestId, tool: invocation.name, reason });
|
|
64
|
-
return { isError: true, content: [{ type: 'text', text: reason + ': Review filters on the
|
|
64
|
+
return { isError: true, content: [{ type: 'text', text: reason + ': Review filters on the local owner Dashboard /filters' }] };
|
|
65
65
|
}
|
|
66
66
|
const result = await options.dispatch(invocation, request);
|
|
67
67
|
for (const hook of hooks) {
|
|
@@ -77,4 +77,3 @@ export function createInvocationPipeline(options) {
|
|
|
77
77
|
return result; // original MCP result, including unknown extension fields
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
-
//# sourceMappingURL=invocation.js.map
|