@logicseek/hello-cc 1.0.2 → 1.1.0-rc.1
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 +111 -23
- package/README.md +10 -1
- package/README.zh-CN.md +8 -1
- package/docs/commands.md +28 -4
- package/docs/commands.zh-CN.md +20 -4
- package/docs/dsh.md +2 -2
- package/docs/dsh.zh-CN.md +3 -3
- package/docs/native.md +84 -10
- package/docs/native.zh-CN.md +66 -5
- package/docs/web-handoff.zh-CN.md +11 -3
- package/lib/cli/app.mjs +42 -1
- package/lib/cli/commands/coordination.mjs +7 -2
- package/lib/cli/commands/dsh.mjs +1 -1
- package/lib/cli/commands/gc.mjs +9 -4
- package/lib/cli/commands/hook.mjs +6 -1
- package/lib/cli/commands/install.mjs +48 -6
- package/lib/cli/commands/lifecycle.mjs +14 -3
- package/lib/cli/commands/native.mjs +16 -9
- package/lib/cli/commands/query.mjs +15 -10
- package/lib/cli/commands/run.mjs +7 -6
- package/lib/core/coordination/results.mjs +27 -10
- package/lib/db/connection.mjs +135 -10
- package/lib/db/schema.mjs +59 -0
- package/lib/discover.mjs +4 -4
- package/lib/guidance.mjs +3 -2
- package/lib/integrations/codex-interactions.mjs +13 -11
- package/lib/integrations/dsh-collaboration.mjs +6 -12
- package/lib/integrations/dsh-cordis.mjs +17 -13
- package/lib/integrations/dsh.mjs +12 -9
- package/lib/integrations/hooks.mjs +132 -40
- package/lib/integrations/mcp-elicitation.mjs +116 -0
- package/lib/integrations/mcp-url-elicitation.mjs +25 -0
- package/lib/integrations/native/interactions.mjs +2 -1
- package/lib/integrations/peers/identity.mjs +42 -7
- package/lib/process/identity.mjs +15 -14
- package/lib/runtime/buffer-evidence.mjs +2 -1
- package/lib/runtime/native/service.mjs +11 -5
- package/lib/runtime/native/store.mjs +70 -18
- package/lib/runtime/paths.mjs +17 -2
- package/lib/runtime/private-file.mjs +76 -0
- package/lib/runtime/private-state.mjs +546 -0
- package/lib/runtime/project-context.mjs +2 -1
- package/lib/runtime/project-path.mjs +148 -12
- package/lib/runtime/project-trust.mjs +79 -0
- package/lib/runtime/projects.mjs +6 -3
- package/lib/runtime/state-migration.mjs +697 -0
- package/lib/runtime/state.mjs +22 -11
- package/lib/shared/automatic-result-key.mjs +6 -0
- package/lib/shared/json-file.mjs +55 -6
- package/lib/ui/help.mjs +6 -4
- package/lib/web/codex-app-server.mjs +26 -5
- package/lib/web/codex-sessions.mjs +5 -3
- package/lib/web/cookie-auth.mjs +1 -1
- package/lib/web/external-sessions.mjs +54 -5
- package/lib/web/http-routes.mjs +13 -9
- package/lib/web/http.mjs +20 -2
- package/lib/web/native-sessions.mjs +14 -7
- package/lib/web/peer-actions.mjs +27 -27
- package/lib/web/project-contexts.mjs +2 -3
- package/lib/web/runtime-main.mjs +22 -5
- package/lib/web/runtime.mjs +18 -4
- package/lib/web/session-serialize.mjs +1 -1
- package/lib/web/startup.mjs +115 -22
- package/lib/web/task-results.mjs +20 -17
- package/lib/web/terminal-assets.mjs +18 -0
- package/lib/web/tls.mjs +84 -33
- package/lib/web/tmux-sessions.mjs +2 -2
- package/lib/web/tmux-stream.mjs +3 -4
- package/lib/web/ui-codex.mjs +161 -32
- package/lib/web/ui-interactions.mjs +137 -4
- package/lib/web/ui-native.mjs +31 -13
- package/lib/web/ui-template.mjs +10 -3
- package/package.json +1 -1
- package/scripts/mcp-form-live-fixture.mjs +81 -0
- package/scripts/native-installed-acceptance.mjs +413 -0
- package/scripts/regression.mjs +46 -29
- package/scripts/web-mcp-form-acceptance.mjs +119 -0
- package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
- package/scripts/web-native-interaction-acceptance.mjs +77 -10
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
3
4
|
import { CliError } from '../shared/errors.mjs';
|
|
5
|
+
import { projectStateDir, legacyProjectStateDir } from './paths.mjs';
|
|
6
|
+
import { ensurePrivateProjectStateDir, inspectPrivateProjectAuthority } from './private-state.mjs';
|
|
7
|
+
import { unsafeDirectoryAcl } from './project-trust.mjs';
|
|
4
8
|
|
|
5
9
|
function pathIsWithin(parent, candidate) {
|
|
6
10
|
const relative = path.relative(parent, candidate);
|
|
@@ -22,8 +26,85 @@ function canonicalProjectRoot(root) {
|
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
function
|
|
26
|
-
|
|
29
|
+
function privateDirectory(directory, description, { tighten = true } = {}) {
|
|
30
|
+
let stat;
|
|
31
|
+
try { stat = fs.lstatSync(directory); }
|
|
32
|
+
catch { throw projectPathForbidden(`Cannot inspect ${description}: ${directory}`); }
|
|
33
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() ||
|
|
34
|
+
(typeof process.getuid === 'function' && stat.uid !== process.getuid())) {
|
|
35
|
+
throw projectPathForbidden(`${description} must be an owned real directory: ${directory}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let fd;
|
|
39
|
+
try {
|
|
40
|
+
fd = fs.openSync(directory, fs.constants.O_RDONLY |
|
|
41
|
+
(fs.constants.O_DIRECTORY || 0) | (fs.constants.O_NOFOLLOW || 0));
|
|
42
|
+
const opened = fs.fstatSync(fd);
|
|
43
|
+
if (!opened.isDirectory() || opened.dev !== stat.dev || opened.ino !== stat.ino ||
|
|
44
|
+
unsafeDirectoryAcl(directory, opened)) {
|
|
45
|
+
throw projectPathForbidden(`${description} changed during validation: ${directory}`);
|
|
46
|
+
}
|
|
47
|
+
if (process.platform !== 'win32' && (opened.mode & 0o777) !== 0o700) {
|
|
48
|
+
if (tighten) fs.fchmodSync(fd, 0o700);
|
|
49
|
+
else if ((opened.mode & 0o022) !== 0) {
|
|
50
|
+
throw projectPathForbidden(`${description} is writable by another user: ${directory}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const current = fs.lstatSync(directory);
|
|
54
|
+
if (current.isSymbolicLink() || current.dev !== stat.dev || current.ino !== stat.ino) {
|
|
55
|
+
throw projectPathForbidden(`${description} changed during validation: ${directory}`);
|
|
56
|
+
}
|
|
57
|
+
return fs.realpathSync(directory);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof CliError) throw error;
|
|
60
|
+
throw projectPathForbidden(`Cannot secure ${description}: ${directory}`);
|
|
61
|
+
} finally {
|
|
62
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ensureStateDirectory(root, createStateDir, secureExisting = createStateDir) {
|
|
67
|
+
const stateDir = projectStateDir(root);
|
|
68
|
+
if (stateDir !== legacyProjectStateDir(root)) {
|
|
69
|
+
const authority = inspectPrivateProjectAuthority(root);
|
|
70
|
+
let privateEntryExists = false;
|
|
71
|
+
try { fs.lstatSync(stateDir); privateEntryExists = true; }
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error?.code !== 'ENOENT') {
|
|
74
|
+
throw projectPathForbidden(`Cannot inspect private project state: ${stateDir}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// A crash can leave a pending marker with an empty directory or a valid
|
|
78
|
+
// manifest not yet promoted to fresh. Older private stores may have no
|
|
79
|
+
// sibling marker. Finish either binding before returning any existing
|
|
80
|
+
// state, including during a read-only request.
|
|
81
|
+
const finishBinding = privateEntryExists &&
|
|
82
|
+
(authority === null || authority === 'pending');
|
|
83
|
+
const existing = ensurePrivateProjectStateDir(root, { create: finishBinding });
|
|
84
|
+
if (existing) return { stateDir, exists: true };
|
|
85
|
+
if (authority === 'fresh' || authority === 'migrated') {
|
|
86
|
+
throw new CliError('STATE_AUTHORITY_MISSING',
|
|
87
|
+
`Previously established private project state is missing: ${stateDir}. Restore it from backup; ` +
|
|
88
|
+
'if the legacy source still exists, stop all old writers and run ' +
|
|
89
|
+
'hcc --root DIR migrate-state --offline --yes; otherwise run ' +
|
|
90
|
+
'hcc --root DIR uninstall --purge --yes to explicitly reset the missing state.');
|
|
91
|
+
}
|
|
92
|
+
const legacy = legacyProjectStateDir(root);
|
|
93
|
+
let legacyStat = null;
|
|
94
|
+
try { legacyStat = fs.lstatSync(legacy); }
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error?.code !== 'ENOENT') throw projectPathForbidden(`Cannot inspect legacy project state: ${legacy}`);
|
|
97
|
+
}
|
|
98
|
+
if (legacyStat && (legacyStat.isSymbolicLink() || !legacyStat.isDirectory())) {
|
|
99
|
+
throw projectPathForbidden(`Legacy project state is not a real directory: ${legacy}`);
|
|
100
|
+
}
|
|
101
|
+
if (legacyStat) {
|
|
102
|
+
throw new CliError('STATE_MIGRATION_REQUIRED',
|
|
103
|
+
`Legacy project state requires an offline migration before use: ${legacy}`);
|
|
104
|
+
}
|
|
105
|
+
const secured = createStateDir ? ensurePrivateProjectStateDir(root, { create: true }) : null;
|
|
106
|
+
return { stateDir, exists: Boolean(secured) };
|
|
107
|
+
}
|
|
27
108
|
let stateStat;
|
|
28
109
|
try {
|
|
29
110
|
stateStat = fs.lstatSync(stateDir);
|
|
@@ -35,8 +116,9 @@ function ensureStateDirectory(root, createStateDir) {
|
|
|
35
116
|
try {
|
|
36
117
|
fs.mkdirSync(stateDir, { mode: 0o700 });
|
|
37
118
|
stateStat = fs.lstatSync(stateDir);
|
|
38
|
-
} catch {
|
|
39
|
-
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error?.code === 'EEXIST') stateStat = fs.lstatSync(stateDir);
|
|
121
|
+
else throw projectPathForbidden(`Cannot create project state directory: ${stateDir}`);
|
|
40
122
|
}
|
|
41
123
|
}
|
|
42
124
|
|
|
@@ -44,7 +126,7 @@ function ensureStateDirectory(root, createStateDir) {
|
|
|
44
126
|
throw projectPathForbidden(`Project state directory must be a real directory: ${stateDir}`);
|
|
45
127
|
}
|
|
46
128
|
try {
|
|
47
|
-
const canonical =
|
|
129
|
+
const canonical = privateDirectory(stateDir, 'Project state directory', { tighten: secureExisting });
|
|
48
130
|
if (canonical !== stateDir || !pathIsWithin(root, canonical)) {
|
|
49
131
|
throw projectPathForbidden(`Project state directory escapes its project root: ${stateDir}`);
|
|
50
132
|
}
|
|
@@ -55,13 +137,26 @@ function ensureStateDirectory(root, createStateDir) {
|
|
|
55
137
|
}
|
|
56
138
|
}
|
|
57
139
|
|
|
58
|
-
function
|
|
140
|
+
export function secureExistingProjectStateDirectory(root) {
|
|
141
|
+
const canonicalRoot = canonicalProjectRoot(root);
|
|
142
|
+
return ensureStateDirectory(canonicalRoot, false, true);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function validateDatabaseParent(stateDir, db, createParents = false) {
|
|
59
146
|
const parent = path.dirname(db);
|
|
60
147
|
const relative = path.relative(stateDir, parent);
|
|
61
148
|
const segments = relative === '' ? [] : relative.split(path.sep);
|
|
62
149
|
let current = stateDir;
|
|
63
150
|
for (const segment of segments) {
|
|
64
151
|
current = path.join(current, segment);
|
|
152
|
+
if (createParents) {
|
|
153
|
+
try { fs.mkdirSync(current, { mode: 0o700 }); }
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error?.code !== 'EEXIST') {
|
|
156
|
+
throw projectPathForbidden(`Cannot create database parent directory: ${current}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
65
160
|
let stat;
|
|
66
161
|
try {
|
|
67
162
|
stat = fs.lstatSync(current);
|
|
@@ -71,6 +166,9 @@ function validateDatabaseParent(stateDir, db) {
|
|
|
71
166
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
72
167
|
throw projectPathForbidden(`Database parent must contain only real directories: ${current}`);
|
|
73
168
|
}
|
|
169
|
+
if (privateDirectory(current, 'Database parent directory', { tighten: createParents }) !== current) {
|
|
170
|
+
throw projectPathForbidden(`Database parent directory changed: ${current}`);
|
|
171
|
+
}
|
|
74
172
|
}
|
|
75
173
|
|
|
76
174
|
try {
|
|
@@ -107,14 +205,18 @@ function validateDatabaseTarget(stateDir, db) {
|
|
|
107
205
|
}
|
|
108
206
|
}
|
|
109
207
|
|
|
110
|
-
export function resolveProjectDatabase({ root, db, createStateDir = false }) {
|
|
208
|
+
export function resolveProjectDatabase({ root, db, createStateDir = false, createDatabaseParents = false }) {
|
|
111
209
|
const requestedRoot = path.resolve(String(root || ''));
|
|
112
210
|
const canonicalRoot = canonicalProjectRoot(requestedRoot);
|
|
113
211
|
const rawDb = path.resolve(String(db || path.join(requestedRoot, '.hello-cc', 'mesh.db')));
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
212
|
+
const intendedStateDir = projectStateDir(canonicalRoot);
|
|
213
|
+
const requestedLegacyState = legacyProjectStateDir(requestedRoot);
|
|
214
|
+
const canonicalLegacyState = legacyProjectStateDir(canonicalRoot);
|
|
215
|
+
let requestedDb = pathIsWithin(requestedLegacyState, rawDb)
|
|
216
|
+
? path.join(intendedStateDir, path.relative(requestedLegacyState, rawDb))
|
|
217
|
+
: pathIsWithin(canonicalLegacyState, rawDb)
|
|
218
|
+
? path.join(intendedStateDir, path.relative(canonicalLegacyState, rawDb))
|
|
219
|
+
: rawDb;
|
|
118
220
|
if (!pathIsWithin(intendedStateDir, requestedDb)) {
|
|
119
221
|
try {
|
|
120
222
|
const canonicalParent = fs.realpathSync(path.dirname(requestedDb));
|
|
@@ -130,10 +232,44 @@ export function resolveProjectDatabase({ root, db, createStateDir = false }) {
|
|
|
130
232
|
return { root: canonicalRoot, stateDir: state.stateDir, db: requestedDb };
|
|
131
233
|
}
|
|
132
234
|
|
|
133
|
-
validateDatabaseParent(state.stateDir, requestedDb);
|
|
235
|
+
validateDatabaseParent(state.stateDir, requestedDb, Boolean(createDatabaseParents));
|
|
134
236
|
return {
|
|
135
237
|
root: canonicalRoot,
|
|
136
238
|
stateDir: state.stateDir,
|
|
137
239
|
db: validateDatabaseTarget(state.stateDir, requestedDb)
|
|
138
240
|
};
|
|
139
241
|
}
|
|
242
|
+
|
|
243
|
+
export function ensurePrivateProjectStateSubdirectory(root, directoryName, { create = true } = {}) {
|
|
244
|
+
if (!directoryName || directoryName !== path.basename(directoryName) ||
|
|
245
|
+
directoryName === '.' || directoryName === '..') {
|
|
246
|
+
throw projectPathForbidden(`Invalid buffer directory name: ${directoryName}`);
|
|
247
|
+
}
|
|
248
|
+
const canonicalRoot = canonicalProjectRoot(root);
|
|
249
|
+
const state = ensureStateDirectory(canonicalRoot, create, create);
|
|
250
|
+
if (!state.exists) return null;
|
|
251
|
+
const { stateDir } = state;
|
|
252
|
+
const directory = path.join(stateDir, directoryName);
|
|
253
|
+
if (create) {
|
|
254
|
+
try { fs.mkdirSync(directory, { mode: 0o700 }); }
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (error?.code !== 'EEXIST') {
|
|
257
|
+
throw projectPathForbidden(`Cannot create buffer directory: ${directory}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
try { fs.lstatSync(directory); }
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (error?.code === 'ENOENT') return null;
|
|
264
|
+
throw projectPathForbidden(`Cannot inspect buffer directory: ${directory}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (privateDirectory(directory, 'Project state subdirectory', { tighten: create }) !== directory) {
|
|
268
|
+
throw projectPathForbidden(`Buffer directory escapes project state directory: ${directory}`);
|
|
269
|
+
}
|
|
270
|
+
return directory;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function ensurePrivateProjectBufferDirectory(root, directoryName = 'bufs', options = {}) {
|
|
274
|
+
return ensurePrivateProjectStateSubdirectory(root, directoryName, options);
|
|
275
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
// A private .hello-cc is only safe behind a pathname that another UID cannot
|
|
7
|
+
// rename. The ownership of the leaf alone is not sufficient: a writable
|
|
8
|
+
// non-sticky ancestor lets another UID replace its directory entry.
|
|
9
|
+
const aclCache = new Map();
|
|
10
|
+
|
|
11
|
+
export function unsafeDirectoryAcl(directory, stat = null) {
|
|
12
|
+
let identity = stat;
|
|
13
|
+
try { identity ||= fs.lstatSync(directory); }
|
|
14
|
+
catch { return true; }
|
|
15
|
+
const signature = `${identity.dev}:${identity.ino}:${identity.ctimeNs ?? identity.ctimeMs}`;
|
|
16
|
+
const cached = aclCache.get(directory);
|
|
17
|
+
if (cached?.signature === signature) return cached.unsafe;
|
|
18
|
+
const ls = fs.existsSync('/bin/ls') ? '/bin/ls'
|
|
19
|
+
: fs.existsSync('/usr/bin/ls') ? '/usr/bin/ls' : null;
|
|
20
|
+
if (!ls) return true;
|
|
21
|
+
const result = spawnSync(ls, process.platform === 'darwin'
|
|
22
|
+
? ['-lde', directory] : ['-ld', directory], {
|
|
23
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000,
|
|
24
|
+
env: { ...process.env, LC_ALL: 'C' }
|
|
25
|
+
});
|
|
26
|
+
if (result.status !== 0) return true;
|
|
27
|
+
const [first, ...entries] = result.stdout.trim().split('\n');
|
|
28
|
+
const permissions = first?.trim().split(/\s+/, 1)[0];
|
|
29
|
+
if (!/^[-dlcbps][rwxStTs-]{9}[+@.]?$/.test(permissions)) return true;
|
|
30
|
+
// A deny-only macOS ACL cannot grant another UID the ability to rebind a
|
|
31
|
+
// directory entry. Unknown or allow ACL entries use the private fallback.
|
|
32
|
+
const aclEntries = entries.filter((entry) => entry.trim().length > 0);
|
|
33
|
+
const unsafe = process.platform === 'darwin'
|
|
34
|
+
? (aclEntries.length > 0
|
|
35
|
+
? aclEntries.some((entry) => !/^\s*\d+:\s+.+\sdeny\s+/.test(entry))
|
|
36
|
+
: permissions[10] === '+')
|
|
37
|
+
: permissions[10] === '+';
|
|
38
|
+
if (aclCache.size >= 1024) aclCache.clear();
|
|
39
|
+
aclCache.set(directory, { signature, unsafe });
|
|
40
|
+
return unsafe;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function stableProjectStateRoot(root) {
|
|
44
|
+
if (typeof process.getuid !== 'function') return null;
|
|
45
|
+
const uid = process.getuid();
|
|
46
|
+
let canonical;
|
|
47
|
+
try { canonical = fs.realpathSync.native(path.resolve(root)); }
|
|
48
|
+
catch { return null; }
|
|
49
|
+
const parts = [];
|
|
50
|
+
for (let current = canonical;; current = path.dirname(current)) {
|
|
51
|
+
parts.push(current);
|
|
52
|
+
if (path.dirname(current) === current) break;
|
|
53
|
+
}
|
|
54
|
+
parts.reverse();
|
|
55
|
+
let parent = null;
|
|
56
|
+
for (const component of parts) {
|
|
57
|
+
let stat;
|
|
58
|
+
try { stat = fs.lstatSync(component); }
|
|
59
|
+
catch { return null; }
|
|
60
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() ||
|
|
61
|
+
(stat.uid !== uid && stat.uid !== 0) || unsafeDirectoryAcl(component, stat)) return null;
|
|
62
|
+
if (parent) {
|
|
63
|
+
const parentMode = parent.mode & 0o7777;
|
|
64
|
+
if ((parentMode & 0o022) !== 0 &&
|
|
65
|
+
((parentMode & 0o1000) === 0 || (stat.uid !== uid && stat.uid !== 0))) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
parent = stat;
|
|
70
|
+
}
|
|
71
|
+
// A cross-UID-writable root can have its .hello-cc entry replaced. Keep its
|
|
72
|
+
// state under the user's private state directory even when root is sticky.
|
|
73
|
+
if ((parent.mode & 0o022) !== 0) return null;
|
|
74
|
+
// A stable but read-only server directory (for example /etc) cannot host a
|
|
75
|
+
// new project-local state directory. It remains selectable via private state.
|
|
76
|
+
try { fs.accessSync(canonical, fs.constants.W_OK | fs.constants.X_OK); }
|
|
77
|
+
catch { return null; }
|
|
78
|
+
return canonical;
|
|
79
|
+
}
|
package/lib/runtime/projects.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { projectDbPath, projectRegistryPath } from './paths.mjs';
|
|
3
|
+
import { projectDbPath, projectRegistryPath, projectStateDir } from './paths.mjs';
|
|
4
4
|
import { withFileLock } from '../shared/file-lock.mjs';
|
|
5
5
|
import { writeJsonSafe } from '../shared/json-file.mjs';
|
|
6
6
|
|
|
@@ -101,9 +101,12 @@ function canonicalProjectDb(value, rawRoot, rootInfo, { strict = false } = {}) {
|
|
|
101
101
|
const db = pathWithin(rawRoot, rawDb)
|
|
102
102
|
? path.resolve(rootInfo.root, relativeToRawRoot)
|
|
103
103
|
: rawDb;
|
|
104
|
+
const privateState = projectStateDir(rootInfo.root);
|
|
105
|
+
const allowed = (candidate) => pathWithin(rootInfo.root, candidate) ||
|
|
106
|
+
(privateState !== path.join(rootInfo.root, '.hello-cc') && pathWithin(privateState, candidate));
|
|
104
107
|
|
|
105
108
|
if (!rootInfo.exists) {
|
|
106
|
-
return
|
|
109
|
+
return allowed(db)
|
|
107
110
|
? db
|
|
108
111
|
: reject(`project DB is outside the project root: ${rawDb}`);
|
|
109
112
|
}
|
|
@@ -145,7 +148,7 @@ function canonicalProjectDb(value, rawRoot, rootInfo, { strict = false } = {}) {
|
|
|
145
148
|
const canonicalDb = dbExists
|
|
146
149
|
? canonicalAncestor
|
|
147
150
|
: path.resolve(canonicalAncestor, path.relative(ancestor, db));
|
|
148
|
-
if (!
|
|
151
|
+
if (!allowed(canonicalDb)) {
|
|
149
152
|
return reject(`project DB is outside the project root: ${rawDb}`);
|
|
150
153
|
}
|
|
151
154
|
return canonicalDb;
|