@saadnvd1/agent-os 0.3.660 → 0.3.662

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.
@@ -8,6 +8,7 @@ import { randomUUID } from "crypto";
8
8
  import { readFile } from "fs/promises";
9
9
  import { homedir } from "os";
10
10
  import { pasteText, tmux } from "@/lib/tmux/exec";
11
+ import { sessionBaseEnv, UNSET_SERVER_ENV } from "@/lib/agents/server-env";
11
12
 
12
13
  // Get Claude session ID from tmux environment
13
14
  async function getClaudeSessionId(tmuxSession: string): Promise<string | null> {
@@ -135,7 +136,7 @@ async function generateSummary(conversation: string): Promise<string> {
135
136
  const routed = await routedRun("helper");
136
137
  return new Promise((resolve, reject) => {
137
138
  const claude = spawn("claude", ["-p", prompt, "--model", routed.model], {
138
- env: { ...process.env, ...routed.env },
139
+ env: { ...sessionBaseEnv(), ...routed.env },
139
140
  stdio: ["pipe", "pipe", "pipe"],
140
141
  });
141
142
 
@@ -309,7 +310,7 @@ export async function POST(
309
310
 
310
311
  // Start new tmux session with Claude directly
311
312
  const isRoot = process.getuid?.() === 0;
312
- const envPrefix = isRoot ? "IS_SANDBOX=1 " : "";
313
+ const envPrefix = UNSET_SERVER_ENV + (isRoot ? "IS_SANDBOX=1 " : "");
313
314
  const claudeCmd = effectiveAutoApprove(session.auto_approve)
314
315
  ? `${envPrefix}claude --dangerously-skip-permissions`
315
316
  : "claude";
@@ -10,6 +10,7 @@ import { runInBackground } from "../async-operations";
10
10
  import { trustPromptKeys } from "../tasks/state";
11
11
  import { BUS_BRIEF } from "./brief";
12
12
  import { selfUrl } from "./self-env";
13
+ import { sessionBaseEnv, UNSET_SERVER_ENV } from "./server-env";
13
14
  import { sessionPath } from "./tool-path";
14
15
  import { autoCompactEnv } from "../chat/refresh";
15
16
  import { subagentEnv } from "../models/subagents";
@@ -110,7 +111,7 @@ export async function launchClaude(opts: {
110
111
  model_provider: opts.provider ?? null,
111
112
  });
112
113
  const mcpFlags = routedMcpFlags(opts.provider);
113
- const agent = `export PATH=${shellQuote(AOS_BIN_DIR)}:"$HOME/.local/bin:$PATH"; ${routing.exportKey} ${provider.command} ${flags} ${mcpFlags}${CLAUDE_STATUS_SETTINGS_FLAG} --append-system-prompt-file ${shellQuote(briefFile)} "$(cat ${shellQuote(promptFile)})"; exec "\${SHELL:-/bin/sh}" -l`;
114
+ const agent = `${UNSET_SERVER_ENV}export PATH=${shellQuote(AOS_BIN_DIR)}:"$HOME/.local/bin:$PATH"; ${routing.exportKey} ${provider.command} ${flags} ${mcpFlags}${CLAUDE_STATUS_SETTINGS_FLAG} --append-system-prompt-file ${shellQuote(briefFile)} "$(cat ${shellQuote(promptFile)})"; exec "\${SHELL:-/bin/sh}" -l`;
114
115
  await execFileAsync(
115
116
  "tmux",
116
117
  [
@@ -123,7 +124,7 @@ export async function launchClaude(opts: {
123
124
  ...envArgs({ ...agentEnv(opts.sessionId), ...routing.env }),
124
125
  agent,
125
126
  ],
126
- { cwd: opts.cwd }
127
+ { cwd: opts.cwd, env: sessionBaseEnv() }
127
128
  );
128
129
  runInBackground(
129
130
  () => acceptTrustPrompt(opts.tmuxName),
@@ -0,0 +1,24 @@
1
+ // Variables the server sets only for itself (`npm start`, `npm run dev`,
2
+ // the dev-instance spawner). A session that inherits them breaks the
3
+ // project it works in: NODE_ENV=production makes Vitest load React's
4
+ // production build, PORT makes a dev server bind the live one's port.
5
+ // AGENTOS_URL, AGENTOS_PORT and the session ids are not here: sessions need them.
6
+ export const SERVER_ONLY_ENV = [
7
+ "NODE_ENV",
8
+ "PORT",
9
+ "AGENTOS_SERVER",
10
+ "AGENTOS_DEV",
11
+ ] as const;
12
+
13
+ // Shell text that clears them inside a pane. `tmux new-session -e` can only
14
+ // add variables, and an already-running tmux server may hold them globally.
15
+ export const UNSET_SERVER_ENV = `unset ${SERVER_ONLY_ENV.join(" ")}; `;
16
+
17
+ // The server's environment as an agent or its tools should see it.
18
+ export function sessionBaseEnv(
19
+ env: NodeJS.ProcessEnv = process.env
20
+ ): NodeJS.ProcessEnv {
21
+ const out = { ...env };
22
+ for (const k of SERVER_ONLY_ENV) delete out[k];
23
+ return out;
24
+ }
@@ -1,3 +1,4 @@
1
+ import { sessionBaseEnv } from "../../agents/server-env";
1
2
  import { randomUUID } from "crypto";
2
3
  import {
3
4
  getSessionMessages,
@@ -145,7 +146,7 @@ export const claudeDriver: ChatDriver = {
145
146
  cwd,
146
147
  permissionMode: "bypassPermissions",
147
148
  allowDangerouslySkipPermissions: true,
148
- env: { ...process.env, ...env },
149
+ env: { ...sessionBaseEnv(), ...env },
149
150
  },
150
151
  });
151
152
  try {
@@ -235,7 +236,7 @@ export const claudeDriver: ChatDriver = {
235
236
  append: options.systemAppend,
236
237
  },
237
238
  env: {
238
- ...process.env,
239
+ ...sessionBaseEnv(),
239
240
  ...options.env,
240
241
  // When the agent is done with every turn it had queued: the end
241
242
  // of a turn that never sent its result.
@@ -1,3 +1,4 @@
1
+ import { sessionBaseEnv } from "../../agents/server-env";
1
2
  import type { ChatConversation, ChatDriver } from "../driver";
2
3
  import type { ChatImage, DriverEvent } from "../events";
3
4
  import { InputQueue } from "../queue";
@@ -16,7 +17,7 @@ export const codexDriver: ChatDriver = {
16
17
 
17
18
  async discover({ cwd, env }) {
18
19
  const rpc = new CodexRpc(
19
- { cwd, env: { ...process.env, ...env } },
20
+ { cwd, env: { ...sessionBaseEnv(), ...env } },
20
21
  { notify: () => {}, request: () => {}, exit: () => {} }
21
22
  );
22
23
  try {
@@ -51,7 +52,7 @@ export const codexDriver: ChatDriver = {
51
52
  let starting = false;
52
53
  let stopOnStart = false;
53
54
  const rpc: CodexRpc = new CodexRpc(
54
- { cwd: options.cwd, env: { ...process.env, ...options.env } },
55
+ { cwd: options.cwd, env: { ...sessionBaseEnv(), ...options.env } },
55
56
  {
56
57
  notify(method, params) {
57
58
  if (method === "item/started") approvals.sawItem(params.item as P);
@@ -1,3 +1,4 @@
1
+ import { sessionBaseEnv } from "../../agents/server-env";
1
2
  import type { ChatConversation, ChatDriver, ChatStartOptions } from "../driver";
2
3
  import type { ChatImage, DriverEvent } from "../events";
3
4
  import { InputQueue } from "../queue";
@@ -52,7 +53,7 @@ export const copilotDriver: ChatDriver = {
52
53
  const held: { text: string; images?: ChatImage[] }[] = [];
53
54
  const rpc: CopilotRpc = new CopilotRpc(
54
55
  copilotArgs(options),
55
- { cwd: options.cwd, env: { ...process.env, ...options.env } },
56
+ { cwd: options.cwd, env: { ...sessionBaseEnv(), ...options.env } },
56
57
  {
57
58
  notify(method, params) {
58
59
  if (method === "session/update")
@@ -1,3 +1,4 @@
1
+ import { sessionBaseEnv } from "../../agents/server-env";
1
2
  import type { ChatConversation, ChatDriver } from "../driver";
2
3
  import type { ChatImage, DriverEvent } from "../events";
3
4
  import { InputQueue } from "../queue";
@@ -46,7 +47,7 @@ export const openCodeDriver: ChatDriver = {
46
47
  atRest: false,
47
48
 
48
49
  async discover({ cwd, env }) {
49
- const server = new OpenCodeServer(cwd, { ...process.env, ...env });
50
+ const server = new OpenCodeServer(cwd, { ...sessionBaseEnv(), ...env });
50
51
  try {
51
52
  const [providers, commands] = await Promise.all([
52
53
  server.call<{ all: P[]; connected: string[] }>("GET", "/provider"),
@@ -79,7 +80,7 @@ export const openCodeDriver: ChatDriver = {
79
80
  const out = new InputQueue<DriverEvent>();
80
81
  const emit = (e: DriverEvent) => out.push(e);
81
82
  const server = new OpenCodeServer(options.cwd, {
82
- ...process.env,
83
+ ...sessionBaseEnv(),
83
84
  ...options.env,
84
85
  });
85
86
  let sessionId: string | null = null;
@@ -1,3 +1,4 @@
1
+ import { sessionBaseEnv } from "../../agents/server-env";
1
2
  import type { ChatConversation, ChatDriver, ChatStartOptions } from "../driver";
2
3
  import type { ChatImage, DriverEvent } from "../events";
3
4
  import { InputQueue } from "../queue";
@@ -45,7 +46,7 @@ export const piDriver: ChatDriver = {
45
46
  async discover({ cwd, env }) {
46
47
  const rpc = new PiRpc(
47
48
  ["--no-session", "--offline"],
48
- { cwd, env: { ...process.env, ...env } },
49
+ { cwd, env: { ...sessionBaseEnv(), ...env } },
49
50
  { event: () => {}, ui: () => {}, exit: () => {} }
50
51
  );
51
52
  try {
@@ -82,7 +83,7 @@ export const piDriver: ChatDriver = {
82
83
  {
83
84
  cwd: options.cwd,
84
85
  env: {
85
- ...process.env,
86
+ ...sessionBaseEnv(),
86
87
  ...options.env,
87
88
  AGENTOS_PI_ACCESS_FILE: access.file,
88
89
  PI_OFFLINE: "1",
@@ -1,3 +1,4 @@
1
+ import { UNSET_SERVER_ENV, sessionBaseEnv } from "../../agents/server-env";
1
2
  /**
2
3
  * The server's side of a chat worker: starts one in tmux when none is
3
4
  * running, connects to its socket, and passes commands and events along.
@@ -224,7 +225,7 @@ async function spawnWorker(sessionId: string): Promise<void> {
224
225
  if (await tmuxHas(name)) return;
225
226
  fs.mkdirSync(workerDir(), { recursive: true, mode: 0o700 });
226
227
  const env: Record<string, string | undefined> = {
227
- ...process.env,
228
+ ...sessionBaseEnv(),
228
229
  DB_PATH: path.resolve(
229
230
  process.env.DB_PATH || path.join(process.cwd(), "agent-os.db")
230
231
  ),
@@ -233,18 +234,22 @@ async function spawnWorker(sessionId: string): Promise<void> {
233
234
  delete env.TMUX_PANE;
234
235
  fs.writeFileSync(envPath(sessionId), JSON.stringify(env), { mode: 0o600 });
235
236
  const entry = path.join(process.cwd(), "lib/chat/worker/main.ts");
236
- await execFileAsync("tmux", [
237
- "new-session",
238
- "-d",
239
- "-s",
240
- name,
241
- "-c",
242
- process.cwd(),
243
- // The worker finds its env file by DB_PATH, so it can't come from there.
244
- "-e",
245
- `DB_PATH=${env.DB_PATH}`,
246
- `exec ${shellQuote(process.execPath)} --import tsx ${shellQuote(entry)} ${shellQuote(sessionId)} > ${shellQuote(path.join(workerDir(), `${sessionId}.log`))} 2>&1`,
247
- ]);
237
+ await execFileAsync(
238
+ "tmux",
239
+ [
240
+ "new-session",
241
+ "-d",
242
+ "-s",
243
+ name,
244
+ "-c",
245
+ process.cwd(),
246
+ // The worker finds its env file by DB_PATH, so it can't come from there.
247
+ "-e",
248
+ `DB_PATH=${env.DB_PATH}`,
249
+ `${UNSET_SERVER_ENV}exec ${shellQuote(process.execPath)} --import tsx ${shellQuote(entry)} ${shellQuote(sessionId)} > ${shellQuote(path.join(workerDir(), `${sessionId}.log`))} 2>&1`,
250
+ ],
251
+ { env: sessionBaseEnv() }
252
+ );
248
253
  }
249
254
 
250
255
  const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
@@ -1,5 +1,12 @@
1
1
  import { SSH_OPTIONS, loginShellCommand, shellQuote } from "./ssh";
2
2
  import { isValidTmuxName } from "./tmux-name";
3
+ import { SERVER_ONLY_ENV } from "../agents/server-env";
4
+
5
+ // For a shared tmux server that already holds them: attach is the one path
6
+ // that cannot prefix the pane command.
7
+ const UNSET_GLOBAL_ENV = SERVER_ONLY_ENV.map(
8
+ (k) => `set-environment -gu ${k} \\;`
9
+ ).join(" ");
3
10
 
4
11
  export interface AttachSpec {
5
12
  sessionName: string;
@@ -28,7 +35,7 @@ export function buildTmuxAttachCommand(spec: AttachSpec): string {
28
35
  }
29
36
  // Commands chained after an attaching command only run once the client
30
37
  // detaches, so options go first; start-server keeps a fresh server alive.
31
- const prefix = "tmux start-server \\; set -g mouse on \\;";
38
+ const prefix = `tmux start-server \\; set -g mouse on \\; ${UNSET_GLOBAL_ENV}`;
32
39
  if (spec.attachOnly) {
33
40
  // Quoted: zsh expands a bare "=word" to a command path.
34
41
  return `${prefix} attach-session -t ${shellQuote(`=${spec.sessionName}`)}`;
@@ -15,6 +15,7 @@ import { resolveModelForAgent } from "./model-catalog";
15
15
  import { type AgentType, getProvider } from "./providers";
16
16
  import { statusDetector } from "./status-detector";
17
17
  import { wrapWithBanner } from "./banner";
18
+ import { UNSET_SERVER_ENV } from "./agents/server-env";
18
19
  import { runInBackground } from "./async-operations";
19
20
  import { tmux } from "./tmux/exec";
20
21
  import { endTmuxSession } from "./tmux/end";
@@ -168,7 +169,8 @@ export async function spawnWorker(
168
169
 
169
170
  // Create tmux session with the agent and banner
170
171
  const agentCmd = `${provider.command} ${flagsStr}`;
171
- const newSessionCmd = wrapWithBanner(agentCmd, provider.preamble);
172
+ const newSessionCmd =
173
+ UNSET_SERVER_ENV + wrapWithBanner(agentCmd, provider.preamble);
172
174
  try {
173
175
  await tmux(["set", "-g", "mouse", "on"]).catch(() => {});
174
176
  // The last argument is the agent's command line, which tmux runs.
@@ -1,3 +1,4 @@
1
+ import { sessionBaseEnv } from "./agents/server-env";
1
2
  import { routedRun } from "./models/launch";
2
3
  import { execFile } from "child_process";
3
4
  import { git as runGit } from "./git-exec";
@@ -163,7 +164,7 @@ async function claudePrint(cwd: string, prompt: string): Promise<string> {
163
164
  ["--print", "--model", routed.model],
164
165
  {
165
166
  cwd,
166
- env: { ...process.env, ...routed.env },
167
+ env: { ...sessionBaseEnv(), ...routed.env },
167
168
  encoding: "utf-8",
168
169
  timeout: 30_000,
169
170
  killSignal: "SIGKILL",
package/lib/tmux/exec.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
+ import { sessionBaseEnv } from "../agents/server-env";
3
4
 
4
5
  const execFileAsync = promisify(execFile);
5
6
 
@@ -9,6 +10,7 @@ export async function tmux(args: string[], timeout = 10_000): Promise<string> {
9
10
  const { stdout } = await execFileAsync("tmux", args, {
10
11
  timeout,
11
12
  maxBuffer: 8 << 20,
13
+ env: sessionBaseEnv(),
12
14
  });
13
15
  return stdout;
14
16
  }
@@ -24,7 +26,7 @@ export async function pasteText(
24
26
  const child = execFile(
25
27
  "tmux",
26
28
  ["load-buffer", "-b", buffer, "-"],
27
- { timeout: 10_000 },
29
+ { timeout: 10_000, env: sessionBaseEnv() },
28
30
  (err) => (err ? reject(err) : resolve())
29
31
  );
30
32
  // tmux gone before it read everything is a failed paste, not a crash.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.660",
3
+ "version": "0.3.662",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"