@alecsibilia/luca 14.0.0-alpha.24 → 14.0.0-alpha.25

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,9 @@
1
1
  # @alecsibilia/luca
2
2
 
3
+ ## 14.0.0-alpha.25
4
+
5
+ No changes in this release.
6
+
3
7
  ## 14.0.0-alpha.24
4
8
 
5
9
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luca/board",
3
- "version": "14.0.0-alpha.24",
3
+ "version": "14.0.0-alpha.25",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,2 +1,2 @@
1
1
  /** The Luca version of this board, stamped when the package was packed. */
2
- export const LUCA_VERSION: string = "14.0.0-alpha.24"
2
+ export const LUCA_VERSION: string = "14.0.0-alpha.25"
@@ -173,7 +173,7 @@ export const resultJsonSchema = ({
173
173
  * hooks.
174
174
  *
175
175
  * @example
176
- * const options = agentOptions({ role, may_edit_tests, cwd, model: CLAUDE_MODEL, effort: 'xhigh', claude_path, config, common_git_dir, home, env, guard_hook, luca_server: null, delivery_hook: null })
176
+ * const options = agentOptions({ role, may_edit_tests, cwd, model: CLAUDE_MODEL, effort: 'xhigh', claude_path, config, common_git_dir, home, user_temp_dir, env, guard_hook, luca_server: null, delivery_hook: null })
177
177
  */
178
178
  export const agentOptions = ({
179
179
  role,
@@ -185,6 +185,7 @@ export const agentOptions = ({
185
185
  config,
186
186
  common_git_dir,
187
187
  home,
188
+ user_temp_dir,
188
189
  env,
189
190
  guard_hook,
190
191
  luca_server,
@@ -203,6 +204,8 @@ export const agentOptions = ({
203
204
  config: EngineConfig
204
205
  common_git_dir: string
205
206
  home: string
207
+ /** macOS's per-user temp folder (`darwinUserTempDir`), or `null`. */
208
+ user_temp_dir: string | null
206
209
  env: Record<string, string>
207
210
  guard_hook: HookCallback
208
211
  /** The engine's `luca` server (`createLucaServer`), or `null` for none. */
@@ -253,6 +256,7 @@ export const agentOptions = ({
253
256
  worktree: cwd,
254
257
  common_git_dir,
255
258
  home,
259
+ user_temp_dir,
256
260
  config,
257
261
  }),
258
262
  env,
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Where the Claude launcher finds things on disk: the real path of the
3
- * `claude` binary, and the repo's common git dir (shared by its worktrees).
3
+ * `claude` binary, the repo's common git dir (shared by its worktrees), and
4
+ * macOS's per-user temp folder.
4
5
  */
5
6
  import { realpathSync } from 'node:fs'
7
+ import { isAbsolute } from 'node:path'
6
8
 
7
9
  import { runCommand } from '../shell/run-command'
8
10
 
@@ -27,3 +29,41 @@ export const commonGitDir = async ({
27
29
  }
28
30
  return realpathSync(result.stdout.trim())
29
31
  }
32
+
33
+ /** The command that prints macOS's per-user temp folder. */
34
+ export const GETCONF_USER_TEMP_DIR = ['getconf', 'DARWIN_USER_TEMP_DIR']
35
+
36
+ /**
37
+ * The real path of macOS's per-user temp folder (`getconf
38
+ * DARWIN_USER_TEMP_DIR`, such as `/private/var/folders/ab/cd1234/T`), or
39
+ * `null` off macOS, or when `getconf` fails or names no folder there.
40
+ *
41
+ * A sandboxed writer needs it: Foundation stages every atomic write there,
42
+ * not in `$TMPDIR`, so `swift test` and `xcodebuild` can't rebuild without
43
+ * it ("unable to write manifest ... Operation not permitted").
44
+ *
45
+ * @example
46
+ * const user_temp_dir = await darwinUserTempDir({ platform: process.platform, getconf: GETCONF_USER_TEMP_DIR })
47
+ */
48
+ export const darwinUserTempDir = async ({
49
+ platform,
50
+ getconf,
51
+ }: {
52
+ platform: NodeJS.Platform
53
+ /** The command to ask; the launcher passes `GETCONF_USER_TEMP_DIR`. */
54
+ getconf: string[]
55
+ }): Promise<string | null> => {
56
+ if (platform !== 'darwin') return null
57
+ try {
58
+ const result = await runCommand({
59
+ cmd: getconf,
60
+ cwd: '/',
61
+ timeout_ms: 10_000,
62
+ })
63
+ const path = result.stdout.trim()
64
+ if (result.exit_code !== 0 || !isAbsolute(path)) return null
65
+ return realpathSync(path)
66
+ } catch {
67
+ return null
68
+ }
69
+ }
@@ -25,7 +25,12 @@ import {
25
25
  } from './claude-messages'
26
26
  import { agentEnv, agentOptions, checkLaunchModel } from './claude-options'
27
27
  import { createInputChannel } from './input-channel'
28
- import { commonGitDir, realClaudePath } from './launch-paths'
28
+ import {
29
+ commonGitDir,
30
+ darwinUserTempDir,
31
+ GETCONF_USER_TEMP_DIR,
32
+ realClaudePath,
33
+ } from './launch-paths'
29
34
  import { createDeliveryHook, createLucaServer } from './message-tool'
30
35
  import {
31
36
  nextOutcome,
@@ -116,6 +121,10 @@ export const createLaunch = ({
116
121
  config,
117
122
  common_git_dir: gitDir,
118
123
  home: realpathSync(homedir()),
124
+ user_temp_dir: await darwinUserTempDir({
125
+ platform: process.platform,
126
+ getconf: GETCONF_USER_TEMP_DIR,
127
+ }),
119
128
  env: agentEnv({ source: process.env }),
120
129
  guard_hook: createGuardHook({
121
130
  role: guardRoleOf({ role }),
@@ -2,6 +2,7 @@ import { isAbsolute, join } from 'node:path'
2
2
 
3
3
  import type { SandboxSettings } from '@anthropic-ai/claude-agent-sdk'
4
4
 
5
+ import { isWriter } from './guard-roles'
5
6
  import { LOCKFILES, type GuardRole } from './role-rules'
6
7
 
7
8
  import type { EngineConfig } from '../config/engine-config'
@@ -37,8 +38,13 @@ const mustBeAbsolute = (paths: Record<string, string>): void => {
37
38
  * learner, and a test-writer that may not edit tests may not write the
38
39
  * worktree at all.
39
40
  *
41
+ * Test-writers and implementers, who run the test commands, may also write
42
+ * macOS's per-user temp folder (`user_temp_dir`), where Foundation stages
43
+ * atomic writes: without it, `swift test` and `xcodebuild` can't rebuild.
44
+ * Nothing else is opened, and nothing at all without that folder.
45
+ *
40
46
  * @example
41
- * const sandbox = sandboxSettings({ role: 'implementer', may_edit_tests: false, worktree, common_git_dir, home, config })
47
+ * const sandbox = sandboxSettings({ role: 'implementer', may_edit_tests: false, worktree, common_git_dir, home, user_temp_dir, config })
42
48
  */
43
49
  export const sandboxSettings = ({
44
50
  role,
@@ -46,6 +52,7 @@ export const sandboxSettings = ({
46
52
  worktree,
47
53
  common_git_dir,
48
54
  home,
55
+ user_temp_dir,
49
56
  config,
50
57
  }: {
51
58
  role: GuardRole
@@ -56,9 +63,19 @@ export const sandboxSettings = ({
56
63
  /** The repo's shared `.git` folder (`git rev-parse --git-common-dir`). */
57
64
  common_git_dir: string
58
65
  home: string
66
+ /**
67
+ * macOS's per-user temp folder (`darwinUserTempDir`), real and
68
+ * absolute, or `null` off macOS or when it can't be found.
69
+ */
70
+ user_temp_dir: string | null
59
71
  config: EngineConfig
60
72
  }): SandboxSettings => {
61
- mustBeAbsolute({ worktree, common_git_dir, home })
73
+ mustBeAbsolute({
74
+ worktree,
75
+ common_git_dir,
76
+ home,
77
+ ...(user_temp_dir === null ? {} : { user_temp_dir }),
78
+ })
62
79
  const git = [common_git_dir, join(worktree, '.git')]
63
80
  const inWorktree = (patterns: string[]) =>
64
81
  patterns.map((pattern) => join(worktree, pattern))
@@ -87,6 +104,9 @@ export const sandboxSettings = ({
87
104
  allowLocalBinding: false,
88
105
  },
89
106
  filesystem: {
107
+ ...(user_temp_dir !== null && isWriter(role)
108
+ ? { allowWrite: [user_temp_dir] }
109
+ : {}),
90
110
  denyWrite: [...git, ...byRole[role]],
91
111
  denyRead: SECRET_PATHS.map((path) => join(home, path)),
92
112
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alecsibilia/luca",
3
3
  "description": "Luca: turns a spec on GitHub Issues into a reviewed pull request, run from Paseo with Claude Code",
4
- "version": "14.0.0-alpha.24",
4
+ "version": "14.0.0-alpha.25",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Alec Sibilia <sibilia.alec@gmail.com>",
7
7
  "homepage": "https://github.com/asibilia/luca-framework#readme",