jeo-code 0.12.15 → 0.12.16

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
@@ -9,6 +9,17 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.12.16] - 2026-11-04
13
+
14
+ ### Upgrade
15
+ - **Discord messenger:** Existing configurations whose `cwd` overlaps the messenger profile directory now fail closed. Set `cwd` to a directory disjoint from the profile before restarting; re-running setup preserves an explicitly configured `cwd` and does not relocate files. Changing `cwd` starts new sessions for existing threads.
16
+
17
+ ### Fixed
18
+ - **Discord concurrency** — independent threads can progress concurrently, and same-thread follow-ups wait without occupying a worker slot; rejected full-queue requests receive a failure status without creating a thread.
19
+ - **Discord sandbox setup** — create fresh workspaces outside private configuration paths, validate workspace admission before replacing settings, include `sandbox_request` in the default tool list, and keep messenger profile files outside workspace and permission grants.
20
+ - **Antigravity model responses** — retry malformed function-call responses within the existing retry limit without executing incomplete tool calls or replaying already-streamed output.
21
+ - **Windows sandbox** — preserve owner-only ACL protection and serialize authority directory lifecycle.
22
+
12
23
  ## [0.12.15] - 2026-11-03
13
24
 
14
25
  The GitHub-only 0.12.14 release was not published to npm; its changes (listed under [0.12.14]) are included in this release.
@@ -2671,7 +2682,8 @@ _Initial release._
2671
2682
  ### Added
2672
2683
  - Initial jeo-code agent and CLI.
2673
2684
 
2674
- [Unreleased]: https://github.com/akillness/jeo-code/compare/v0.12.6...HEAD
2685
+ [Unreleased]: https://github.com/akillness/jeo-code/compare/v0.12.16...HEAD
2686
+ [0.12.16]: https://github.com/akillness/jeo-code/releases/tag/v0.12.16
2675
2687
  [0.12.6]: https://github.com/akillness/jeo-code/releases/tag/v0.12.6
2676
2688
  [0.11.4]: https://github.com/akillness/jeo-code/releases/tag/v0.11.4
2677
2689
  [0.11.3]: https://github.com/akillness/jeo-code/releases/tag/v0.11.3
package/README.ja.md CHANGED
@@ -447,10 +447,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
447
447
 
448
448
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
449
449
  - **[Unreleased]**
450
+ - **[0.12.16]** (2026-11-04)
450
451
  - **[0.12.15]** (2026-11-03)
451
452
  - **[0.12.14]** (2026-11-03)
452
453
  - **[0.12.13]** (2026-11-03)
453
- - **[0.12.12]** (2026-11-03)
454
454
 
455
455
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
456
456
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -502,10 +502,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
502
502
 
503
503
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
504
504
  - **[Unreleased]**
505
+ - **[0.12.16]** (2026-11-04)
505
506
  - **[0.12.15]** (2026-11-03)
506
507
  - **[0.12.14]** (2026-11-03)
507
508
  - **[0.12.13]** (2026-11-03)
508
- - **[0.12.12]** (2026-11-03)
509
509
 
510
510
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
511
511
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -491,10 +491,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
491
491
 
492
492
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
493
493
  - **[Unreleased]**
494
+ - **[0.12.16]** (2026-11-04)
494
495
  - **[0.12.15]** (2026-11-03)
495
496
  - **[0.12.14]** (2026-11-03)
496
497
  - **[0.12.13]** (2026-11-03)
497
- - **[0.12.12]** (2026-11-03)
498
498
 
499
499
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
500
500
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -445,10 +445,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
445
445
 
446
446
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
447
447
  - **[Unreleased]**
448
+ - **[0.12.16]** (2026-11-04)
448
449
  - **[0.12.15]** (2026-11-03)
449
450
  - **[0.12.14]** (2026-11-03)
450
451
  - **[0.12.13]** (2026-11-03)
451
- - **[0.12.12]** (2026-11-03)
452
452
 
453
453
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
454
454
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.12.15",
3
+ "version": "0.12.16",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
 
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import { canonicalSandboxPath, createSandboxPolicy, sandboxPathContains } from "./policy";
4
+ import { canonicalSandboxPath, createSandboxPolicy, sandboxPathContains, windowsSystemRoot } from "./policy";
5
5
  import type { SandboxConfig, SandboxMount, SandboxSupervisor, SandboxTermination } from "./types";
6
6
 
7
7
  /** Constructed by trusted parent entry points, never from model/tool arguments. */
@@ -50,61 +50,124 @@ export interface GrantAuthorityOptions {
50
50
 
51
51
  type DecisionWaiter = { resolve(value: GrantRequest): void; reject(reason: Error): void };
52
52
 
53
+ const pendingPrivateDirectories = new Map<string, Promise<string>>();
54
+
53
55
  /** Create app-private state, or verify existing state without widening its ACL. */
54
56
  export async function ensurePrivateSandboxDirectory(directory: string): Promise<string> {
55
57
  const canonical = await canonicalSandboxPath(directory, false);
56
- if (process.platform === "win32") {
57
- const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT;
58
- if (!systemRoot || !path.isAbsolute(systemRoot)) throw new Error("SANDBOX_STATE: Windows system directory is unavailable");
59
- await fs.mkdir(path.dirname(canonical), { recursive: true });
60
- // Fixed parent-owned program; paths are data, never interpolated into code.
61
- // Windows PowerShell 5.1 provides .NET Framework's atomic ACL creation API.
62
- const script = `
58
+ const key = process.platform === "win32" ? canonical.toLowerCase() : canonical;
59
+ const active = pendingPrivateDirectories.get(key);
60
+ if (active) return active;
61
+ const operation = (async (): Promise<string> => {
62
+ if (process.platform === "win32") {
63
+ const systemRoot = windowsSystemRoot();
64
+ await fs.mkdir(path.dirname(canonical), { recursive: true });
65
+ // Fixed parent-owned program; paths are data, never interpolated into code.
66
+ // Windows PowerShell 5.1 provides .NET Framework's ACL creation and SetAccessControl APIs.
67
+ // Note: Win32 CreateDirectoryW applies explicit ACEs from SECURITY_ATTRIBUTES but strips
68
+ // SE_DACL_PROTECTED unless SetAccessControl (SetNamedSecurityInfoW) is called afterwards.
69
+ const script = `
63
70
  $ErrorActionPreference = 'Stop'
71
+ $mutex = New-Object System.Threading.Mutex($false, 'Local\\jeo-sandbox-private-directory')
72
+ $locked = $false
64
73
  try {
74
+ try { $locked = $mutex.WaitOne(30000) } catch [System.Threading.AbandonedMutexException] { $locked = $true }
75
+ if (-not $locked) { throw 'mutex timeout' }
65
76
  $directory = $env:JEO_SANDBOX_PRIVATE_DIRECTORY
66
- $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
77
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
78
+ $sid = $identity.User
79
+ $tokenOwner = $identity.Owner
67
80
  $inherit = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
81
+ $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($sid, 'FullControl', $inherit, 'None', 'Allow')
82
+ $daclSecurity = New-Object System.Security.AccessControl.DirectorySecurity
83
+ $daclSecurity.SetAccessRuleProtection($true, $false)
84
+ $daclSecurity.AddAccessRule($accessRule)
85
+ $created = $false
68
86
  if (-not [System.IO.Directory]::Exists($directory)) {
69
- $security = New-Object System.Security.AccessControl.DirectorySecurity
70
- $security.SetAccessRuleProtection($true, $false)
71
- $security.SetOwner($sid)
72
- $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($sid, 'FullControl', $inherit, 'None', 'Allow')
73
- $security.AddAccessRule($rule)
74
- [void][System.IO.Directory]::CreateDirectory($directory, $security)
87
+ $createSecurity = New-Object System.Security.AccessControl.DirectorySecurity
88
+ $createSecurity.SetAccessRuleProtection($true, $false)
89
+ $createSecurity.SetOwner($sid)
90
+ $createSecurity.AddAccessRule($accessRule)
91
+ [void][System.IO.Directory]::CreateDirectory($directory, $createSecurity)
92
+ $created = $true
93
+ }
94
+ try {
95
+ if (([System.IO.File]::GetAttributes($directory) -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'reparse point' }
96
+ if ($created) {
97
+ [System.IO.Directory]::SetAccessControl($directory, $daclSecurity)
98
+ $currentOwner = [System.IO.Directory]::GetAccessControl($directory).GetOwner([System.Security.Principal.SecurityIdentifier])
99
+ if ($currentOwner.Value -ne $sid.Value) {
100
+ $ownerSecurity = [System.IO.Directory]::GetAccessControl($directory)
101
+ $ownerSecurity.SetAccessRuleProtection($true, $false)
102
+ $ownerSecurity.SetOwner($sid)
103
+ [System.IO.Directory]::SetAccessControl($directory, $ownerSecurity)
104
+ }
105
+ } else {
106
+ $existing = [System.IO.Directory]::GetAccessControl($directory)
107
+ $existingOwner = $existing.GetOwner([System.Security.Principal.SecurityIdentifier])
108
+ $existingRules = $existing.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])
109
+ if ($existingRules.Count -eq 1) {
110
+ $existingRule = $existingRules[0]
111
+ $ownerMatchesToken = ($existingOwner.Value -eq $sid.Value) -or ($null -ne $tokenOwner -and $existingOwner.Value -eq $tokenOwner.Value)
112
+ if ($ownerMatchesToken -and $existingRule.IdentityReference.Value -eq $sid.Value -and -not $existingRule.IsInherited -and $existingRule.AccessControlType -eq 'Allow' -and $existingRule.FileSystemRights -eq 'FullControl' -and $existingRule.InheritanceFlags -eq $inherit -and $existingRule.PropagationFlags -eq 'None') {
113
+ if (-not $existing.AreAccessRulesProtected) {
114
+ [System.IO.Directory]::SetAccessControl($directory, $daclSecurity)
115
+ }
116
+ if ($existingOwner.Value -ne $sid.Value) {
117
+ $ownerSecurity = [System.IO.Directory]::GetAccessControl($directory)
118
+ $ownerSecurity.SetAccessRuleProtection($true, $false)
119
+ $ownerSecurity.SetOwner($sid)
120
+ [System.IO.Directory]::SetAccessControl($directory, $ownerSecurity)
121
+ }
122
+ }
123
+ }
124
+ }
125
+ $actual = [System.IO.Directory]::GetAccessControl($directory)
126
+ if (-not $actual.AreAccessRulesProtected -or $actual.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'owner or inheritance' }
127
+ $rules = $actual.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])
128
+ if ($rules.Count -ne 1) { throw 'unexpected access rules' }
129
+ $rule = $rules[0]
130
+ if ($rule.IdentityReference.Value -ne $sid.Value -or $rule.IsInherited -or $rule.AccessControlType -ne 'Allow' -or $rule.FileSystemRights -ne 'FullControl' -or $rule.InheritanceFlags -ne $inherit -or $rule.PropagationFlags -ne 'None') { throw 'non-private rule' }
131
+ } catch {
132
+ if ($created) { try { [System.IO.Directory]::Delete($directory, $false) } catch {} }
133
+ throw
75
134
  }
76
- if (([System.IO.File]::GetAttributes($directory) -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'reparse point' }
77
- $actual = [System.IO.Directory]::GetAccessControl($directory)
78
- if (-not $actual.AreAccessRulesProtected -or $actual.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'owner or inheritance' }
79
- $rules = $actual.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])
80
- if ($rules.Count -ne 1) { throw 'unexpected access rules' }
81
- $rule = $rules[0]
82
- if ($rule.IdentityReference.Value -ne $sid.Value -or $rule.IsInherited -or $rule.AccessControlType -ne 'Allow' -or $rule.FileSystemRights -ne 'FullControl' -or $rule.InheritanceFlags -ne $inherit -or $rule.PropagationFlags -ne 'None') { throw 'non-private rule' }
83
135
  [Console]::Out.Write('JEO_PRIVATE_DIRECTORY_VERIFIED')
84
136
  exit 0
85
137
  } catch { exit 1 }
138
+ finally {
139
+ if ($locked) { try { $mutex.ReleaseMutex() } catch {} }
140
+ $mutex.Dispose()
141
+ }
86
142
  `;
87
- const executable = path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
88
- const child = Bun.spawn([executable, "-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], {
89
- env: { SystemRoot: systemRoot, JEO_SANDBOX_PRIVATE_DIRECTORY: canonical },
90
- stdin: "ignore", stdout: "pipe", stderr: "ignore",
91
- });
92
- const timer = setTimeout(() => child.kill(), 30_000);
93
- try {
94
- const [exitCode, output] = await Promise.all([child.exited, new Response(child.stdout).text()]);
95
- if (exitCode !== 0 || output !== "JEO_PRIVATE_DIRECTORY_VERIFIED") {
96
- throw new Error("SANDBOX_STATE: private Windows owner-only ACL could not be verified");
97
- }
98
- } finally { clearTimeout(timer); }
99
- } else {
100
- await fs.mkdir(canonical, { recursive: true, mode: 0o700 });
101
- }
102
- const stat = await fs.lstat(canonical);
103
- if (!stat.isDirectory() || stat.isSymbolicLink() ||
104
- (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.()))) {
105
- throw new Error("SANDBOX_STATE: authority state must be a private owner-only directory");
143
+ const executable = path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
144
+ const child = Bun.spawn([executable, "-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], {
145
+ env: { SystemRoot: systemRoot, JEO_SANDBOX_PRIVATE_DIRECTORY: canonical },
146
+ stdin: "ignore", stdout: "pipe", stderr: "ignore",
147
+ });
148
+ const timer = setTimeout(() => child.kill(), 30_000);
149
+ try {
150
+ const [exitCode, output] = await Promise.all([child.exited, new Response(child.stdout).text()]);
151
+ if (exitCode !== 0 || output !== "JEO_PRIVATE_DIRECTORY_VERIFIED") {
152
+ throw new Error("SANDBOX_STATE: private Windows owner-only ACL could not be verified");
153
+ }
154
+ } finally { clearTimeout(timer); }
155
+ } else {
156
+ await fs.mkdir(canonical, { recursive: true, mode: 0o700 });
157
+ }
158
+ const stat = await fs.lstat(canonical);
159
+ if (!stat.isDirectory() || stat.isSymbolicLink() ||
160
+ (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.()))) {
161
+ throw new Error("SANDBOX_STATE: authority state must be a private owner-only directory");
162
+ }
163
+ return canonical;
164
+ })();
165
+ pendingPrivateDirectories.set(key, operation);
166
+ try {
167
+ return await operation;
168
+ } finally {
169
+ if (pendingPrivateDirectories.get(key) === operation) pendingPrivateDirectories.delete(key);
106
170
  }
107
- return canonical;
108
171
  }
109
172
 
110
173
  /** All mutable authority remains in this parent closure, outside worker mounts. */
@@ -19,8 +19,11 @@ export async function prepareHandsBundle(): Promise<string> {
19
19
  const temporary = path.join(staging, "hands.js");
20
20
  try {
21
21
  const output = await fs.open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
22
- try { await output.writeFile(source); await output.sync(); await output.chmod(0o400); }
23
- finally { await output.close(); }
22
+ try {
23
+ await output.writeFile(source);
24
+ await output.sync();
25
+ if (process.platform !== "win32") await output.chmod(0o400);
26
+ } finally { await output.close(); }
24
27
  try { await fs.link(temporary, file); }
25
28
  catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
26
29
  } finally { await fs.rm(staging, { recursive: true, force: true }); }
@@ -76,6 +76,14 @@ export function createDockerBackend(options: DockerBackendOptions = {}): Sandbox
76
76
  Bun.spawn([...argv], { env: environment, stdin: "pipe", stdout: "pipe", stderr: "pipe" }) as unknown as DockerChild);
77
77
  const command = (args: readonly string[]) => run([executable, ...args]);
78
78
 
79
+ function normalizeMountSource(value: string): string {
80
+ if (platform !== "win32") return value;
81
+ const match = /^\/(?:run\/desktop\/mnt\/host|host_mnt)\/([a-zA-Z])(?:\/(.*))?$/.exec(value.replace(/\\/g, "/"));
82
+ if (!match) return value;
83
+ const drive = `${match[1]!.toUpperCase()}:\\`;
84
+ return match[2] ? path.win32.join(drive, ...match[2].split("/").filter(Boolean)) : drive;
85
+ }
86
+
79
87
  async function checked(args: readonly string[], context: string): Promise<string> {
80
88
  let result: DockerCommandResult;
81
89
  try { result = await command(args); }
@@ -158,8 +166,9 @@ export function createDockerBackend(options: DockerBackendOptions = {}): Sandbox
158
166
  if (!labels?.[OWNER] || !labels[SESSION] || !/^\d+$/.test(labels[REVISION] ?? "") || !Array.isArray(record.Mounts)) throw new Error("SANDBOX_UNAVAILABLE: unknown managed ownership or mounts");
159
167
  for (const mount of record.Mounts) {
160
168
  if (mount.Type === "tmpfs" && mount.Destination === "/tmp") continue;
161
- if (mount.Type !== "bind" || typeof mount.Source !== "string" || !path.isAbsolute(mount.Source)) throw new Error("SANDBOX_UNAVAILABLE: unknown managed mount metadata");
162
- const source = await canonicalSandboxPath(mount.Source);
169
+ const normalizedSource = typeof mount.Source === "string" ? normalizeMountSource(mount.Source) : "";
170
+ if (mount.Type !== "bind" || !normalizedSource || !path.isAbsolute(normalizedSource)) throw new Error("SANDBOX_UNAVAILABLE: unknown managed mount metadata");
171
+ const source = await canonicalSandboxPath(normalizedSource);
163
172
  if (sources.some(candidate => candidate !== source && (sandboxPathContains(candidate, source) || sandboxPathContains(source, candidate)))) {
164
173
  throw new Error("SANDBOX_MOUNT_OVERLAP: managed mount roots must be identical or disjoint");
165
174
  }
@@ -262,7 +271,8 @@ export function createDockerBackend(options: DockerBackendOptions = {}): Sandbox
262
271
  if (actual.length !== expected.length) throw new Error("SANDBOX_UNAVAILABLE: unexpected container bind mounts");
263
272
  for (const mount of expected) {
264
273
  const observed = actual.find(candidate => candidate.Destination === mount.guest);
265
- if (!observed?.Source || await canonicalSandboxPath(observed.Source) !== mount.source || observed.RW !== mount.writable) throw new Error("SANDBOX_UNAVAILABLE: actual container bind configuration changed");
274
+ const normalizedObserved = observed?.Source ? normalizeMountSource(observed.Source) : "";
275
+ if (!normalizedObserved || await canonicalSandboxPath(normalizedObserved) !== mount.source || observed?.RW !== mount.writable) throw new Error("SANDBOX_UNAVAILABLE: actual container bind configuration changed");
266
276
  }
267
277
  if (record.State?.Running === true) break;
268
278
  if (Date.now() >= deadline) throw new Error("SANDBOX_UNAVAILABLE: container did not start");