@technomoron/agent-run 0.99.39 → 0.99.41

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 CHANGED
@@ -1,6 +1,28 @@
1
1
  CHANGES
2
2
  =======
3
3
 
4
+ Version 0.99.41 (2026-09-25)
5
+ ----------------------------
6
+ * Use a shared Codex home with native named profiles, keeping project
7
+ instructions, MCP configuration and same-name skills separate. Codex 0.157
8
+ profile launches avoid the automatic per-project daemon installation and
9
+ Unix socket path-length failure while using the global executable.
10
+ * Preserve old session rollouts through a non-overwriting migration; retain
11
+ original project homes and let Codex rebuild its derived session index.
12
+ * Use the native unelevated Windows sandbox by default for generated profiles
13
+ and start Codex from the source project directory.
14
+
15
+ Version 0.99.40 (2026-09-24)
16
+ ----------------------------
17
+ * Add native Windows agent-brain service and MCP bridging over per-user named
18
+ pipes, with owner/ACL checks and a hidden sign-in scheduled task installer.
19
+ * Fix recursive Windows command guard resolution after argument scanning and
20
+ normalize CRLF when checking generated profiles.
21
+ * Preserve the complete generated compression prompt through Windows npm shims
22
+ and exercise the integration fixtures with native Windows paths.
23
+ * Load Gemini profile settings from its private user home on Windows, retaining
24
+ shared preferences without requiring administrator-owned system settings.
25
+
4
26
  Version 0.99.39 (2026-09-11)
5
27
  ----------------------------
6
28
  * Fix the AI tools updater failing under sudo or a root shell with inherited
package/README.md CHANGED
@@ -61,6 +61,11 @@ Example:
61
61
  ```text
62
62
  ~/.agent-run/
63
63
  agent-run.defaults.jsonc
64
+ runtime/codex/ # shared, ignored Codex home
65
+ agent-run-<id>.config.toml
66
+ skills/agent-run-<id>/
67
+ sessions/
68
+ memories/
64
69
  org/
65
70
  my-api/
66
71
  agent-run.jsonc
@@ -77,8 +82,6 @@ Example:
77
82
  codex-home/
78
83
  AGENTS.md
79
84
  config.toml
80
- memories/ # native Codex memory, local generated state
81
- skills/
82
85
  gemini/
83
86
  AGENTS.md
84
87
  settings.json
@@ -107,8 +110,10 @@ the agent config tree.
107
110
  - `overrides/`: optional per-profile template overrides
108
111
  - `notes/memory/`: durable project memory shared through the config repository
109
112
  - `live/memories/codex-home/AGENTS.md`: generated Codex instructions
110
- - `live/memories/codex-home/config.toml`: generated Codex config
111
- - `live/memories/codex-home/skills/`: generated Codex skills
113
+ - `live/memories/codex-home/config.toml`: rendered Codex template for inspection
114
+ - `<config-root>/runtime/codex/`: shared Codex home, authentication and session state
115
+ - `runtime/codex/agent-run-<id>.config.toml`: named Codex profile with project instructions and settings
116
+ - `runtime/codex/skills/agent-run-<id>/`: generated skills, enabled only for that profile
112
117
  - `live/CLAUDE.md`: generated Claude instructions
113
118
  - `live/.claude/`: generated Claude settings, MCP configuration, and local
114
119
  plugin skills
@@ -288,15 +293,30 @@ and `agent-run grok --help` pass `--help` through to the underlying tool.
288
293
  For any native agent command, use `--generate` to generate profile files
289
294
  without launching the underlying tool.
290
295
 
291
- Codex defaults to `-a on-request -s workspace-write`. `agent-run` sets
292
- `CODEX_HOME` under the private agent directory, where Codex discovers the
293
- generated `AGENTS.md`, `config.toml`, and skills natively. It starts Codex from
294
- that private directory, passes the project root with `-C`, and keeps generated
295
- guard shims on `PATH`. Use `--network` to enable network access in the
296
- workspace-write sandbox.
297
- When `~/.codex/auth.json` exists, profile-specific Codex homes link their
298
- `auth.json` to that shared login cache so changing profiles does not require a
299
- new ChatGPT login.
296
+ Codex defaults to `-a on-request -s workspace-write`. `agent-run` uses one
297
+ `CODEX_HOME` at `<config-root>/runtime/codex` and selects a generated named
298
+ profile with `--profile`. Project instructions are added through
299
+ `developer_instructions`; MCP settings and per-skill enablement stay in the
300
+ named profile. The normal global Codex executable is used. Codex 0.157.0 runs
301
+ named-profile sessions with its embedded backend, avoiding automatic daemon
302
+ package copies and the long per-project Unix socket path. Agent-run does not
303
+ add `--no-daemon`. This layout requires Codex with named profile-file support
304
+ (0.134.0 or newer); Windows and Debian are tested with 0.157.0.
305
+
306
+ Codex starts from the project root, also passed with `-C`, with generated guard
307
+ shims on `PATH`. Use `--network` to enable network access in the workspace-write
308
+ sandbox. Windows profiles default to the native unelevated sandbox unless the
309
+ profile template specifies another mode. When `~/.codex/auth.json` exists, the
310
+ shared home links that login cache (copies it when Windows cannot create file
311
+ symlinks), so changing projects does not require another login.
312
+
313
+ On first use of each old project home, agent-run copies its `sessions/` and
314
+ `archived_sessions/` rollouts into the shared home without overwriting existing
315
+ files. Codex rebuilds its derived session index. Original files, databases,
316
+ history, native memories and old daemon packages remain untouched in the old
317
+ home; databases and daemon installations are not copied. Close old agent-run
318
+ sessions before upgrading so their final turns are included. New sessions and
319
+ native memories use the shared home; durable project knowledge remains separate.
300
320
 
301
321
  ### Project Memory
302
322
 
@@ -306,7 +326,7 @@ current task. Agents may update project memory when the user explicitly requests
306
326
  authorizes a standing workflow, such as saving durable corrections and updating
307
327
  affected specs. This authorization does not permit Git or external-service writes.
308
328
 
309
- Codex's native `$CODEX_HOME/memories/` remains under ignored `live/` state. It
329
+ Codex's native `$CODEX_HOME/memories/` lives in the ignored shared home. It
310
330
  is generated, machine-local recall data and is not copied into the tracked
311
331
  project memory directory. Project memory is plain Markdown so it can be
312
332
  reviewed and shared through the Git repository that normally holds the
@@ -334,9 +354,13 @@ Gemini runs from the real project directory with a private
334
354
  `GEMINI_CLI_HOME`. Its generated system settings configure `AGENTS.md` as the
335
355
  context filename and add the private runtime plus project memory as context
336
356
  directories. Generated skills use Gemini's `.agents/skills` alias. Existing
337
- Gemini user settings, login credentials, commands, and user skills are linked
338
- into the private home when present; generated system settings remain the
339
- highest-priority project layer.
357
+ Gemini login credentials, commands, and user skills are linked into the private
358
+ home when present. On Windows, shared user preferences and generated settings
359
+ are merged into the private home's `.gemini/settings.json`; shared settings
360
+ remain untouched. This avoids Gemini's administrator-only system-settings ACL
361
+ requirement. On other platforms, user settings are linked and generated system
362
+ settings remain the highest-priority project layer. Gemini's normal folder-trust
363
+ and sign-in requirements still apply.
340
364
 
341
365
  Grok runs from the real project directory with `GROK_HOME` set to
342
366
  `live/grok/`. Grok natively reads the generated home-level `AGENTS.md`,
@@ -417,7 +441,8 @@ then rewrites generated files:
417
441
  - `live/CLAUDE.md`
418
442
  - `live/memories/codex-home/AGENTS.md`
419
443
  - `live/memories/codex-home/config.toml`
420
- - `live/memories/codex-home/skills/**`
444
+ - `<config-root>/runtime/codex/agent-run-<id>.config.toml`
445
+ - `<config-root>/runtime/codex/skills/agent-run-<id>/**`
421
446
  - `live/.claude/**`
422
447
  - `live/gemini/**`
423
448
  - `live/grok/**`
@@ -510,7 +535,8 @@ command stages moves but never commits them.
510
535
 
511
536
  `agent-brain` gives the native agents the same project knowledge, skills, and
512
537
  tasks through MCP. It uses the official Node MCP SDK. The persistent
513
- service runs under `@technomoron/apicore-server` on a private Unix socket.
538
+ service runs under `@technomoron/apicore-server` on a private Unix socket or a
539
+ Windows named pipe restricted to the current user, SYSTEM, and administrators.
514
540
  Node 24 or newer is required; SQLite and FTS come from Node, with no database
515
541
  package to compile.
516
542
 
@@ -978,8 +1004,8 @@ not performed; the service can optionally pull at startup as described below.
978
1004
 
979
1005
  ### Persistent service under apicore-server
980
1006
 
981
- All native agents connect to the same long-lived service for the Unix user.
982
- Start it on Linux or macOS:
1007
+ All native agents connect to the same long-lived service for the current user.
1008
+ Start it on Linux, macOS, or Windows:
983
1009
 
984
1010
  ```sh
985
1011
  agent-brain serve
@@ -1005,7 +1031,27 @@ for a particular service. The bridge verifies the socket's ownership and private
1005
1031
  permissions. A missing or unreachable socket is an error; start the service
1006
1032
  before connecting. There is no standalone stdio server or TCP fallback. Apicore
1007
1033
  supports Unix sockets directly, so localhost binding is unnecessary. The brain
1008
- service currently requires Linux or macOS.
1034
+ service supports Linux, macOS, and native Windows.
1035
+
1036
+ On Windows the default endpoint is `\\.\pipe\agent-brain-<hash>`, derived from
1037
+ the user's home and configuration root. No TCP port is opened. Windows
1038
+ PowerShell applies a private pipe ACL before requests are accepted; each bridge
1039
+ checks the pipe owner and ACL before connecting. `--socket` can select another
1040
+ local named pipe. Windows removes the pipe automatically when its server exits.
1041
+
1042
+ To start it automatically at Windows sign-in, initialize the desired config root
1043
+ and run the packaged installer in PowerShell:
1044
+
1045
+ ```powershell
1046
+ agent-brain init --configdir "$env:USERPROFILE\.agent-run"
1047
+ $prefix = npm.cmd prefix -g
1048
+ & "$prefix\node_modules\@technomoron\agent-run\scripts\install-windows-brain.ps1" -ConfigRoot "$env:USERPROFILE\.agent-run"
1049
+ ```
1050
+
1051
+ The `Agent Brain` scheduled task runs hidden as the signed-in user, without
1052
+ elevation, and retries failures. Its log is `<config-root>/runtime/service.log`.
1053
+ After upgrading the package, restart it with `Stop-ScheduledTask` followed by
1054
+ `Start-ScheduledTask` for task name `Agent Brain`.
1009
1055
 
1010
1056
  After a crash, remove a stale socket only after checking that its service is
1011
1057
  stopped. Starting a second service on an existing socket fails without replacing
@@ -9,14 +9,15 @@ const render_settings_1 = require("../config/render-settings");
9
9
  const render_skills_1 = require("../config/render-skills");
10
10
  const environment_1 = require("../runtime/environment");
11
11
  const shared_state_1 = require("../runtime/shared-state");
12
+ const codex_profile_1 = require("../runtime/codex-profile");
12
13
  const shared_1 = require("./shared");
13
14
  function layout(context) {
14
15
  return {
15
16
  dir: context.paths.codexHomeDir,
16
17
  instructionFile: path.join(context.paths.codexHomeDir, 'AGENTS.md'),
17
- configFiles: [path.join(context.paths.codexHomeDir, 'config.toml')],
18
- skillsDir: context.paths.codexSkillsDir,
19
- requiredDirs: [context.paths.codexHomeDir, context.paths.codexSkillsDir],
18
+ configFiles: [path.join(context.paths.codexHomeDir, 'config.toml'), path.join((0, codex_profile_1.codexSharedHome)(context), `${(0, codex_profile_1.codexProfileName)(context)}.config.toml`)],
19
+ skillsDir: (0, codex_profile_1.codexProfileSkills)(context),
20
+ requiredDirs: [context.paths.codexHomeDir, (0, codex_profile_1.codexProfileSkills)(context)],
20
21
  preservedSkillNames: new Set(['.system'])
21
22
  };
22
23
  }
@@ -24,9 +25,11 @@ function generate(generation) {
24
25
  const agentLayout = layout(generation.context);
25
26
  const baseConfig = (0, render_settings_1.renderProfileConfig)(generation.env, generation.configRoot, generation.context, 'codex-config.toml.njk', 'global/tool-templates/codex-config.toml.njk', () => (0, defaults_1.defaultCodexConfigContent)(generation.context), 'Codex config.toml', generation.trace);
26
27
  const mcp = (0, render_mcp_1.renderCodexMcp)(generation.context);
28
+ const config = `${baseConfig.trimEnd()}${mcp ? `\n\n${mcp}` : '\n'}`;
27
29
  const files = [
28
30
  { path: agentLayout.instructionFile, content: generation.agentsMd },
29
- { path: agentLayout.configFiles[0] ?? '', content: `${baseConfig.trimEnd()}${mcp ? `\n\n${mcp}` : '\n'}` },
31
+ { path: agentLayout.configFiles[0] ?? '', content: config },
32
+ { path: agentLayout.configFiles[1] ?? '', content: (0, codex_profile_1.renderCodexProfile)(generation.context, config, generation.agentsMd), atomic: true },
30
33
  ...(0, render_skills_1.renderSkillFiles)(generation.context, agentLayout.skillsDir)
31
34
  ];
32
35
  return { ...agentLayout, id: 'codex', projectDir: generation.context.projectRoot, files, context: generation.context };
@@ -46,7 +49,16 @@ exports.codexAdapter = {
46
49
  layout,
47
50
  generate,
48
51
  prepare(runtime) {
49
- (0, shared_state_1.linkSharedEntry)(path.join(os.homedir(), '.codex', 'auth.json'), path.join(runtime.dir, 'auth.json'), 'profile Codex auth');
52
+ (0, shared_state_1.linkSharedEntry)(path.join(os.homedir(), '.codex', 'auth.json'), path.join((0, codex_profile_1.codexSharedHome)(runtime.context), 'auth.json'), 'shared Codex auth');
53
+ (0, codex_profile_1.migrateCodexSessions)(runtime.dir, (0, codex_profile_1.codexSharedHome)(runtime.context));
54
+ const profile = runtime.files.find((file) => file.path === runtime.configFiles[1]);
55
+ const base = runtime.files.find((file) => file.path === runtime.configFiles[0]);
56
+ const instructions = runtime.files.find((file) => file.path === runtime.instructionFile);
57
+ if (profile && base && instructions) {
58
+ profile.content = (0, codex_profile_1.renderCodexProfile)(runtime.context, base.content, instructions.content);
59
+ }
60
+ // Disable the pending skills in other profiles before making them discoverable.
61
+ (0, codex_profile_1.refreshCodexProfileSkills)(runtime.context);
50
62
  },
51
63
  spawn(runtime, options) {
52
64
  (0, shared_1.assertRuntimeFile)('codex', runtime.instructionFile, runtime.context.profileDir);
@@ -56,6 +68,7 @@ exports.codexAdapter = {
56
68
  return {
57
69
  command: options.binary,
58
70
  args: [
71
+ '--profile', (0, codex_profile_1.codexProfileName)(runtime.context),
59
72
  ...runtimeArgs,
60
73
  '--add-dir',
61
74
  runtime.context.paths.projectMemoryDir,
@@ -66,8 +79,8 @@ exports.codexAdapter = {
66
79
  : []),
67
80
  ...options.passthroughArgs
68
81
  ],
69
- cwd: runtime.dir,
70
- env: { ...(0, environment_1.buildAgentEnvironment)(runtime.context), CODEX_HOME: runtime.dir },
82
+ cwd: runtime.projectDir,
83
+ env: { ...(0, environment_1.buildAgentEnvironment)(runtime.context), CODEX_HOME: (0, codex_profile_1.codexSharedHome)(runtime.context) },
71
84
  onExit: (0, shared_1.withPostflight)(runtime, options.wrapperArgs)
72
85
  };
73
86
  },
@@ -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
@@ -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({
@@ -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
- 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.`);
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
- fs.chmodSync(socketPath, 0o600);
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
- const socket = fs.lstatSync(socketPath, { throwIfNoEntry: false });
166
- if (!socket)
167
- 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.`);
168
- requirePrivateSocketDirectory(socketPath);
169
- if (!socket.isSocket() || socket.uid !== process.getuid() || (socket.mode & 0o077) !== 0)
170
- throw new Error('The brain socket must belong to the current user with mode 0600.');
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/constants.js CHANGED
@@ -33,6 +33,8 @@ exports.BRAIN_GITIGNORE_ENTRIES = [
33
33
  ''
34
34
  ];
35
35
  exports.GENERATED_GITIGNORE_ENTRIES = [
36
+ '# Shared machine-local Codex state',
37
+ '/runtime/codex/',
36
38
  '# Generated agent-run live profiles',
37
39
  '**/live/',
38
40
  '',
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%"=="%~f0" (',
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
@@ -18,6 +18,7 @@ const project_1 = require("./project");
18
18
  const templates_1 = require("./templates");
19
19
  const utils_1 = require("./utils");
20
20
  const config_1 = require("./brain/config");
21
+ const codex_profile_1 = require("./runtime/codex-profile");
21
22
  function renderProfile(projectRoot, agentDir, checkOnly = false, targetTool = null, trace, options) {
22
23
  const configRoot = options?.configRoot ?? (0, project_1.defaultConfigRoot)(projectRoot);
23
24
  const profile = options?.profile ?? (0, project_1.resolveProfile)(projectRoot);
@@ -92,11 +93,14 @@ function syncAgentProfile(projectRoot, agentDir, options) {
92
93
  return syncRenderedProfile(rendered);
93
94
  }
94
95
  function syncRenderedProfile(rendered) {
96
+ return (0, codex_profile_1.withCodexProfileLock)(rendered.context, () => writeRenderedProfile(rendered));
97
+ }
98
+ function writeRenderedProfile(rendered) {
95
99
  (0, config_tree_1.ensureConfigRootGitignore)(rendered.configRoot);
96
100
  syncRuntimeDirs(rendered);
97
101
  removeStaleGeneratedEntries(rendered);
98
102
  for (const file of rendered.files) {
99
- writeGeneratedFile(file.path, file.content, file.executable ?? false);
103
+ writeGeneratedFile(file.path, file.content, file.executable ?? false, file.atomic ?? false);
100
104
  }
101
105
  removeLegacyGeneratedCodexFiles(rendered.context);
102
106
  removeLegacyGeneratedClaudeSettings(rendered.context);
@@ -129,11 +133,14 @@ function syncRuntimeDirs(rendered) {
129
133
  }
130
134
  }
131
135
  }
132
- function writeGeneratedFile(filePath, content, executable) {
136
+ function writeGeneratedFile(filePath, content, executable, atomic) {
133
137
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
134
138
  const unchanged = fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content;
135
139
  if (!unchanged) {
136
- fs.writeFileSync(filePath, content, 'utf8');
140
+ if (atomic)
141
+ (0, codex_profile_1.writeCodexProfileFile)(filePath, content);
142
+ else
143
+ fs.writeFileSync(filePath, content, 'utf8');
137
144
  (0, utils_1.verbose)(`write ${filePath}`);
138
145
  }
139
146
  if (executable && !constants_1.IS_WINDOWS) {
@@ -229,7 +236,7 @@ function checkRenderedFiles(rendered, findings) {
229
236
  continue;
230
237
  }
231
238
  const actual = fs.readFileSync(file.path, 'utf8').replace(/\r\n/g, '\n');
232
- if (actual !== file.content) {
239
+ if (actual !== file.content.replace(/\r\n/g, '\n')) {
233
240
  findings.push({ message: `generated file is out of date: ${file.path}`, severity: 'ERROR' });
234
241
  }
235
242
  }
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.codexSharedHome = codexSharedHome;
4
+ exports.codexProfileName = codexProfileName;
5
+ exports.codexProfileSkills = codexProfileSkills;
6
+ exports.renderCodexProfile = renderCodexProfile;
7
+ exports.refreshCodexProfileSkills = refreshCodexProfileSkills;
8
+ exports.writeCodexProfileFile = writeCodexProfileFile;
9
+ exports.withCodexProfileLock = withCodexProfileLock;
10
+ exports.migrateCodexSessions = migrateCodexSessions;
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+ const crypto_1 = require("crypto");
14
+ const smol_toml_1 = require("smol-toml");
15
+ function codexSharedHome(context) {
16
+ return path.join(context.configRoot, 'runtime', 'codex');
17
+ }
18
+ function codexProfileName(context) {
19
+ const identity = path.resolve(context.profileDir);
20
+ const canonical = process.platform === 'win32' ? identity.toLowerCase() : identity;
21
+ return `agent-run-${(0, crypto_1.createHash)('sha256').update(canonical).digest('hex').slice(0, 20)}`;
22
+ }
23
+ function codexProfileSkills(context) {
24
+ return path.join(codexSharedHome(context), 'skills', codexProfileName(context));
25
+ }
26
+ // Only scan agent-run's generated skills. Personal/system skills keep their native settings.
27
+ function generatedSkills(home) {
28
+ const root = path.join(home, 'skills');
29
+ if (!fs.existsSync(root))
30
+ return [];
31
+ const files = [];
32
+ for (const profile of fs.readdirSync(root, { withFileTypes: true })) {
33
+ if (!profile.isDirectory() || !/^agent-run-[a-f0-9]{20}$/.test(profile.name))
34
+ continue;
35
+ const dir = path.join(root, profile.name);
36
+ for (const skill of fs.readdirSync(dir, { withFileTypes: true })) {
37
+ const file = path.join(dir, skill.name, 'SKILL.md');
38
+ if (skill.isDirectory() && fs.existsSync(file))
39
+ files.push(file);
40
+ }
41
+ }
42
+ return files.sort();
43
+ }
44
+ function configureSkills(config, home, profile, files) {
45
+ const settings = (config.skills ?? {});
46
+ const existing = (settings.config ?? []);
47
+ const generatedRoot = path.join(home, 'skills') + path.sep;
48
+ const isGenerated = (file) => {
49
+ const relative = path.resolve(file).slice(generatedRoot.length);
50
+ return path.resolve(file).startsWith(generatedRoot) && /^agent-run-[a-f0-9]{20}[\\/]/.test(relative);
51
+ };
52
+ settings.config = [
53
+ ...existing.filter((entry) => !isGenerated(entry.path)),
54
+ ...files.map((file) => ({
55
+ path: file,
56
+ enabled: path.dirname(path.dirname(file)) === path.join(home, 'skills', profile)
57
+ && (existing.find((entry) => entry.path === file)?.enabled ?? true)
58
+ }))
59
+ ];
60
+ config.skills = settings;
61
+ }
62
+ function renderCodexProfile(context, base, instructions) {
63
+ const config = (0, smol_toml_1.parse)(base);
64
+ config.developer_instructions = [config.developer_instructions, instructions].filter(Boolean).join('\n\n');
65
+ if (process.platform === 'win32') {
66
+ // A fresh home otherwise silently downgrades workspace-write to read-only.
67
+ config.windows = { sandbox: 'unelevated', ...(config.windows ?? {}) };
68
+ }
69
+ configureSkills(config, codexSharedHome(context), codexProfileName(context), pendingSkills(context));
70
+ return (0, smol_toml_1.stringify)(config) + '\n';
71
+ }
72
+ function pendingSkills(context) {
73
+ const ownDir = codexProfileSkills(context);
74
+ const others = generatedSkills(codexSharedHome(context)).filter((file) => path.dirname(path.dirname(file)) !== ownDir);
75
+ const own = context.skills.map((skill) => path.join(ownDir, skill.name, 'SKILL.md'));
76
+ return [...new Set([...others, ...own])].sort();
77
+ }
78
+ // Refresh older generated profiles too: newly generated project skills must not
79
+ // become enabled in another profile merely because they share a discovery root.
80
+ function refreshCodexProfileSkills(context) {
81
+ const home = codexSharedHome(context);
82
+ const files = pendingSkills(context);
83
+ for (const entry of fs.readdirSync(home)) {
84
+ if (!/^agent-run-[a-f0-9]{20}\.config\.toml$/.test(entry))
85
+ continue;
86
+ const file = path.join(home, entry);
87
+ const original = fs.readFileSync(file, 'utf8');
88
+ const config = (0, smol_toml_1.parse)(original);
89
+ configureSkills(config, home, entry.replace('.config.toml', ''), files);
90
+ const content = (0, smol_toml_1.stringify)(config) + '\n';
91
+ if (content !== original)
92
+ writeCodexProfileFile(file, content);
93
+ }
94
+ }
95
+ function writeCodexProfileFile(file, content) {
96
+ const temporary = `${file}.${(0, crypto_1.randomUUID)()}.tmp`;
97
+ try {
98
+ fs.writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600 });
99
+ fs.renameSync(temporary, file);
100
+ }
101
+ finally {
102
+ fs.rmSync(temporary, { force: true });
103
+ }
104
+ }
105
+ // Profile refresh, skill publication and session migration share one writer.
106
+ // Codex readers see complete profile files through atomic rename.
107
+ function withCodexProfileLock(context, run) {
108
+ const home = codexSharedHome(context);
109
+ fs.mkdirSync(home, { recursive: true });
110
+ const lock = path.join(home, '.agent-run-sync.lock');
111
+ const deadline = Date.now() + 30000;
112
+ let descriptor;
113
+ for (;;) {
114
+ try {
115
+ descriptor = fs.openSync(lock, 'wx', 0o600);
116
+ break;
117
+ }
118
+ catch (error) {
119
+ if (error.code !== 'EEXIST')
120
+ throw error;
121
+ try {
122
+ const owner = Number(fs.readFileSync(lock, 'utf8'));
123
+ if (Number.isInteger(owner) && owner > 0) {
124
+ try {
125
+ process.kill(owner, 0);
126
+ }
127
+ catch (probe) {
128
+ if (probe.code === 'ESRCH') {
129
+ fs.rmSync(lock, { force: true });
130
+ continue;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ catch (readError) {
136
+ if (readError.code === 'ENOENT')
137
+ continue;
138
+ throw readError;
139
+ }
140
+ if (Date.now() >= deadline)
141
+ throw new Error(`Timed out waiting for Codex profile generation: ${lock}`);
142
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
143
+ }
144
+ }
145
+ try {
146
+ fs.writeFileSync(descriptor, String(process.pid));
147
+ return run();
148
+ }
149
+ finally {
150
+ fs.closeSync(descriptor);
151
+ fs.rmSync(lock, { force: true });
152
+ }
153
+ }
154
+ function migrateCodexSessions(legacyHome, sharedHome) {
155
+ const marker = path.join(sharedHome, 'agent-run-migrations', `${(0, crypto_1.createHash)('sha256').update(path.resolve(legacyHome)).digest('hex')}.json`);
156
+ if (fs.existsSync(marker))
157
+ return;
158
+ // Rollouts are Codex's durable session source. Let Codex rebuild its derived
159
+ // database; never merge SQLite files or copy daemon installs, sockets or locks.
160
+ function copyMissing(source, target) {
161
+ if (!fs.existsSync(source))
162
+ return;
163
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
164
+ const from = path.join(source, entry.name);
165
+ const to = path.join(target, entry.name);
166
+ if (entry.isDirectory())
167
+ copyMissing(from, to);
168
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
169
+ fs.mkdirSync(target, { recursive: true });
170
+ try {
171
+ fs.copyFileSync(from, to, fs.constants.COPYFILE_EXCL);
172
+ }
173
+ catch (error) {
174
+ if (error.code !== 'EEXIST')
175
+ throw error;
176
+ }
177
+ }
178
+ }
179
+ }
180
+ for (const dir of ['sessions', 'archived_sessions'])
181
+ copyMissing(path.join(legacyHome, dir), path.join(sharedHome, dir));
182
+ fs.mkdirSync(path.dirname(marker), { recursive: true });
183
+ fs.writeFileSync(marker, JSON.stringify({ source: legacyHome, migrated: new Date().toISOString() }) + '\n');
184
+ }
package/dist/templates.js CHANGED
@@ -12,6 +12,7 @@ const nunjucks = require("nunjucks");
12
12
  const constants_1 = require("./constants");
13
13
  const utils_1 = require("./utils");
14
14
  const config_1 = require("./brain/config");
15
+ const codex_profile_1 = require("./runtime/codex-profile");
15
16
  function createNunjucksEnv(configRoot) {
16
17
  return new nunjucks.Environment(new nunjucks.FileSystemLoader(configRoot, { noCache: true }), {
17
18
  autoescape: false,
@@ -82,10 +83,10 @@ function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env)
82
83
  memoriesDir,
83
84
  globalMemoryDir: (0, config_1.readBrainConfig)(configRoot)?.enabled ? path.join(configRoot, 'global', 'memory') : path.join(configRoot, 'notes', 'memory'),
84
85
  projectMemoryDir,
85
- nativeMemoryDir: path.join(codexHomeDir, 'memories'),
86
+ nativeMemoryDir: path.join(configRoot, 'runtime', 'codex', 'memories'),
86
87
  codexHomeDir,
87
88
  overridesDir: path.join(profileDir, 'overrides'),
88
- codexSkillsDir: path.join(codexHomeDir, 'skills'),
89
+ codexSkillsDir: (0, codex_profile_1.codexProfileSkills)(baseContext),
89
90
  claudeSkillsDir: path.join(liveDir, '.claude', 'skills'),
90
91
  geminiRuntimeDir,
91
92
  geminiHomeDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/agent-run",
3
- "version": "0.99.39",
3
+ "version": "0.99.41",
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"
@@ -31,7 +35,8 @@
31
35
  "clean:dist": "rimraf ./dist/",
32
36
  "cleanbuild": "run-s clean:dist build",
33
37
  "prepack": "run-s build",
34
- "test": "run-s build test:integration test:release",
38
+ "test": "run-s build test:codex-profiles test:integration test:release",
39
+ "test:codex-profiles": "node --test scripts/test-codex-profiles.cjs",
35
40
  "test:brain": "node --test scripts/test-brain.cjs",
36
41
  "test:integration": "bash ./scripts/test.sh",
37
42
  "test:release": "bash ./scripts/test-release.sh",
@@ -61,6 +66,7 @@
61
66
  "cross-spawn": "^7.0.6",
62
67
  "jsonc-parser": "^3.3.1",
63
68
  "nunjucks": "^3.2.4",
69
+ "smol-toml": "^1.9.0",
64
70
  "yaml": "^2.9.0",
65
71
  "zod": "^4.5.4"
66
72
  }
@@ -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
+ }