@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.
- package/app/api/sessions/[id]/summarize/route.ts +3 -2
- package/lib/agents/launch.ts +3 -2
- package/lib/agents/server-env.ts +24 -0
- package/lib/chat/drivers/claude.ts +3 -2
- package/lib/chat/drivers/codex.ts +3 -2
- package/lib/chat/drivers/copilot.ts +2 -1
- package/lib/chat/drivers/opencode.ts +3 -2
- package/lib/chat/drivers/pi.ts +3 -2
- package/lib/chat/worker/client.ts +18 -13
- package/lib/hosts/attach.ts +8 -1
- package/lib/orchestration.ts +3 -1
- package/lib/pr-generation.ts +2 -1
- package/lib/tmux/exec.ts +3 -1
- package/package.json +1 -1
|
@@ -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: { ...
|
|
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";
|
package/lib/agents/launch.ts
CHANGED
|
@@ -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 =
|
|
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: { ...
|
|
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
|
-
...
|
|
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: { ...
|
|
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: { ...
|
|
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: { ...
|
|
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, { ...
|
|
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
|
-
...
|
|
83
|
+
...sessionBaseEnv(),
|
|
83
84
|
...options.env,
|
|
84
85
|
});
|
|
85
86
|
let sessionId: string | null = null;
|
package/lib/chat/drivers/pi.ts
CHANGED
|
@@ -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: { ...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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(
|
|
237
|
-
"
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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, `'\\''`)}'`;
|
package/lib/hosts/attach.ts
CHANGED
|
@@ -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 =
|
|
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}`)}`;
|
package/lib/orchestration.ts
CHANGED
|
@@ -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 =
|
|
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.
|
package/lib/pr-generation.ts
CHANGED
|
@@ -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: { ...
|
|
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.
|