Skip to content

Commit 62820ca

Browse files
committed
Release v0.2.53
1 parent f05c862 commit 62820ca

24 files changed

Lines changed: 653 additions & 117 deletions

‎RELEASE.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
2-
"version": "0.2.52",
2+
"version": "0.2.53",
33
"license": "FSL-1.1-ALv2",
44
"baselineSha256": "7847f65a9b20b366a60d52d19ca3cd6bdd1bb09b8cb506659a270f27a21d8877",
55
"e2b": {
66
"recipeSha256": "761809ee2efa2b6c750451ace29283cbb8e55b6ce3b5b14c2bb4509df71df801",
77
"profileSha256": "414dbecee2d47de36016ea05c736e2edff3a0deb298babf923887ea184a01c10"
88
},
99
"dxd": {
10-
"url": "https://github.com/dxcode-dev/dxcode/releases/download/v0.2.52/dxd-linux-x64",
10+
"url": "https://github.com/dxcode-dev/dxcode/releases/download/v0.2.53/dxd-linux-x64",
1111
"sha256": "6d2409188da917eaf4fbe03e7be6c25293442b5fa2901ba07c844e16b2adff5b"
1212
},
1313
"toolchain": {

‎apps/core/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@dx/core",
33
"private": true,
4-
"version": "0.2.52",
4+
"version": "0.2.53",
55
"license": "FSL-1.1-ALv2",
66
"type": "module",
77
"scripts": {

‎apps/core/src/claude-code/claude-code.test.ts‎

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { createClaudeCodeProvider } from "./provider.js";
3333
import { claudeCodeSandbox } from "./runs.js";
3434
import { ClaudeCodeStream } from "./stream.js";
3535
import { claudeCodeMirrorTools } from "./tools.js";
36+
import { claudeCodeAnswer } from "./workspace-run.js";
3637

3738
/**
3839
* Claude Code mode against a Thread workspace on this machine: guest paths
@@ -127,7 +128,7 @@ exit 1
127128
const createGuest = async (
128129
options: {
129130
readonly claude?: boolean;
130-
/** A Claude Code of this version on PATH instead of the current one. */
131+
/** A Claude Code of this version instead of the current one. */
131132
readonly oldClaude?: string;
132133
/** The downloaded installer installs the current Claude Code. */
133134
readonly installerWorks?: boolean;
@@ -167,16 +168,29 @@ exec /usr/bin/awk "$@"
167168
? `#!/bin/sh\necho 'mkdir -p "$HOME/.local/bin" && cp ${join(root, "claude-latest")} "$HOME/.local/bin/claude"'\n`
168169
: "#!/bin/sh\necho 'echo \"Installation failed: checksum mismatch\" >&2; exit 1'\n",
169170
);
170-
const installClaude = () => tool("claude", FAKE_CLAUDE);
171+
// dx runs Claude Code from where its installer puts it, never from PATH.
172+
const claudeAt = (body: string) =>
173+
mkdir(join(home, ".local/bin"), { recursive: true }).then(() =>
174+
writeFile(join(home, ".local/bin/claude"), body, { mode: 0o755 }),
175+
);
176+
const installClaude = () => claudeAt(FAKE_CLAUDE);
171177
if (options.oldClaude !== undefined)
172-
await tool("claude", OLD_CLAUDE(options.oldClaude));
178+
await claudeAt(OLD_CLAUDE(options.oldClaude));
173179
else if (options.claude !== false) await installClaude();
174180
const base = await local({
175181
cwd: join(home, "workspace/repo"),
176182
env: { HOME: home, PATH: `${bin}:/usr/bin:/bin` },
177183
}).createSandbox({ id: "guest" });
178184
const map = (value: string) => value.replaceAll(GUEST_HOME, home);
179185
const ops: GuestOp[] = [];
186+
/**
187+
* What each command carried to the provider: a provider runs it under
188+
* \`bash -l\`, whose profile sees its text and environment.
189+
*/
190+
const commands: Array<{
191+
readonly command: string;
192+
readonly env: Readonly<Record<string, string>>;
193+
}> = [];
180194
/** Runs before each operation; a returned error fails it. */
181195
let before: (op: GuestOp) => Error | undefined = () => undefined;
182196
const track = async <A>(
@@ -193,8 +207,10 @@ exec /usr/bin/awk "$@"
193207
const sandbox: Sandbox = {
194208
cwd: `${GUEST_HOME}/workspace/repo`,
195209
resolvePath: (path) => path,
196-
exec: (command, execOptions) =>
197-
track("exec", command, () => base.exec(map(command), execOptions)),
210+
exec: (command, execOptions) => {
211+
commands.push({ command, env: execOptions?.env ?? {} });
212+
return track("exec", command, () => base.exec(map(command), execOptions));
213+
},
198214
readFile: (path) => track("read", path, () => base.readFile(map(path))),
199215
readFileBuffer: (path) =>
200216
track("read", path, () => base.readFileBuffer(map(path))),
@@ -208,9 +224,23 @@ exec /usr/bin/awk "$@"
208224
rm: (path, rmOptions) =>
209225
track("write", path, () => base.rm(map(path), rmOptions)),
210226
};
227+
/** Files under the guest's home, other than the fake CLI's, that hold it. */
228+
const filesHolding = async (value: string) => {
229+
const found: string[] = [];
230+
for (const name of await readdir(home, { recursive: true })) {
231+
if (name === "claude-token") continue;
232+
const contents = await readFile(join(home, name), "latin1").catch(
233+
() => undefined,
234+
);
235+
if (contents?.includes(value) === true) found.push(name);
236+
}
237+
return found;
238+
};
211239
return {
212240
sandbox,
213241
ops,
242+
commands,
243+
filesHolding,
214244
installClaude,
215245
/** Put a command on the guest's PATH ahead of the machine's. */
216246
tool,
@@ -486,7 +516,7 @@ describe("Claude Code mode in a workspace", { timeout: 30_000 }, () => {
486516
expect(await guest.sandbox.exists(userInstructions)).toBe(false);
487517
});
488518

489-
it("hands the plan token to the CLI alone, never to a process environment", async () => {
519+
it("hands the plan token to the CLI alone, never to a process environment, a command, or a file left behind", async () => {
490520
const guest = await createGuest();
491521
await guest.script([
492522
init(),
@@ -501,6 +531,42 @@ describe("Claude Code mode in a workspace", { timeout: 30_000 }, () => {
501531
expect(done).toMatchObject({ type: "done", reason: "stop" });
502532
expect(guest.home("claude-token")).toBe(claude.token);
503533
expect(guest.home("claude-token-readers")).toBe("");
534+
expect(guest.commands.length).toBeGreaterThan(0);
535+
expect(
536+
guest.commands.filter(
537+
({ command, env }) =>
538+
command.includes(claude.token) ||
539+
Object.values(env).some((value) => value.includes(claude.token)),
540+
),
541+
).toEqual([]);
542+
expect(await guest.filesHolding(claude.token)).toEqual([]);
543+
});
544+
545+
it("answers a title on the plan token without giving it to a command or leaving it in a file", async () => {
546+
const guest = await createGuest();
547+
await guest.script([
548+
init(),
549+
...response("m1", [{ text: "Greeting" }]),
550+
result("Greeting"),
551+
]);
552+
const token = `plan-token-${crypto.randomUUID()}`;
553+
const answer = await claudeCodeAnswer(guest.sandbox, {
554+
model: "claude-haiku-5-5",
555+
token,
556+
input: "Title this: say hello",
557+
});
558+
expect(answer.text).toBe("Greeting");
559+
expect(guest.home("claude-token")).toBe(token);
560+
expect(guest.home("claude-token-readers")).toBe("");
561+
expect(guest.home("claude-stdin.jsonl")).toBe("Title this: say hello");
562+
expect(
563+
guest.commands.filter(
564+
({ command, env }) =>
565+
command.includes(token) ||
566+
Object.values(env).some((value) => value.includes(token)),
567+
),
568+
).toEqual([]);
569+
expect(await guest.filesHolding(token)).toEqual([]);
504570
});
505571

506572
it("launches with a prompt too large for one command line", async () => {

0 commit comments

Comments
 (0)