@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,32 +1,70 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
3
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
4
5
|
import { randomBytes } from 'node:crypto';
|
|
5
6
|
import { CliError } from '../../shared/errors.mjs';
|
|
7
|
+
import { resolveProjectDatabase, ensurePrivateProjectStateSubdirectory } from '../project-path.mjs';
|
|
8
|
+
import { unsafeDirectoryAcl } from '../project-trust.mjs';
|
|
9
|
+
import { readPrivateTextFile } from '../private-file.mjs';
|
|
6
10
|
|
|
7
11
|
function statOrMissing(file) {
|
|
8
12
|
try { return fs.lstatSync(file); }
|
|
9
13
|
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
function
|
|
16
|
+
function privateDatabase(file) {
|
|
13
17
|
let stat = statOrMissing(file);
|
|
14
|
-
if (!stat
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
if (!stat) {
|
|
19
|
+
try {
|
|
20
|
+
const fd = fs.openSync(file, fs.constants.O_RDWR | fs.constants.O_CREAT |
|
|
21
|
+
fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
22
|
+
fs.closeSync(fd);
|
|
23
|
+
} catch (error) { if (error?.code !== 'EEXIST') throw error; }
|
|
24
|
+
stat = fs.lstatSync(file);
|
|
25
|
+
}
|
|
26
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 ||
|
|
27
|
+
(typeof process.getuid === 'function' && stat.uid !== process.getuid()) ||
|
|
28
|
+
(process.platform !== 'win32' && (stat.mode & 0o022) !== 0) ||
|
|
29
|
+
unsafeDirectoryAcl(file, stat)) {
|
|
30
|
+
throw new CliError('NATIVE_STATE_UNSAFE', `Native database must be an owned regular file: ${file}`);
|
|
31
|
+
}
|
|
32
|
+
const fd = fs.openSync(file, fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0));
|
|
33
|
+
try {
|
|
34
|
+
const opened = fs.fstatSync(fd);
|
|
35
|
+
if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino ||
|
|
36
|
+
(process.platform !== 'win32' && (opened.mode & 0o022) !== 0) ||
|
|
37
|
+
unsafeDirectoryAcl(file, opened)) {
|
|
38
|
+
throw new CliError('NATIVE_STATE_UNSAFE', `Native database changed during validation: ${file}`);
|
|
39
|
+
}
|
|
40
|
+
if (process.platform !== 'win32' && (opened.mode & 0o777) !== 0o600) fs.fchmodSync(fd, 0o600);
|
|
41
|
+
const current = fs.lstatSync(file);
|
|
42
|
+
if (current.isSymbolicLink() || current.dev !== stat.dev || current.ino !== stat.ino) {
|
|
43
|
+
throw new CliError('NATIVE_STATE_UNSAFE', `Native database changed during validation: ${file}`);
|
|
44
|
+
}
|
|
45
|
+
} finally { fs.closeSync(fd); }
|
|
18
46
|
}
|
|
19
47
|
|
|
20
48
|
export function nativePaths(ctx, { create = false } = {}) {
|
|
21
49
|
const root = fs.realpathSync(ctx.root);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
let dir;
|
|
51
|
+
try {
|
|
52
|
+
resolveProjectDatabase({ root, createStateDir: create });
|
|
53
|
+
dir = ensurePrivateProjectStateSubdirectory(root, 'native', { create });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error?.code === 'PROJECT_PATH_FORBIDDEN') {
|
|
56
|
+
throw new CliError('NATIVE_STATE_UNSAFE', error.message);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (!dir) return null;
|
|
26
61
|
for (const name of ['runtime.json', 'state.db', 'state.db-wal', 'state.db-shm', 'runtime.log']) {
|
|
27
62
|
const file = path.join(dir, name);
|
|
28
63
|
const stat = statOrMissing(file);
|
|
29
|
-
if (stat && (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1
|
|
64
|
+
if (stat && (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1 ||
|
|
65
|
+
(typeof process.getuid === 'function' && stat.uid !== process.getuid()) ||
|
|
66
|
+
(process.platform !== 'win32' && (stat.mode & 0o022) !== 0) ||
|
|
67
|
+
unsafeDirectoryAcl(file, stat))) {
|
|
30
68
|
throw new CliError('NATIVE_STATE_UNSAFE', `Native state file is not a regular file: ${file}`);
|
|
31
69
|
}
|
|
32
70
|
}
|
|
@@ -43,10 +81,18 @@ export function writeNativePointer(ctx, value) {
|
|
|
43
81
|
|
|
44
82
|
export function readNativePointer(ctx) {
|
|
45
83
|
const paths = nativePaths(ctx);
|
|
46
|
-
if (!paths
|
|
47
|
-
|
|
84
|
+
if (!paths) return null;
|
|
85
|
+
let raw;
|
|
86
|
+
try { raw = readPrivateTextFile(paths.pointer, { maxBytes: 16384 }); }
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error?.code === 'PROJECT_PATH_FORBIDDEN') {
|
|
89
|
+
throw new CliError('NATIVE_STATE_UNSAFE', error.message);
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
if (raw === null) return null;
|
|
48
94
|
let value;
|
|
49
|
-
try { value = JSON.parse(
|
|
95
|
+
try { value = JSON.parse(raw); }
|
|
50
96
|
catch { throw new CliError('NATIVE_STATE_INVALID', 'Native runtime pointer is invalid'); }
|
|
51
97
|
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
52
98
|
value.root !== fs.realpathSync(ctx.root) || value.meshDb !== path.resolve(ctx.dbPath) ||
|
|
@@ -60,8 +106,8 @@ export function readNativePointer(ctx) {
|
|
|
60
106
|
|
|
61
107
|
export function createNativeStore(ctx) {
|
|
62
108
|
const paths = nativePaths(ctx, { create: true });
|
|
109
|
+
privateDatabase(paths.db);
|
|
63
110
|
const db = new DatabaseSync(paths.db, { timeout: 5000 });
|
|
64
|
-
fs.chmodSync(paths.db, 0o600);
|
|
65
111
|
db.exec(`PRAGMA journal_mode = WAL;
|
|
66
112
|
PRAGMA busy_timeout = 5000;
|
|
67
113
|
PRAGMA foreign_keys = ON;
|
|
@@ -72,11 +118,16 @@ export function createNativeStore(ctx) {
|
|
|
72
118
|
CREATE TABLE IF NOT EXISTS deliveries (
|
|
73
119
|
id INTEGER PRIMARY KEY, peer TEXT NOT NULL, message_id INTEGER NOT NULL,
|
|
74
120
|
submission_id TEXT NOT NULL UNIQUE, state TEXT NOT NULL, turn_id TEXT,
|
|
75
|
-
detail TEXT, updated_at INTEGER NOT NULL, UNIQUE(peer, message_id)
|
|
121
|
+
detail TEXT, origin TEXT NOT NULL DEFAULT 'peer', updated_at INTEGER NOT NULL, UNIQUE(peer, message_id)
|
|
76
122
|
);
|
|
77
123
|
CREATE TABLE IF NOT EXISTS provider_events (
|
|
78
124
|
id INTEGER PRIMARY KEY, peer TEXT NOT NULL, payload TEXT NOT NULL, created_at INTEGER NOT NULL
|
|
79
125
|
);`);
|
|
126
|
+
// Existing deliveries came from the shared message bus. Migration must not
|
|
127
|
+
// promote a stored sender name into user authority.
|
|
128
|
+
if (!db.prepare('PRAGMA table_info(deliveries)').all().some(column => column.name === 'origin')) {
|
|
129
|
+
db.exec("ALTER TABLE deliveries ADD COLUMN origin TEXT NOT NULL DEFAULT 'peer'");
|
|
130
|
+
}
|
|
80
131
|
const now = () => Date.now();
|
|
81
132
|
return {
|
|
82
133
|
db,
|
|
@@ -88,9 +139,10 @@ export function createNativeStore(ctx) {
|
|
|
88
139
|
cwd=excluded.cwd, status=excluded.status, updated_at=excluded.updated_at`)
|
|
89
140
|
.run(worker.peer, worker.provider, worker.sessionId || null, worker.cwd, worker.status, now());
|
|
90
141
|
},
|
|
91
|
-
queue(peer, messageId, submissionId) {
|
|
92
|
-
|
|
93
|
-
|
|
142
|
+
queue(peer, messageId, submissionId, origin = 'peer') {
|
|
143
|
+
if (!['peer', 'user'].includes(origin)) throw new CliError('BAD_ARGS', 'Unknown native message origin');
|
|
144
|
+
db.prepare(`INSERT OR IGNORE INTO deliveries(peer,message_id,submission_id,state,origin,updated_at) VALUES (?,?,?,'queued',?,?)`)
|
|
145
|
+
.run(peer, messageId, submissionId, origin, now());
|
|
94
146
|
return this.delivery(peer, messageId);
|
|
95
147
|
},
|
|
96
148
|
delivery(peer, messageId) { return db.prepare('SELECT * FROM deliveries WHERE peer=? AND message_id=?').get(peer, messageId) || null; },
|
package/lib/runtime/paths.mjs
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { privateProjectStateDir, privateProjectAuthorityPath, validatedGlobalStateDir } from './private-state.mjs';
|
|
4
|
+
import { stableProjectStateRoot } from './project-trust.mjs';
|
|
3
5
|
|
|
4
6
|
export function projectStateDir(root) {
|
|
7
|
+
const privateDir = privateProjectStateDir(root);
|
|
8
|
+
// An established private binding is authoritative even if permissions on
|
|
9
|
+
// the project path later change. A damaged or unreadable private store must
|
|
10
|
+
// fail validation, never silently reactivate an old project-local database.
|
|
11
|
+
try { fs.lstatSync(privateDir); return privateDir; }
|
|
12
|
+
catch (error) { if (error?.code !== 'ENOENT') return privateDir; }
|
|
13
|
+
try { fs.lstatSync(privateProjectAuthorityPath(root)); return privateDir; }
|
|
14
|
+
catch (error) { if (error?.code !== 'ENOENT') return privateDir; }
|
|
15
|
+
const stableRoot = stableProjectStateRoot(root);
|
|
16
|
+
return stableRoot ? path.join(stableRoot, '.hello-cc') : privateDir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function legacyProjectStateDir(root) {
|
|
5
20
|
return path.join(root, '.hello-cc');
|
|
6
21
|
}
|
|
7
22
|
|
|
@@ -18,7 +33,7 @@ export function webLogPath(ctx) {
|
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
export function globalStateDir() {
|
|
21
|
-
return
|
|
36
|
+
return validatedGlobalStateDir();
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
export function globalRuntimePath() {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { CliError } from '../shared/errors.mjs';
|
|
4
|
+
import { unsafeDirectoryAcl } from './project-trust.mjs';
|
|
5
|
+
|
|
6
|
+
function forbidden(file) {
|
|
7
|
+
return new CliError('PROJECT_PATH_FORBIDDEN', `Managed file must be an owned private regular file: ${file}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function openPrivateAppendFile(file) {
|
|
11
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT |
|
|
12
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
13
|
+
let fd;
|
|
14
|
+
try {
|
|
15
|
+
fd = fs.openSync(file, flags, 0o600);
|
|
16
|
+
const opened = fs.fstatSync(fd);
|
|
17
|
+
if (!opened.isFile() || opened.nlink !== 1 ||
|
|
18
|
+
(typeof process.getuid === 'function' && opened.uid !== process.getuid()) ||
|
|
19
|
+
(process.platform !== 'win32' && (opened.mode & 0o777) !== 0o600) ||
|
|
20
|
+
unsafeDirectoryAcl(file, opened)) {
|
|
21
|
+
throw forbidden(file);
|
|
22
|
+
}
|
|
23
|
+
const current = fs.lstatSync(file);
|
|
24
|
+
const secured = fs.fstatSync(fd);
|
|
25
|
+
if (current.isSymbolicLink() || !current.isFile() || current.nlink !== 1 ||
|
|
26
|
+
current.dev !== secured.dev || current.ino !== secured.ino ||
|
|
27
|
+
(typeof process.getuid === 'function' && current.uid !== process.getuid()) ||
|
|
28
|
+
(process.platform !== 'win32' && (secured.mode & 0o777) !== 0o600)) {
|
|
29
|
+
throw forbidden(file);
|
|
30
|
+
}
|
|
31
|
+
return fd;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
34
|
+
if (error instanceof CliError) throw error;
|
|
35
|
+
throw forbidden(file);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readPrivateTextFile(file, { maxBytes = 65536 } = {}) {
|
|
40
|
+
let before;
|
|
41
|
+
try { before = fs.lstatSync(file); }
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error?.code === 'ENOENT') return null;
|
|
44
|
+
throw forbidden(file);
|
|
45
|
+
}
|
|
46
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1 ||
|
|
47
|
+
(typeof process.getuid === 'function' && before.uid !== process.getuid()) ||
|
|
48
|
+
(process.platform !== 'win32' && (before.mode & 0o777) !== 0o600) ||
|
|
49
|
+
before.size > maxBytes || unsafeDirectoryAcl(file, before)) {
|
|
50
|
+
throw forbidden(file);
|
|
51
|
+
}
|
|
52
|
+
let fd;
|
|
53
|
+
try {
|
|
54
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
55
|
+
const opened = fs.fstatSync(fd);
|
|
56
|
+
if (!opened.isFile() || opened.nlink !== 1 || opened.dev !== before.dev ||
|
|
57
|
+
opened.ino !== before.ino || opened.size > maxBytes ||
|
|
58
|
+
(typeof process.getuid === 'function' && opened.uid !== process.getuid()) ||
|
|
59
|
+
(process.platform !== 'win32' && (opened.mode & 0o777) !== 0o600)) {
|
|
60
|
+
throw forbidden(file);
|
|
61
|
+
}
|
|
62
|
+
const content = fs.readFileSync(fd, 'utf8');
|
|
63
|
+
const after = fs.lstatSync(file);
|
|
64
|
+
const current = fs.fstatSync(fd);
|
|
65
|
+
if (after.isSymbolicLink() || after.dev !== opened.dev || after.ino !== opened.ino ||
|
|
66
|
+
current.size !== opened.size || current.mtimeMs !== opened.mtimeMs) {
|
|
67
|
+
throw forbidden(file);
|
|
68
|
+
}
|
|
69
|
+
return content;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof CliError) throw error;
|
|
72
|
+
throw forbidden(file);
|
|
73
|
+
} finally {
|
|
74
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
75
|
+
}
|
|
76
|
+
}
|