@love-moon/ai-sdk 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 30afccc: Make the claude backend usable when conductor runs as root.
8
+
9
+ Claude Code refuses `bypassPermissions` / `--dangerously-skip-permissions` under
10
+ root unless `IS_SANDBOX=1` or `CLAUDE_CODE_BUBBLEWRAP` is set, so every claude
11
+ turn died immediately on root installs (docker, CI, bare VPS). Both routes a
12
+ claude command takes are now root-aware, sharing one check
13
+ (`isClaudeRootPermissionRestricted`, which mirrors claude's own gate):
14
+
15
+ - The agent-SDK session downgrades the permission mode to `acceptEdits` (auto
16
+ mode) and stops passing `allowDangerouslySkipPermissions`.
17
+ - The daemon's tool-preset PTY path rewrites the configured `allow_cli_list`
18
+ command via `resolveClaudeCommandForRoot`, using the same env the spawned
19
+ child receives, so a per-task `IS_SANDBOX=1` opt-out is honored.
20
+ - `conductor config` no longer writes `--dangerously-skip-permissions` into a
21
+ config generated on a root machine.
22
+
23
+ The mode is also configurable now: `claude --permission-mode acceptEdits` in
24
+ `allow_cli_list` is lifted out of the command string. An unrecognized mode still
25
+ falls back to the default instead of failing, but logs a warning at session boot.
26
+
27
+ ai-sdk gains three exports: `isClaudeRootPermissionRestricted`,
28
+ `resolveClaudeCommandForRoot`, `resolveClaudePermissionPolicy`.
29
+
3
30
  ## 0.10.0
4
31
 
5
32
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
2
2
  export { PROVIDER_MEDIA_CAPABILITIES } from "./media-adapters.js";
3
3
  export { createAiSession, RemoteAiSession } from "./client.js";
4
+ export { isClaudeRootPermissionRestricted, resolveClaudeCommandForRoot, resolveClaudePermissionPolicy } from "./providers/claude-agent-sdk-session.js";
4
5
  export { appendContextFilesToPrompt, normalizeContextFiles } from "./context-files.js";
5
6
  export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normalizeMediaInputs, resolveTurnMedia } from "./media-input.js";
6
7
  export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus } from "./shared.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createAiSession, RemoteAiSession } from "./client.js";
2
2
  export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
3
3
  export { PROVIDER_MEDIA_CAPABILITIES } from "./media-adapters.js";
4
+ export { isClaudeRootPermissionRestricted, resolveClaudeCommandForRoot, resolveClaudePermissionPolicy, } from "./providers/claude-agent-sdk-session.js";
4
5
  export { appendContextFilesToPrompt, normalizeContextFiles } from "./context-files.js";
5
6
  export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normalizeMediaInputs, resolveTurnMedia, } from "./media-input.js";
6
7
  export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus, } from "./shared.js";
@@ -1,3 +1,11 @@
1
+ export function isClaudeRootPermissionRestricted(env?: NodeJS.ProcessEnv): boolean;
2
+ export function resolveClaudePermissionPolicy(options?: {}, env?: NodeJS.ProcessEnv): {
3
+ permissionMode: string;
4
+ allowDangerouslySkipPermissions: boolean;
5
+ downgradedFrom: string;
6
+ invalidMode: any;
7
+ };
8
+ export function resolveClaudeCommandForRoot(commandLine: any, env?: NodeJS.ProcessEnv): string;
1
9
  export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
2
10
  static capabilities: Readonly<{
3
11
  goal: true;
@@ -49,6 +57,12 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
49
57
  turnDeadlineMs: any;
50
58
  sdkModulePromise: Promise<any> | Promise<typeof import("@anthropic-ai/claude-agent-sdk")> | null;
51
59
  env: any;
60
+ permissionPolicy: {
61
+ permissionMode: string;
62
+ allowDangerouslySkipPermissions: boolean;
63
+ downgradedFrom: string;
64
+ invalidMode: any;
65
+ };
52
66
  writeLog(message: any): void;
53
67
  trace(message: any): void;
54
68
  get threadId(): any;
@@ -131,7 +145,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
131
145
  abortController: any;
132
146
  cwd: any;
133
147
  env: any;
134
- permissionMode: "default" | "plan" | "acceptEdits" | "bypassPermissions" | "dontAsk";
148
+ permissionMode: string;
135
149
  settingSources: string[];
136
150
  persistSession: boolean;
137
151
  };
@@ -8,6 +8,10 @@ const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
8
8
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
9
9
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
10
10
  const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
11
+ const PERMISSION_MODES = new Set(["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk"]);
12
+ const DEFAULT_PERMISSION_MODE = "bypassPermissions";
13
+ // Auto ("accept edits") mode: the strongest mode claude allows as root.
14
+ const ROOT_PERMISSION_MODE = "acceptEdits";
11
15
  function waitForever() {
12
16
  return new Promise(() => { });
13
17
  }
@@ -50,16 +54,83 @@ function normalizeSettingSources(value) {
50
54
  }
51
55
  function normalizePermissionMode(value) {
52
56
  const normalized = typeof value === "string" ? value.trim() : "";
53
- switch (normalized) {
54
- case "default":
55
- case "acceptEdits":
56
- case "bypassPermissions":
57
- case "plan":
58
- case "dontAsk":
59
- return normalized;
60
- default:
61
- return "bypassPermissions";
57
+ return PERMISSION_MODES.has(normalized) ? normalized : DEFAULT_PERMISSION_MODE;
58
+ }
59
+ // Mirrors claude's own root gate byte for byte:
60
+ //
61
+ // permissionMode === "bypassPermissions" || dangerouslySkipPermissions
62
+ // ? getuid() === 0 && process.env.IS_SANDBOX !== "1" && !CLAUDE_CODE_BUBBLEWRAP
63
+ // -> "--dangerously-skip-permissions cannot be used with root/sudo
64
+ // privileges for security reasons" + exit(1)
65
+ //
66
+ // Two different env idioms in that one line, and both are load-bearing:
67
+ // IS_SANDBOX is a *strict* `=== "1"` compare, while CLAUDE_CODE_BUBBLEWRAP goes
68
+ // through claude's loose truthy parser (`1|true|yes|on`, trimmed+lowercased).
69
+ // Do not "helpfully" accept IS_SANDBOX=true here — being more permissive than
70
+ // the gate keeps bypassPermissions and just moves the failure into claude,
71
+ // which is the exact bug we are fixing.
72
+ export function isClaudeRootPermissionRestricted(env = process.env) {
73
+ if (typeof process.getuid !== "function" || process.getuid() !== 0) {
74
+ return false;
75
+ }
76
+ if (env?.IS_SANDBOX === "1") {
77
+ return false;
78
+ }
79
+ const bubblewrap = String(env?.CLAUDE_CODE_BUBBLEWRAP ?? "").trim().toLowerCase();
80
+ return !["1", "true", "yes", "on"].includes(bubblewrap);
81
+ }
82
+ // Root installs (docker, CI, bare VPS) would otherwise fail every turn, so fall
83
+ // back to the closest usable mode instead of dying.
84
+ export function resolveClaudePermissionPolicy(options = {}, env = process.env) {
85
+ const requested = normalizePermissionMode(options.permissionMode);
86
+ // A configured value we don't recognize still falls back to the default (a
87
+ // typo must never take the session down), but the caller reports it so the
88
+ // user finds out their config line is doing nothing.
89
+ const rawMode = typeof options.permissionMode === "string" ? options.permissionMode.trim() : "";
90
+ const invalidMode = rawMode && !PERMISSION_MODES.has(rawMode) ? rawMode : "";
91
+ if (!isClaudeRootPermissionRestricted(env)) {
92
+ return {
93
+ permissionMode: requested,
94
+ allowDangerouslySkipPermissions: requested === DEFAULT_PERMISSION_MODE || options.allowDangerouslySkipPermissions === true,
95
+ downgradedFrom: "",
96
+ invalidMode,
97
+ };
98
+ }
99
+ const permissionMode = requested === DEFAULT_PERMISSION_MODE ? ROOT_PERMISSION_MODE : requested;
100
+ return {
101
+ permissionMode,
102
+ allowDangerouslySkipPermissions: false,
103
+ downgradedFrom: permissionMode === requested ? "" : requested,
104
+ invalidMode,
105
+ };
106
+ }
107
+ // The PTY / tool-preset path in the daemon hands a configured allow_cli_list
108
+ // command straight to a shell, so it never reaches the session class above and
109
+ // never sees resolveClaudePermissionPolicy. Rewrite the command string there
110
+ // too, or a root box keeps failing in terminal tasks after the SDK path is fixed.
111
+ export function resolveClaudeCommandForRoot(commandLine, env = process.env) {
112
+ const command = typeof commandLine === "string" ? commandLine : "";
113
+ if (!command.trim() || !isClaudeRootPermissionRestricted(env)) {
114
+ return command;
115
+ }
116
+ // Also drop a valueless `--permission-mode` (end of string, or immediately
117
+ // followed by another flag). Without this the append branch below would emit
118
+ // `--permission-mode --permission-mode acceptEdits`, and claude reads the
119
+ // flag name as the mode and `acceptEdits` as a positional prompt.
120
+ const stripped = command
121
+ .replace(/\s*--dangerously-skip-permissions(?=\s|$)/g, "")
122
+ .replace(/\s*--permission-mode=(?=\s|$)/g, "")
123
+ .replace(/\s*--permission-mode(?=\s+-|\s*$)/g, "")
124
+ .trim();
125
+ const configuredMode = extractLongFlagFromCommandLine(stripped, "permission-mode");
126
+ if (!configuredMode) {
127
+ return `${stripped} --permission-mode ${ROOT_PERMISSION_MODE}`;
128
+ }
129
+ if (configuredMode === DEFAULT_PERMISSION_MODE) {
130
+ return stripped.replace(/(--permission-mode[=\s]+)bypassPermissions(?=\s|$)/, `$1${ROOT_PERMISSION_MODE}`);
62
131
  }
132
+ // Any other configured mode is already root-safe; leave it verbatim.
133
+ return stripped;
63
134
  }
64
135
  function normalizeText(value) {
65
136
  return typeof value === "string" ? value : "";
@@ -163,6 +234,14 @@ export class ClaudeAgentSdkSession extends EventEmitter {
163
234
  options = { ...options, effort: effortFromCommandLine };
164
235
  }
165
236
  }
237
+ // Same for `--permission-mode`, so `claude --permission-mode acceptEdits`
238
+ // in allow_cli_list picks the mode without new plumbing.
239
+ if (options.permissionMode === undefined && typeof options.commandLine === "string") {
240
+ const modeFromCommandLine = extractLongFlagFromCommandLine(options.commandLine, "permission-mode");
241
+ if (modeFromCommandLine) {
242
+ options = { ...options, permissionMode: modeFromCommandLine };
243
+ }
244
+ }
166
245
  this.options = options;
167
246
  this.logger = normalizeLogger(options.logger);
168
247
  this.cwd =
@@ -205,6 +284,18 @@ export class ClaudeAgentSdkSession extends EventEmitter {
205
284
  if (!this.env.CLAUDE_AGENT_SDK_CLIENT_APP) {
206
285
  this.env.CLAUDE_AGENT_SDK_CLIENT_APP = "conductor-ai-sdk/0.0.0";
207
286
  }
287
+ // Resolved once at boot (options and env are both fixed by now) so the
288
+ // config warning lands at session start instead of once per turn.
289
+ this.permissionPolicy = resolveClaudePermissionPolicy(this.options, { ...process.env, ...this.env });
290
+ if (this.permissionPolicy.invalidMode) {
291
+ this.trace(`WARN unknown permission mode ${JSON.stringify(this.permissionPolicy.invalidMode)} in config; `
292
+ + `using ${this.permissionPolicy.permissionMode} instead `
293
+ + `(valid: ${[...PERMISSION_MODES].join(", ")})`);
294
+ }
295
+ if (this.permissionPolicy.downgradedFrom) {
296
+ this.trace(`permission mode ${this.permissionPolicy.downgradedFrom} -> ${this.permissionPolicy.permissionMode} `
297
+ + `(claude rejects ${this.permissionPolicy.downgradedFrom} as root; set IS_SANDBOX=1 to keep it)`);
298
+ }
208
299
  }
209
300
  writeLog(message) {
210
301
  emitLog(this.logger, message);
@@ -537,7 +628,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
537
628
  });
538
629
  }
539
630
  buildSdkOptions(abortController) {
540
- const permissionMode = normalizePermissionMode(this.options.permissionMode);
631
+ const { permissionMode } = this.permissionPolicy;
541
632
  const options = {
542
633
  abortController,
543
634
  cwd: this.cwd,
@@ -549,7 +640,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
549
640
  settingSources: normalizeSettingSources(this.options.settingSources),
550
641
  persistSession: this.options.persistSession !== false,
551
642
  };
552
- if (permissionMode === "bypassPermissions" || this.options.allowDangerouslySkipPermissions === true) {
643
+ if (this.permissionPolicy.allowDangerouslySkipPermissions) {
553
644
  options.allowDangerouslySkipPermissions = true;
554
645
  }
555
646
  if (this.sessionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -53,12 +53,12 @@
53
53
  "zod": "^4.1.5"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@love-moon/chat-web": "^0.10.0"
56
+ "@love-moon/chat-web": "^0.11.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^22.10.2",
60
60
  "tsx": "^4.20.6",
61
61
  "typescript": "^5.6.3"
62
62
  },
63
- "gitCommitId": "bc7b3a5"
63
+ "gitCommitId": "7d57852"
64
64
  }