@venturewild/workspace 0.6.21 → 0.6.23
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
|
@@ -128,9 +128,11 @@ export async function fetchLatestVersion(channel, {
|
|
|
128
128
|
/** Run `npm i -g <spec>`. Resolves {code, output, timedOut?, error?}; never rejects. */
|
|
129
129
|
export function npmInstall(spec, {
|
|
130
130
|
spawnImpl = spawn, timeoutMs = 180000, ensurePathImpl = ensureToolPath, env = process.env,
|
|
131
|
+
platform = process.platform,
|
|
131
132
|
} = {}) {
|
|
132
133
|
return new Promise((resolve) => {
|
|
133
|
-
const
|
|
134
|
+
const isWin = platform === 'win32';
|
|
135
|
+
const cmd = isWin ? 'npm.cmd' : 'npm';
|
|
134
136
|
// The always-on supervisor (our caller in the field) runs under launchd/GUI,
|
|
135
137
|
// which inherits a MINIMAL PATH omitting ~/.npm-global, /usr/local/bin,
|
|
136
138
|
// Homebrew, nvm — so a bare `npm` spawn would ENOENT (the 0.1.8 `claude`
|
|
@@ -140,7 +142,15 @@ export function npmInstall(spec, {
|
|
|
140
142
|
try { ensurePathImpl(childEnv); } catch { /* best-effort — fall back to inherited PATH */ }
|
|
141
143
|
let child;
|
|
142
144
|
try {
|
|
143
|
-
|
|
145
|
+
// On Windows npm is a `.cmd` shim, and Node >=18.20.2/20.12.2/22 REFUSES to
|
|
146
|
+
// spawn a .cmd/.bat directly (CVE-2024-27980 hardening) — it throws EINVAL
|
|
147
|
+
// synchronously. That is exactly how field auto-update failed instantly
|
|
148
|
+
// (code=-1 in ~16ms, never reaching npm). shell:true routes the shim
|
|
149
|
+
// through cmd.exe so it actually runs. The only interpolated arg is a
|
|
150
|
+
// package spec we build ourselves (a constant name + a semver from npm's
|
|
151
|
+
// own dist-tag), so there is no shell-injection surface. POSIX stays
|
|
152
|
+
// shell:false — npm is a real executable there.
|
|
153
|
+
child = spawnImpl(cmd, ['i', '-g', spec], { windowsHide: true, env: childEnv, shell: isWin });
|
|
144
154
|
} catch (e) {
|
|
145
155
|
return resolve({ code: -1, error: e?.message || String(e), output: '' });
|
|
146
156
|
}
|
|
@@ -226,7 +236,7 @@ export class AutoUpdater {
|
|
|
226
236
|
this.logImpl(`auto-update: installing ${this.packageName}@${target} (from ${from || 'unknown'})`);
|
|
227
237
|
const install = await this.installImpl(`${this.packageName}@${target}`);
|
|
228
238
|
if (install.code !== 0) {
|
|
229
|
-
this.logImpl(`auto-update: install failed code=${install.code}${install.timedOut ? ' (timeout)' : ''}`);
|
|
239
|
+
this.logImpl(`auto-update: install failed code=${install.code}${install.timedOut ? ' (timeout)' : ''}${install.error ? ` error=${install.error}` : ''}`);
|
|
230
240
|
const rec = recordUpdate(this.globalDir, { from, to: target, at: this.nowImpl(), status: 'install-failed' });
|
|
231
241
|
this.onUpdate?.(rec);
|
|
232
242
|
return { ok: false, stage: 'install', install, rec };
|
package/server/src/index.mjs
CHANGED
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
seedDefault,
|
|
77
77
|
workspaceIdForDir,
|
|
78
78
|
} from './workspace-registry.mjs';
|
|
79
|
+
import { ensureDataDirScaffold } from './workspace-scaffold.mjs';
|
|
79
80
|
import { createWorkspaceRails } from './workspaces.mjs';
|
|
80
81
|
import { matchCandidates } from './bazaar/mock-tickup.mjs';
|
|
81
82
|
import { servePreviewFile, confineBuildDir } from './bazaar/preview-server.mjs';
|
|
@@ -182,6 +183,7 @@ export async function createServer(overrides = {}) {
|
|
|
182
183
|
// Refuse to start on a public bind with a forgeable default secret. (C1/C2)
|
|
183
184
|
assertSecureBinding(config);
|
|
184
185
|
if (!existsSync(config.dataDir)) mkdirSync(config.dataDir, { recursive: true });
|
|
186
|
+
ensureDataDirScaffold(config.dataDir); // self-ignore from the user's git (principle #1)
|
|
185
187
|
|
|
186
188
|
const activityBus = new ActivityBus();
|
|
187
189
|
// Live workspace presence (R-followup): tracks which member EMAILS are currently
|
|
@@ -1040,8 +1042,17 @@ export async function createServer(overrides = {}) {
|
|
|
1040
1042
|
name: w.name,
|
|
1041
1043
|
};
|
|
1042
1044
|
}
|
|
1045
|
+
// Every workspace's dataDir, once per process, gets the self-ignoring `.gitignore`
|
|
1046
|
+
// so a workspace opened from an existing git repo never leaks `.wild-workspace/`
|
|
1047
|
+
// into the user's commits (principle #1). Guarded by a Set → O(1) after first touch.
|
|
1048
|
+
const scaffoldedDataDirs = new Set();
|
|
1043
1049
|
function workspaceFor(c) {
|
|
1044
|
-
|
|
1050
|
+
const ws = c.get('workspace') || resolveWorkspace(c.req.header('x-workspace-id'));
|
|
1051
|
+
if (ws?.dataDir && !scaffoldedDataDirs.has(ws.dataDir)) {
|
|
1052
|
+
scaffoldedDataDirs.add(ws.dataDir);
|
|
1053
|
+
ensureDataDirScaffold(ws.dataDir);
|
|
1054
|
+
}
|
|
1055
|
+
return ws;
|
|
1045
1056
|
}
|
|
1046
1057
|
|
|
1047
1058
|
// Persistent audit trail for privileged actions (SECURITY.md S8). The [http]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Workspace dataDir scaffold — keeps wild-workspace's system state out of the user's
|
|
2
|
+
// git repo (CLAUDE.md principle #1: "never write junk into the user's synced
|
|
3
|
+
// workspace"). A workspace folder is frequently an existing git repo (the user opened
|
|
4
|
+
// it via "Open a folder with your work"); without this, the `.wild-workspace/` dir we
|
|
5
|
+
// create for sessions/canvas/settings/identity/resume-tokens shows up as untracked and
|
|
6
|
+
// a `git add .` would commit it.
|
|
7
|
+
//
|
|
8
|
+
// The fix is SELF-CONTAINED: we drop a `.gitignore` INSIDE `.wild-workspace/` that
|
|
9
|
+
// ignores its own contents (`*`), so git never sees the folder — WITHOUT touching the
|
|
10
|
+
// user's own root `.gitignore` (honors the "never touch the user's project files"
|
|
11
|
+
// rule the adopt flow follows). Idempotent + best-effort: a read-only fs must never
|
|
12
|
+
// break a turn, and we never overwrite an existing `.gitignore`.
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
export const SCAFFOLD_GITIGNORE =
|
|
18
|
+
'# wild-workspace system state — never commit into the user repo (CLAUDE.md principle #1)\n*\n';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ensure a workspace's `.wild-workspace/` dataDir exists and self-ignores from git.
|
|
22
|
+
* Safe to call often (cheap once the file exists). Returns true iff it wrote the file.
|
|
23
|
+
*/
|
|
24
|
+
export function ensureDataDirScaffold(dataDir) {
|
|
25
|
+
if (!dataDir || typeof dataDir !== 'string') return false;
|
|
26
|
+
try {
|
|
27
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
28
|
+
const gi = path.join(dataDir, '.gitignore');
|
|
29
|
+
if (fs.existsSync(gi)) return false;
|
|
30
|
+
fs.writeFileSync(gi, SCAFFOLD_GITIGNORE);
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false; // read-only fs / permissions — never throw into a request path
|
|
34
|
+
}
|
|
35
|
+
}
|