@technomoron/agent-run 0.99.39 → 0.99.40
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/CHANGES +11 -0
- package/README.md +32 -7
- package/dist/agents/gemini.js +26 -2
- package/dist/brain/config.js +6 -0
- package/dist/brain/server.js +41 -14
- package/dist/brain/windows-pipe.js +19 -0
- package/dist/commands.js +4 -0
- package/dist/guards.js +3 -2
- package/dist/renderer.js +1 -1
- package/package.json +5 -1
- package/scripts/brain-pipe-security.ps1 +40 -0
- package/scripts/install-windows-brain.ps1 +14 -0
- package/scripts/run-hidden.vbs +15 -0
- package/scripts/run-windows-brain.ps1 +23 -0
package/CHANGES
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
CHANGES
|
|
2
2
|
=======
|
|
3
3
|
|
|
4
|
+
Version 0.99.40 (2026-09-24)
|
|
5
|
+
----------------------------
|
|
6
|
+
* Add native Windows agent-brain service and MCP bridging over per-user named
|
|
7
|
+
pipes, with owner/ACL checks and a hidden sign-in scheduled task installer.
|
|
8
|
+
* Fix recursive Windows command guard resolution after argument scanning and
|
|
9
|
+
normalize CRLF when checking generated profiles.
|
|
10
|
+
* Preserve the complete generated compression prompt through Windows npm shims
|
|
11
|
+
and exercise the integration fixtures with native Windows paths.
|
|
12
|
+
* Load Gemini profile settings from its private user home on Windows, retaining
|
|
13
|
+
shared preferences without requiring administrator-owned system settings.
|
|
14
|
+
|
|
4
15
|
Version 0.99.39 (2026-09-11)
|
|
5
16
|
----------------------------
|
|
6
17
|
* Fix the AI tools updater failing under sudo or a root shell with inherited
|
package/README.md
CHANGED
|
@@ -334,9 +334,13 @@ Gemini runs from the real project directory with a private
|
|
|
334
334
|
`GEMINI_CLI_HOME`. Its generated system settings configure `AGENTS.md` as the
|
|
335
335
|
context filename and add the private runtime plus project memory as context
|
|
336
336
|
directories. Generated skills use Gemini's `.agents/skills` alias. Existing
|
|
337
|
-
Gemini
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
Gemini login credentials, commands, and user skills are linked into the private
|
|
338
|
+
home when present. On Windows, shared user preferences and generated settings
|
|
339
|
+
are merged into the private home's `.gemini/settings.json`; shared settings
|
|
340
|
+
remain untouched. This avoids Gemini's administrator-only system-settings ACL
|
|
341
|
+
requirement. On other platforms, user settings are linked and generated system
|
|
342
|
+
settings remain the highest-priority project layer. Gemini's normal folder-trust
|
|
343
|
+
and sign-in requirements still apply.
|
|
340
344
|
|
|
341
345
|
Grok runs from the real project directory with `GROK_HOME` set to
|
|
342
346
|
`live/grok/`. Grok natively reads the generated home-level `AGENTS.md`,
|
|
@@ -510,7 +514,8 @@ command stages moves but never commits them.
|
|
|
510
514
|
|
|
511
515
|
`agent-brain` gives the native agents the same project knowledge, skills, and
|
|
512
516
|
tasks through MCP. It uses the official Node MCP SDK. The persistent
|
|
513
|
-
service runs under `@technomoron/apicore-server` on a private Unix socket
|
|
517
|
+
service runs under `@technomoron/apicore-server` on a private Unix socket or a
|
|
518
|
+
Windows named pipe restricted to the current user, SYSTEM, and administrators.
|
|
514
519
|
Node 24 or newer is required; SQLite and FTS come from Node, with no database
|
|
515
520
|
package to compile.
|
|
516
521
|
|
|
@@ -978,8 +983,8 @@ not performed; the service can optionally pull at startup as described below.
|
|
|
978
983
|
|
|
979
984
|
### Persistent service under apicore-server
|
|
980
985
|
|
|
981
|
-
All native agents connect to the same long-lived service for the
|
|
982
|
-
Start it on Linux or
|
|
986
|
+
All native agents connect to the same long-lived service for the current user.
|
|
987
|
+
Start it on Linux, macOS, or Windows:
|
|
983
988
|
|
|
984
989
|
```sh
|
|
985
990
|
agent-brain serve
|
|
@@ -1005,7 +1010,27 @@ for a particular service. The bridge verifies the socket's ownership and private
|
|
|
1005
1010
|
permissions. A missing or unreachable socket is an error; start the service
|
|
1006
1011
|
before connecting. There is no standalone stdio server or TCP fallback. Apicore
|
|
1007
1012
|
supports Unix sockets directly, so localhost binding is unnecessary. The brain
|
|
1008
|
-
service
|
|
1013
|
+
service supports Linux, macOS, and native Windows.
|
|
1014
|
+
|
|
1015
|
+
On Windows the default endpoint is `\\.\pipe\agent-brain-<hash>`, derived from
|
|
1016
|
+
the user's home and configuration root. No TCP port is opened. Windows
|
|
1017
|
+
PowerShell applies a private pipe ACL before requests are accepted; each bridge
|
|
1018
|
+
checks the pipe owner and ACL before connecting. `--socket` can select another
|
|
1019
|
+
local named pipe. Windows removes the pipe automatically when its server exits.
|
|
1020
|
+
|
|
1021
|
+
To start it automatically at Windows sign-in, initialize the desired config root
|
|
1022
|
+
and run the packaged installer in PowerShell:
|
|
1023
|
+
|
|
1024
|
+
```powershell
|
|
1025
|
+
agent-brain init --configdir "$env:USERPROFILE\.agent-run"
|
|
1026
|
+
$prefix = npm.cmd prefix -g
|
|
1027
|
+
& "$prefix\node_modules\@technomoron\agent-run\scripts\install-windows-brain.ps1" -ConfigRoot "$env:USERPROFILE\.agent-run"
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
The `Agent Brain` scheduled task runs hidden as the signed-in user, without
|
|
1031
|
+
elevation, and retries failures. Its log is `<config-root>/runtime/service.log`.
|
|
1032
|
+
After upgrading the package, restart it with `Stop-ScheduledTask` followed by
|
|
1033
|
+
`Start-ScheduledTask` for task name `Agent Brain`.
|
|
1009
1034
|
|
|
1010
1035
|
After a crash, remove a stale socket only after checking that its service is
|
|
1011
1036
|
stopped. Starting a second service on an existing socket fails without replacing
|
package/dist/agents/gemini.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.geminiAdapter = void 0;
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const path = require("path");
|
|
6
|
+
const fs = require("fs");
|
|
6
7
|
const render_mcp_1 = require("../config/render-mcp");
|
|
7
8
|
const render_settings_1 = require("../config/render-settings");
|
|
8
9
|
const render_skills_1 = require("../config/render-skills");
|
|
@@ -63,7 +64,7 @@ exports.geminiAdapter = {
|
|
|
63
64
|
const sharedGeminiDir = path.join(os.homedir(), '.gemini');
|
|
64
65
|
const privateGeminiDir = path.join(runtime.context.paths.geminiHomeDir, '.gemini');
|
|
65
66
|
for (const name of [
|
|
66
|
-
'settings.json',
|
|
67
|
+
...(process.platform === 'win32' ? [] : ['settings.json']),
|
|
67
68
|
'oauth_creds.json',
|
|
68
69
|
'google_accounts.json',
|
|
69
70
|
'trustedFolders.json',
|
|
@@ -76,6 +77,19 @@ exports.geminiAdapter = {
|
|
|
76
77
|
]) {
|
|
77
78
|
(0, shared_state_1.linkSharedEntry)(path.join(sharedGeminiDir, name), path.join(privateGeminiDir, name), `Gemini ${name}`);
|
|
78
79
|
}
|
|
80
|
+
if (process.platform === 'win32') {
|
|
81
|
+
// Gemini now requires administrator-owned system settings on Windows.
|
|
82
|
+
// Keep the profile in its private user home, without modifying shared settings.
|
|
83
|
+
const settings = path.join(privateGeminiDir, 'settings.json');
|
|
84
|
+
const read = (file) => fs.existsSync(file)
|
|
85
|
+
? (0, render_settings_1.parseJsonObject)(fs.readFileSync(file, 'utf8'), 'Gemini user settings') : {};
|
|
86
|
+
const merged = mergeSettings(mergeSettings(read(path.join(sharedGeminiDir, 'settings.json')), read(settings)), read(runtime.configFiles[0]));
|
|
87
|
+
fs.mkdirSync(privateGeminiDir, { recursive: true });
|
|
88
|
+
// Atomic replacement also detaches an older shared-settings symlink.
|
|
89
|
+
const temporary = `${settings}.${process.pid}.tmp`;
|
|
90
|
+
fs.writeFileSync(temporary, `${JSON.stringify(merged, null, 2)}\n`, { flag: 'wx' });
|
|
91
|
+
fs.renameSync(temporary, settings);
|
|
92
|
+
}
|
|
79
93
|
},
|
|
80
94
|
spawn(runtime, options) {
|
|
81
95
|
(0, shared_1.assertRuntimeFile)('gemini', runtime.instructionFile, runtime.context.profileDir);
|
|
@@ -92,7 +106,7 @@ exports.geminiAdapter = {
|
|
|
92
106
|
env: {
|
|
93
107
|
...(0, environment_1.buildAgentEnvironment)(runtime.context),
|
|
94
108
|
GEMINI_CLI_HOME: runtime.context.paths.geminiHomeDir,
|
|
95
|
-
GEMINI_CLI_SYSTEM_SETTINGS_PATH: runtime.configFiles[0],
|
|
109
|
+
...(process.platform === 'win32' ? {} : { GEMINI_CLI_SYSTEM_SETTINGS_PATH: runtime.configFiles[0] }),
|
|
96
110
|
GEMINI_CLI_TRUSTED_FOLDERS_PATH: sharedTrustedFolders
|
|
97
111
|
},
|
|
98
112
|
onExit: (0, shared_1.withPostflight)(runtime, options.wrapperArgs)
|
|
@@ -105,6 +119,16 @@ exports.geminiAdapter = {
|
|
|
105
119
|
return wrapperArgs.sandboxMode === 'sandboxed' ? ['--sandbox'] : [];
|
|
106
120
|
}
|
|
107
121
|
};
|
|
122
|
+
function mergeSettings(base, override) {
|
|
123
|
+
const result = { ...base };
|
|
124
|
+
for (const [key, value] of Object.entries(override)) {
|
|
125
|
+
if (['__proto__', 'constructor', 'prototype'].includes(key))
|
|
126
|
+
continue;
|
|
127
|
+
result[key] = value && typeof value === 'object' && !Array.isArray(value)
|
|
128
|
+
? mergeSettings(asObject(base[key]), value) : value;
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
108
132
|
function asObject(value) {
|
|
109
133
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
110
134
|
? value
|
package/dist/brain/config.js
CHANGED
|
@@ -9,11 +9,17 @@ exports.registeredBrainProfile = registeredBrainProfile;
|
|
|
9
9
|
exports.registeredBrainProject = registeredBrainProject;
|
|
10
10
|
const fs = require("node:fs");
|
|
11
11
|
const path = require("node:path");
|
|
12
|
+
const os = require("node:os");
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
12
14
|
const jsonc_parser_1 = require("jsonc-parser");
|
|
13
15
|
const yaml_1 = require("yaml");
|
|
14
16
|
const zod_1 = require("zod");
|
|
15
17
|
const utils_1 = require("../utils");
|
|
16
18
|
function defaultSocketPath(configRoot) {
|
|
19
|
+
if (process.platform === 'win32') {
|
|
20
|
+
const identity = `${os.homedir().toLowerCase()}\0${path.resolve(configRoot).toLowerCase()}`;
|
|
21
|
+
return `\\\\.\\pipe\\agent-brain-${(0, node_crypto_1.createHash)('sha256').update(identity).digest('hex').slice(0, 32)}`;
|
|
22
|
+
}
|
|
17
23
|
return path.join(configRoot, 'runtime', 'agent-brain.sock');
|
|
18
24
|
}
|
|
19
25
|
exports.brainConfigSchema = zod_1.z.object({
|
package/dist/brain/server.js
CHANGED
|
@@ -20,6 +20,7 @@ const skills_1 = require("./skills");
|
|
|
20
20
|
const todos_1 = require("./todos");
|
|
21
21
|
const connectors_1 = require("./connectors");
|
|
22
22
|
const git_1 = require("./git");
|
|
23
|
+
const windows_pipe_1 = require("./windows-pipe");
|
|
23
24
|
var config_1 = require("./config");
|
|
24
25
|
Object.defineProperty(exports, "defaultSocketPath", { enumerable: true, get: function () { return config_1.defaultSocketPath; } });
|
|
25
26
|
function createBrainServer(store) {
|
|
@@ -113,20 +114,26 @@ function createBrainServer(store) {
|
|
|
113
114
|
return server;
|
|
114
115
|
}
|
|
115
116
|
function requirePrivateSocketDirectory(socketPath) {
|
|
116
|
-
if (process.platform === 'win32')
|
|
117
|
-
throw new Error('The agent-brain Unix-socket service requires Linux or macOS.');
|
|
118
117
|
const stat = fs.lstatSync(path.dirname(socketPath));
|
|
119
118
|
if (!stat.isDirectory() || stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0)
|
|
120
119
|
throw new Error('The socket directory must be owned by the current user with mode 0700.');
|
|
121
120
|
}
|
|
122
121
|
async function serveApiCore(configRoot, socketPath) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
if (process.platform !== 'win32') {
|
|
123
|
+
const directory = path.dirname(socketPath);
|
|
124
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
125
|
+
requirePrivateSocketDirectory(socketPath);
|
|
126
|
+
if (fs.existsSync(socketPath))
|
|
127
|
+
throw new Error(`Socket already exists: ${socketPath}. Stop the running service, or remove the socket after confirming it is stale.`);
|
|
128
|
+
}
|
|
129
|
+
else if (!/^\\\\\.\\pipe\\[a-zA-Z0-9._-]+$/.test(socketPath)) {
|
|
130
|
+
throw new Error('On Windows, --socket must be a local named pipe (\\\\.\\pipe\\name).');
|
|
131
|
+
}
|
|
132
|
+
let ready = false;
|
|
128
133
|
const api = new apicore_server_1.ApiServer({ authApi: false, origins: [], devMode: false });
|
|
129
134
|
api.fastify.post('/mcp', async (request, reply) => {
|
|
135
|
+
if (!ready)
|
|
136
|
+
return reply.code(503).send();
|
|
130
137
|
if (request.headers.origin)
|
|
131
138
|
return reply.code(403).send({ error: 'Browser requests are not supported' });
|
|
132
139
|
if (request.headers['x-agent-brain-root'] !== encodeURIComponent(path.resolve(configRoot)))
|
|
@@ -158,16 +165,36 @@ async function serveApiCore(configRoot, socketPath) {
|
|
|
158
165
|
});
|
|
159
166
|
api.fastify.route({ method: ['GET', 'DELETE'], url: '/mcp', handler: async (_request, reply) => reply.code(405).send() });
|
|
160
167
|
await api.fastify.listen({ path: socketPath });
|
|
161
|
-
|
|
168
|
+
try {
|
|
169
|
+
if (process.platform === 'win32')
|
|
170
|
+
await (0, windows_pipe_1.checkWindowsPipe)(socketPath, true);
|
|
171
|
+
else
|
|
172
|
+
fs.chmodSync(socketPath, 0o600);
|
|
173
|
+
ready = true;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
await api.fastify.close();
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
162
179
|
return { close: () => api.fastify.close() };
|
|
163
180
|
}
|
|
164
181
|
async function proxyStdio(socketPath, cwd, configRoot) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
182
|
+
if (process.platform === 'win32') {
|
|
183
|
+
try {
|
|
184
|
+
await (0, windows_pipe_1.checkWindowsPipe)(socketPath);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
throw new Error(`Agent-brain service is not running or its pipe is not private at ${socketPath}. Start agent-brain serve with the same --configdir and --socket. ${String(error)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const socket = fs.lstatSync(socketPath, { throwIfNoEntry: false });
|
|
192
|
+
if (!socket)
|
|
193
|
+
throw new Error(`Agent-brain service is not running at ${socketPath}. Start agent-brain serve with the same --configdir and --socket, or enable its systemd user service.`);
|
|
194
|
+
requirePrivateSocketDirectory(socketPath);
|
|
195
|
+
if (!socket.isSocket() || socket.uid !== process.getuid() || (socket.mode & 0o077) !== 0)
|
|
196
|
+
throw new Error('The brain socket must belong to the current user with mode 0600.');
|
|
197
|
+
}
|
|
171
198
|
const stdio = new stdio_js_1.StdioServerTransport();
|
|
172
199
|
let protocolVersion;
|
|
173
200
|
async function send(message) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkWindowsPipe = checkWindowsPipe;
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_util_1 = require("node:util");
|
|
7
|
+
async function checkWindowsPipe(pipePath, secure = false) {
|
|
8
|
+
if (!/^\\\\\.\\pipe\\[a-zA-Z0-9._-]+$/.test(pipePath))
|
|
9
|
+
throw new Error('Expected a local Windows named pipe (\\\\.\\pipe\\name).');
|
|
10
|
+
const powershell = path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
11
|
+
const helper = path.resolve(__dirname, '../../scripts/brain-pipe-security.ps1');
|
|
12
|
+
try {
|
|
13
|
+
await (0, node_util_1.promisify)(node_child_process_1.execFile)(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', helper,
|
|
14
|
+
'-PipePath', pipePath, ...(secure ? ['-Secure'] : [])], { encoding: 'utf8', windowsHide: true, timeout: 15000 });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
throw new Error(`Cannot ${secure ? 'secure' : 'verify'} the brain pipe: ${String(error)}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/commands.js
CHANGED
|
@@ -85,6 +85,10 @@ async function runCompress(command) {
|
|
|
85
85
|
finally {
|
|
86
86
|
store.close();
|
|
87
87
|
}
|
|
88
|
+
// cmd.exe terminates a .cmd launch at a literal newline, even inside quotes.
|
|
89
|
+
// Keep this generated prose prompt in one argument through Windows npm shims.
|
|
90
|
+
if (process.platform === 'win32')
|
|
91
|
+
prompt = prompt.replace(/\r?\n/g, ' ');
|
|
88
92
|
// Use the normal adapters, configuration and permission modes. The prompt is one argv value.
|
|
89
93
|
runTool({ command: agent, args: agent === 'gemini' ? ['-i', prompt] : [prompt], wrapperArgs: {
|
|
90
94
|
none: false, create: false, local: false, show: false, generate: false, sandboxMode: null, codexNetwork: false
|
package/dist/guards.js
CHANGED
|
@@ -140,9 +140,10 @@ function windowsShim(name) {
|
|
|
140
140
|
return [
|
|
141
141
|
'@echo off',
|
|
142
142
|
'setlocal DisableDelayedExpansion',
|
|
143
|
+
'set "AGENT_RUN_GUARD_SELF=%~f0"',
|
|
144
|
+
'for %%D in ("%~dp0.") do set "AGENT_RUN_GUARD_DIR=%%~fD"',
|
|
143
145
|
...guard,
|
|
144
146
|
':run',
|
|
145
|
-
'for %%D in ("%~dp0.") do set "AGENT_RUN_GUARD_DIR=%%~fD"',
|
|
146
147
|
'if defined AGENT_RUN_REAL_PATH (',
|
|
147
148
|
' set "PATH=%AGENT_RUN_REAL_PATH%"',
|
|
148
149
|
') else (',
|
|
@@ -154,7 +155,7 @@ function windowsShim(name) {
|
|
|
154
155
|
` echo agent-run: cannot find the real ${name} command outside the guard directory. 1>&2`,
|
|
155
156
|
' exit /b 127',
|
|
156
157
|
')',
|
|
157
|
-
'if /I "%AGENT_RUN_COMMAND%"=="
|
|
158
|
+
'if /I "%AGENT_RUN_COMMAND%"=="%AGENT_RUN_GUARD_SELF%" (',
|
|
158
159
|
` echo agent-run: refused recursive ${name} guard resolution. 1>&2`,
|
|
159
160
|
' exit /b 127',
|
|
160
161
|
')',
|
package/dist/renderer.js
CHANGED
|
@@ -229,7 +229,7 @@ function checkRenderedFiles(rendered, findings) {
|
|
|
229
229
|
continue;
|
|
230
230
|
}
|
|
231
231
|
const actual = fs.readFileSync(file.path, 'utf8').replace(/\r\n/g, '\n');
|
|
232
|
-
if (actual !== file.content) {
|
|
232
|
+
if (actual !== file.content.replace(/\r\n/g, '\n')) {
|
|
233
233
|
findings.push({ message: `generated file is out of date: ${file.path}`, severity: 'ERROR' });
|
|
234
234
|
}
|
|
235
235
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technomoron/agent-run",
|
|
3
|
-
"version": "0.99.
|
|
3
|
+
"version": "0.99.40",
|
|
4
4
|
"packageManager": "pnpm@10.34.5",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-run": "dist/agent-run.js",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"scripts/install-systemd-jobs.sh",
|
|
18
18
|
"scripts/update-ai-tools.sh",
|
|
19
19
|
"scripts/ensure-agent-brain.sh",
|
|
20
|
+
"scripts/brain-pipe-security.ps1",
|
|
21
|
+
"scripts/install-windows-brain.ps1",
|
|
22
|
+
"scripts/run-windows-brain.ps1",
|
|
23
|
+
"scripts/run-hidden.vbs",
|
|
20
24
|
"README.md",
|
|
21
25
|
"LICENSE",
|
|
22
26
|
"CHANGES"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)][string]$PipePath,
|
|
3
|
+
[switch]$Secure
|
|
4
|
+
)
|
|
5
|
+
$ErrorActionPreference = 'Stop'
|
|
6
|
+
if ($PipePath -notmatch '^\\\\\.\\pipe\\[a-zA-Z0-9._-]+$') { throw 'Expected a local Windows named pipe.' }
|
|
7
|
+
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
8
|
+
$trusted = @($identity.Value, 'S-1-5-18', 'S-1-5-32-544')
|
|
9
|
+
$rights = [System.IO.Pipes.PipeAccessRights]::ReadWrite -bor [System.IO.Pipes.PipeAccessRights]::ReadPermissions
|
|
10
|
+
if ($Secure) { $rights = $rights -bor [System.IO.Pipes.PipeAccessRights]::ChangePermissions }
|
|
11
|
+
# Anonymous impersonation prevents a substituted server from impersonating this client.
|
|
12
|
+
$pipe = [System.IO.Pipes.NamedPipeClientStream]::new('.', $PipePath.Substring(9), $rights,
|
|
13
|
+
[System.IO.Pipes.PipeOptions]::None, [System.Security.Principal.TokenImpersonationLevel]::Anonymous,
|
|
14
|
+
[System.IO.HandleInheritability]::None)
|
|
15
|
+
try {
|
|
16
|
+
$pipe.Connect(5000)
|
|
17
|
+
if ($PSVersionTable.PSEdition -eq 'Desktop') { $acl = $pipe.GetAccessControl() }
|
|
18
|
+
else { $acl = [System.IO.Pipes.PipesAclExtensions]::GetAccessControl($pipe) }
|
|
19
|
+
if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $identity.Value) {
|
|
20
|
+
throw 'The brain pipe must be owned by the current Windows user.'
|
|
21
|
+
}
|
|
22
|
+
if ($Secure) {
|
|
23
|
+
$acl = [System.IO.Pipes.PipeSecurity]::new()
|
|
24
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
25
|
+
foreach ($sid in $trusted) {
|
|
26
|
+
$acl.AddAccessRule([System.IO.Pipes.PipeAccessRule]::new(
|
|
27
|
+
[System.Security.Principal.SecurityIdentifier]::new($sid),
|
|
28
|
+
[System.IO.Pipes.PipeAccessRights]::FullControl,
|
|
29
|
+
[System.Security.AccessControl.AccessControlType]::Allow))
|
|
30
|
+
}
|
|
31
|
+
if ($PSVersionTable.PSEdition -eq 'Desktop') { $pipe.SetAccessControl($acl) }
|
|
32
|
+
else { [System.IO.Pipes.PipesAclExtensions]::SetAccessControl($pipe, $acl) }
|
|
33
|
+
} else {
|
|
34
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
|
|
35
|
+
if ($rule.AccessControlType -eq 'Allow' -and $trusted -notcontains $rule.IdentityReference.Value) {
|
|
36
|
+
throw 'The brain pipe grants access to another Windows account.'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} finally { $pipe.Dispose() }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
param([string]$ConfigRoot = (Join-Path $env:USERPROFILE '.agent-run'))
|
|
2
|
+
$ErrorActionPreference = 'Stop'
|
|
3
|
+
$ConfigRoot = [System.IO.Path]::GetFullPath($ConfigRoot)
|
|
4
|
+
$runner = Join-Path $PSScriptRoot 'run-windows-brain.ps1'
|
|
5
|
+
if ($ConfigRoot.Contains('"') -or $runner.Contains('"')) { throw 'Paths cannot contain quotes.' }
|
|
6
|
+
$launcher = Join-Path $PSScriptRoot 'run-hidden.vbs'
|
|
7
|
+
$wscript = Join-Path $env:SystemRoot 'System32\wscript.exe'
|
|
8
|
+
$action = New-ScheduledTaskAction -Execute $wscript -Argument "//B //NoLogo `"$launcher`" `"$runner`" -ConfigRoot `"$ConfigRoot`"" -WorkingDirectory $env:USERPROFILE
|
|
9
|
+
$principal = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Limited
|
|
10
|
+
$trigger = New-ScheduledTaskTrigger -AtLogOn -User ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)
|
|
11
|
+
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew
|
|
12
|
+
Register-ScheduledTask -TaskName 'Agent Brain' -Action $action -Principal $principal -Trigger $trigger -Settings $settings -Description 'Private local MCP brain server for agent-run.' -Force | Out-Null
|
|
13
|
+
Start-ScheduledTask -TaskName 'Agent Brain'
|
|
14
|
+
Write-Host "Agent Brain registered for sign-in and started. Log: $ConfigRoot\runtime\service.log"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Option Explicit
|
|
2
|
+
Dim shell, command, argument, i
|
|
3
|
+
Set shell = CreateObject("WScript.Shell")
|
|
4
|
+
If WScript.Arguments.Count < 1 Then WScript.Quit 2
|
|
5
|
+
command = Quote(shell.ExpandEnvironmentStrings("%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe")) & " -NoProfile -NonInteractive -ExecutionPolicy Bypass -File"
|
|
6
|
+
For i = 0 To WScript.Arguments.Count - 1
|
|
7
|
+
argument = WScript.Arguments(i)
|
|
8
|
+
If InStr(argument, Chr(34)) > 0 Then WScript.Quit 2
|
|
9
|
+
command = command & " " & Quote(argument)
|
|
10
|
+
Next
|
|
11
|
+
WScript.Quit shell.Run(command, 0, True)
|
|
12
|
+
|
|
13
|
+
Function Quote(value)
|
|
14
|
+
Quote = Chr(34) & value & Chr(34)
|
|
15
|
+
End Function
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
param([Parameter(Mandatory = $true)][string]$ConfigRoot)
|
|
2
|
+
$ErrorActionPreference = 'Stop'
|
|
3
|
+
$env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User')
|
|
4
|
+
$brain = Join-Path $PSScriptRoot '..\dist\agent-brain.js'
|
|
5
|
+
$runtime = Join-Path $ConfigRoot 'runtime'
|
|
6
|
+
New-Item -ItemType Directory -Path $runtime -Force | Out-Null
|
|
7
|
+
$log = Join-Path $runtime 'service.log'
|
|
8
|
+
$node = (Get-Command node.exe -ErrorAction Stop).Source
|
|
9
|
+
# WScript prevents console flashes. Watch that launcher so stopping the task
|
|
10
|
+
# also stops the server, even when Windows detaches the launcher's descendants.
|
|
11
|
+
$launcherId = (Get-CimInstance Win32_Process -Filter "ProcessId=$PID").ParentProcessId
|
|
12
|
+
$launcher = Get-Process -Id $launcherId -ErrorAction Stop
|
|
13
|
+
$process = Start-Process -FilePath $node -ArgumentList @("`"$brain`"", 'serve', '--configdir', "`"$ConfigRoot`"") -WindowStyle Hidden -PassThru -RedirectStandardError $log -RedirectStandardOutput (Join-Path $runtime 'service-output.log')
|
|
14
|
+
try {
|
|
15
|
+
while (-not $process.WaitForExit(1000)) {
|
|
16
|
+
if ($launcher.HasExited) { Stop-Process -Id $process.Id -ErrorAction SilentlyContinue; exit 0 }
|
|
17
|
+
}
|
|
18
|
+
exit $process.ExitCode
|
|
19
|
+
} finally {
|
|
20
|
+
if (-not $process.HasExited) { Stop-Process -Id $process.Id -ErrorAction SilentlyContinue }
|
|
21
|
+
$process.Dispose()
|
|
22
|
+
$launcher.Dispose()
|
|
23
|
+
}
|