@vellumai/assistant 0.11.2-dev.202608051933.deffc0f → 0.11.2-dev.202608052033.7272f57

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.2-dev.202608051933.deffc0f",
3
+ "version": "0.11.2-dev.202608052033.7272f57",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,5 +1,5 @@
1
1
  import { homedir, userInfo } from "node:os";
2
- import { describe, expect, test } from "bun:test";
2
+ import { afterEach, describe, expect, test } from "bun:test";
3
3
 
4
4
  import { renderWorkspaceTopLevelContext } from "../workspace/top-level-renderer.js";
5
5
  import type { TopLevelSnapshot } from "../workspace/top-level-scanner.js";
@@ -214,4 +214,57 @@ describe("renderWorkspaceTopLevelContext", () => {
214
214
  expect(result).toContain("Host home directory: /Users/alice");
215
215
  expect(result).toContain(`Host username: ${userInfo().username}`);
216
216
  });
217
+
218
+ describe("containerized daemon", () => {
219
+ // In a container the daemon's own homedir/username describe the
220
+ // container, not the user's machine. Reporting them as the HOST values
221
+ // is what leads a host_bash command to write to a path that exists
222
+ // nowhere on the user's machine.
223
+ const priorIsContainerized = process.env.IS_CONTAINERIZED;
224
+
225
+ afterEach(() => {
226
+ if (priorIsContainerized === undefined) {
227
+ delete process.env.IS_CONTAINERIZED;
228
+ } else {
229
+ process.env.IS_CONTAINERIZED = priorIsContainerized;
230
+ }
231
+ });
232
+
233
+ const snapshot: TopLevelSnapshot = {
234
+ rootPath: "/workspace",
235
+ directories: ["src"],
236
+ files: ["package.json"],
237
+ truncated: false,
238
+ };
239
+
240
+ test("does not pass off the container's own home dir as the host's", () => {
241
+ process.env.IS_CONTAINERIZED = "true";
242
+
243
+ const result = renderWorkspaceTopLevelContext(snapshot, {});
244
+
245
+ expect(result).not.toContain(`Host home directory: ${homedir()}`);
246
+ // Name the way to find the real value rather than leaving a dead end.
247
+ expect(result.split("\n")).toEqual([
248
+ "<workspace>",
249
+ "Root: /workspace",
250
+ "Directories: src",
251
+ "Files: package.json",
252
+ "Host home directory: unknown (ask the host shell: `host_bash` with `echo $HOME`)",
253
+ "Host username: unknown",
254
+ "</workspace>",
255
+ ]);
256
+ });
257
+
258
+ test("still reports the client-supplied host env when it is known", () => {
259
+ process.env.IS_CONTAINERIZED = "true";
260
+
261
+ const result = renderWorkspaceTopLevelContext(snapshot, {
262
+ hostHomeDir: "/Users/alice",
263
+ hostUsername: "alice",
264
+ });
265
+
266
+ expect(result).toContain("Host home directory: /Users/alice");
267
+ expect(result).toContain("Host username: alice");
268
+ });
269
+ });
217
270
  });
@@ -726,7 +726,15 @@ export const PROVIDER_SEED_DATA: Record<
726
726
  ],
727
727
  availableScopes:
728
728
  "https://learn.microsoft.com/en-us/graph/permissions-reference",
729
- authorizeParams: { prompt: "consent" },
729
+ // `select_account`, not `consent`: the Microsoft identity platform accepts
730
+ // a single prompt value, and `consent` honours an existing session cookie —
731
+ // it re-asks for consent but never for *which* account, so a user who
732
+ // already signed in cannot connect a second mailbox. `select_account`
733
+ // always shows the account picker with its "Use another account" option.
734
+ // Consent is still collected for an account that has not granted it, and
735
+ // refresh tokens come from the `offline_access` scope above, so nothing is
736
+ // lost by dropping `consent`.
737
+ authorizeParams: { prompt: "select_account" },
730
738
  tokenEndpointAuthMethod: "client_secret_post",
731
739
  loopbackPort: 17334,
732
740
  managedServiceConfigKey: "outlook-oauth",
@@ -25,7 +25,12 @@ import type {
25
25
  * a non-string (or empty) value means "untargeted".
26
26
  */
27
27
  export const hostFileEditInputSchema = z.looseObject({
28
- path: z.string().min(1).describe("Absolute host path to the file to edit"),
28
+ path: z
29
+ .string()
30
+ .min(1)
31
+ .describe(
32
+ "Absolute path on the guardian's device, which is a separate filesystem from your workspace, to edit.",
33
+ ),
29
34
  old_string: z
30
35
  .string()
31
36
  .min(1, { message: "old_string must not be empty" })
@@ -38,7 +38,12 @@ import type {
38
38
  * "untargeted".
39
39
  */
40
40
  export const hostFileReadInputSchema = z.looseObject({
41
- path: z.string().min(1).describe("Absolute path to the host file to read"),
41
+ path: z
42
+ .string()
43
+ .min(1)
44
+ .describe(
45
+ "Absolute path on the guardian's device, which is a separate filesystem from your workspace, to read.",
46
+ ),
42
47
  offset: z
43
48
  .number()
44
49
  .describe("Line number to start reading from (1-indexed)")
@@ -24,7 +24,12 @@ import type {
24
24
  * non-string (or empty) value means "untargeted".
25
25
  */
26
26
  export const hostFileWriteInputSchema = z.looseObject({
27
- path: z.string().min(1).describe("Absolute host path to the file to write"),
27
+ path: z
28
+ .string()
29
+ .min(1)
30
+ .describe(
31
+ "Absolute path on the guardian's device, which is a separate filesystem from your workspace, to write.",
32
+ ),
28
33
  content: z.string().describe("The content to write to the file"),
29
34
  target_client_id: z
30
35
  .string()
@@ -121,7 +121,7 @@ export const hostShellInputSchema = z.looseObject({
121
121
  z
122
122
  .string()
123
123
  .describe(
124
- "Optional absolute host working directory (defaults to user home)",
124
+ "Optional absolute path on the host's filesystem, which is separate from your workspace (defaults to the host user's home).",
125
125
  ),
126
126
  ),
127
127
  timeout_seconds: z
@@ -150,7 +150,7 @@ export const hostShellInputSchema = z.looseObject({
150
150
  export const hostShellTool = {
151
151
  name: "host_bash",
152
152
  description:
153
- "LAST RESORT — Execute a shell command directly on the host machine. You MUST strongly prefer the regular `bash` tool for all commands. Only use `host_bash` when you are absolutely certain the command MUST run on the host machine and CANNOT run in the workspace (e.g., managing host-level system services, accessing host-only peripherals, or interacting with host paths outside the workspace). If in doubt, use `bash` instead. Approval-gated: each invocation must be explicitly approved. Do not use for commands that require injected credentials or secrets.",
153
+ "LAST RESORT — Execute a shell command directly on the host machine. You MUST strongly prefer the regular `bash` tool for all commands. Only use `host_bash` when you are absolutely certain the command MUST run on the host machine and CANNOT run in the workspace (e.g., managing host-level system services, accessing host-only peripherals, or interacting with host paths outside the workspace). If in doubt, use `bash` instead. Paths resolve on the host's filesystem, which is separate from your workspace; write output somewhere on the host and read it back with `host_file_read`. Approval-gated: each invocation must be explicitly approved. Do not use for commands that require injected credentials or secrets.",
154
154
  category: "host-terminal",
155
155
  executionTarget: "host",
156
156
  defaultRiskLevel: RiskLevel.Medium,
@@ -1,5 +1,6 @@
1
1
  import { homedir, userInfo } from "node:os";
2
2
 
3
+ import { getIsContainerized } from "../config/env-registry.js";
3
4
  import type { TopLevelSnapshot } from "./top-level-scanner.js";
4
5
 
5
6
  // `os.userInfo()` throws `SystemError` when the current UID has no passwd
@@ -52,10 +53,22 @@ export function renderWorkspaceTopLevelContext(
52
53
  if (snapshot.truncated) {
53
54
  lines.push("(list truncated — more entries exist)");
54
55
  }
55
- lines.push(`Host home directory: ${options.hostHomeDir ?? homedir()}`);
56
+ // The daemon's own `homedir()` / `userInfo()` describe the HOST only when
57
+ // the daemon runs on it. In a container they describe the container, and
58
+ // labelling them "Host …" invites host-side tools (`host_bash`,
59
+ // `host_file_*`, computer use) to be handed a path that exists nowhere on
60
+ // the user's machine. Fall back only when the two are the same machine;
61
+ // otherwise say the value is unknown and name the way to find it, which is
62
+ // cheaper than a failed write on the user's Mac.
63
+ const hostHomeDir =
64
+ options.hostHomeDir ?? (getIsContainerized() ? undefined : homedir());
65
+ const hostUsername =
66
+ options.hostUsername ??
67
+ (getIsContainerized() ? undefined : safeUserInfoUsername());
56
68
  lines.push(
57
- `Host username: ${options.hostUsername ?? safeUserInfoUsername()}`,
69
+ `Host home directory: ${hostHomeDir ?? "unknown (ask the host shell: `host_bash` with `echo $HOME`)"}`,
58
70
  );
71
+ lines.push(`Host username: ${hostUsername ?? "unknown"}`);
59
72
  lines.push("</workspace>");
60
73
  return lines.join("\n");
61
74
  }