@logicseek/hello-cc 1.0.5 → 1.1.0-rc.3
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 +72 -0
- package/docs/codex-account.zh-CN.md +38 -0
- package/docs/codex-protocol.md +59 -0
- package/docs/commands.md +9 -0
- package/docs/commands.zh-CN.md +7 -0
- package/docs/macos-process-identity.md +2 -0
- package/docs/native.md +179 -14
- package/docs/native.zh-CN.md +136 -6
- package/docs/web-handoff.zh-CN.md +118 -8
- package/lib/cli/app.mjs +47 -4
- package/lib/cli/commands/coordination.mjs +14 -4
- package/lib/cli/commands/dsh.mjs +1 -1
- package/lib/cli/commands/gc.mjs +22 -15
- package/lib/cli/commands/hook.mjs +19 -3
- package/lib/cli/commands/install.mjs +80 -10
- package/lib/cli/commands/misc.mjs +36 -9
- package/lib/cli/commands/native.mjs +4 -38
- package/lib/cli/commands/peer.mjs +3 -0
- package/lib/cli/commands/run.mjs +13 -6
- package/lib/cli/commands/tmux.mjs +1 -2
- package/lib/cli-runtime.mjs +19 -2
- package/lib/core/coordination/results.mjs +27 -10
- package/lib/core/sessions/hook-root-identity.mjs +47 -0
- package/lib/core/sessions/launch.mjs +7 -1
- package/lib/db/connection.mjs +140 -42
- package/lib/db/schema.mjs +59 -0
- package/lib/diagnostics/codex-protocol.mjs +105 -0
- package/lib/guidance.mjs +21 -36
- package/lib/integrations/codex-account.mjs +143 -0
- 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 +16 -11
- package/lib/integrations/mcp-elicitation.mjs +116 -0
- package/lib/integrations/mcp-url-elicitation.mjs +25 -0
- package/lib/integrations/native/claude.mjs +24 -6
- package/lib/integrations/native/codex.mjs +38 -7
- package/lib/integrations/native/dsh-acp.mjs +67 -24
- package/lib/integrations/native/index.mjs +4 -1
- package/lib/integrations/native/interactions.mjs +2 -1
- package/lib/integrations/native/jsonrpc.mjs +37 -5
- package/lib/integrations/native/telemetry.mjs +30 -0
- package/lib/process/identity.mjs +4 -0
- package/lib/process/pinned-cwd.mjs +302 -0
- package/lib/process/pinned-guidance.mjs +138 -0
- package/lib/process/pinned-project-state.mjs +105 -0
- package/lib/process/selected-cwd-identity.mjs +95 -0
- package/lib/runtime/buffer-evidence.mjs +2 -1
- package/lib/runtime/client.mjs +9 -0
- package/lib/runtime/native/client.mjs +4 -1
- package/lib/runtime/native/history.mjs +140 -0
- package/lib/runtime/native/launcher.mjs +106 -0
- package/lib/runtime/native/service.mjs +64 -8
- package/lib/runtime/native/store.mjs +83 -58
- 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 +92 -20
- package/lib/runtime/project-trust.mjs +79 -0
- package/lib/runtime/projects.mjs +9 -3
- package/lib/runtime/state-migration.mjs +697 -0
- package/lib/runtime/state.mjs +69 -27
- package/lib/shared/automatic-result-key.mjs +6 -0
- package/lib/shared/json-file.mjs +55 -6
- package/lib/shared/redact.mjs +2 -1
- package/lib/ui/help.mjs +6 -4
- package/lib/web/agent-defaults.mjs +110 -0
- package/lib/web/auto-attach.mjs +3 -2
- package/lib/web/browser/contracts.d.ts +10 -0
- package/lib/web/browser/core.mjs +1997 -0
- package/lib/web/browser/project-data.mjs +158 -0
- package/lib/web/browser/session-sync.mjs +82 -0
- package/lib/web/browser-assets.mjs +19 -0
- package/lib/web/buffer-gc-runtime.mjs +2 -2
- package/lib/web/codex-app-server.mjs +111 -37
- package/lib/web/codex-sessions.mjs +61 -12
- package/lib/web/context-files.mjs +82 -0
- package/lib/web/cookie-auth.mjs +51 -3
- package/lib/web/external-sessions.mjs +76 -9
- package/lib/web/http-routes.mjs +238 -25
- package/lib/web/http.mjs +50 -4
- package/lib/web/liveness-reaper.mjs +2 -2
- package/lib/web/native-sessions.mjs +323 -14
- package/lib/web/project-contexts.mjs +236 -44
- package/lib/web/project-file-write.mjs +249 -0
- package/lib/web/project-files.mjs +268 -0
- package/lib/web/pty-sessions.mjs +11 -3
- package/lib/web/runtime-main.mjs +132 -80
- package/lib/web/session-serialize.mjs +87 -8
- package/lib/web/session-state-sync.mjs +98 -0
- package/lib/web/shutdown-diagnostics.mjs +38 -0
- package/lib/web/shutdown-response.mjs +21 -0
- package/lib/web/shutdown-state.mjs +91 -0
- package/lib/web/startup.mjs +103 -35
- package/lib/web/task-results.mjs +19 -19
- package/lib/web/tls.mjs +84 -33
- package/lib/web/tmux-sessions.mjs +73 -9
- package/lib/web/tmux-stream.mjs +36 -8
- package/lib/web/ui-agent-defaults.mjs +149 -0
- package/lib/web/ui-agent-start.mjs +330 -0
- package/lib/web/ui-codex-account.mjs +81 -0
- package/lib/web/ui-codex.mjs +105 -85
- package/lib/web/ui-command-palette.mjs +21 -5
- package/lib/web/ui-file-writes.mjs +220 -0
- package/lib/web/ui-files.mjs +231 -0
- package/lib/web/ui-handoff.mjs +10 -2
- package/lib/web/ui-history.mjs +175 -105
- package/lib/web/ui-i18n.mjs +280 -8
- package/lib/web/ui-interactions.mjs +107 -6
- package/lib/web/ui-native-timeline.mjs +326 -0
- package/lib/web/ui-native.mjs +93 -43
- package/lib/web/ui-review.mjs +5 -3
- package/lib/web/ui-safe-markdown.mjs +79 -0
- package/lib/web/ui-session-tools.mjs +224 -0
- package/lib/web/ui-template.mjs +29 -1968
- package/lib/web/ui-terminal-find.mjs +5 -3
- package/lib/web/ui-workbench.mjs +5 -3
- package/lib/web/ui-workspace.mjs +3 -0
- package/package.json +8 -2
- package/scripts/benchmark-session-sync.mjs +53 -0
- package/scripts/codex-protocol.mjs +88 -0
- package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
- package/scripts/dsh-package-provenance.mjs +118 -0
- package/scripts/mcp-form-live-fixture.mjs +81 -0
- package/scripts/native-bounded-live-acceptance.mjs +193 -0
- package/scripts/native-fixture-root.mjs +49 -0
- package/scripts/native-installed-acceptance.mjs +611 -0
- package/scripts/native-stability-checks.mjs +40 -0
- package/scripts/regression-cookie-expiry.mjs +70 -0
- package/scripts/regression-http-transport.mjs +9 -0
- package/scripts/regression-terminal-marker.mjs +95 -0
- package/scripts/regression.mjs +481 -219
- package/scripts/shim-reentry-probe.mjs +105 -0
- package/scripts/web-acceptance-diagnostics.mjs +90 -0
- package/scripts/web-codex-account-acceptance.mjs +178 -0
- package/scripts/web-file-preview-checks.mjs +302 -0
- package/scripts/web-mcp-form-acceptance.mjs +134 -0
- package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
- package/scripts/web-native-handoff-checks.mjs +128 -0
- package/scripts/web-native-interaction-acceptance.mjs +219 -18
- package/scripts/web-native-interaction-diagnostics.mjs +80 -0
- package/scripts/web-native-model-acceptance.mjs +22 -7
- package/scripts/web-session-tools-checks.mjs +112 -0
- package/scripts/web-workbench-acceptance.mjs +435 -0
- package/scripts/web-workbench-installed-acceptance.mjs +122 -0
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { shortHash } from '../peers/format.mjs';
|
|
5
|
+
import { HOOK_ROOT_IDENTITY_ENV, hookRootIdentityValue } from './hook-root-identity.mjs';
|
|
5
6
|
|
|
6
7
|
export const WEB_CHILD_ENV = 'HCC_WEB_CHILD';
|
|
7
8
|
export const LAUNCH_FINGERPRINT_ENV = 'HCC_LAUNCH_FINGERPRINT';
|
|
@@ -11,6 +12,7 @@ const LAUNCH_ENV_IGNORED_KEYS = new Set([
|
|
|
11
12
|
'_',
|
|
12
13
|
'COLUMNS',
|
|
13
14
|
'HCC_DB',
|
|
15
|
+
HOOK_ROOT_IDENTITY_ENV,
|
|
14
16
|
'HCC_NO_AUTO_INSTALL_TMUX',
|
|
15
17
|
'HCC_PEER',
|
|
16
18
|
'HCC_ROOT',
|
|
@@ -41,8 +43,12 @@ const LAUNCH_ENV_IGNORED_KEYS = new Set([
|
|
|
41
43
|
'SSH_AUTH_SOCK'
|
|
42
44
|
]);
|
|
43
45
|
|
|
44
|
-
export function childSessionEnv(extra = {}, baseEnv = process.env) {
|
|
46
|
+
export function childSessionEnv(extra = {}, baseEnv = process.env, { rootIdentity = null } = {}) {
|
|
45
47
|
const env = { ...(baseEnv || {}), ...extra };
|
|
48
|
+
if (Object.hasOwn(extra, 'HCC_ROOT')) {
|
|
49
|
+
if (extra.HCC_ROOT) env[HOOK_ROOT_IDENTITY_ENV] = hookRootIdentityValue(extra.HCC_ROOT, rootIdentity);
|
|
50
|
+
else delete env[HOOK_ROOT_IDENTITY_ENV];
|
|
51
|
+
}
|
|
46
52
|
delete env[WEB_CHILD_ENV];
|
|
47
53
|
delete env.HCC_RUNTIME_LOCAL_ONLY;
|
|
48
54
|
return env;
|
package/lib/db/connection.mjs
CHANGED
|
@@ -10,11 +10,16 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
10
10
|
import { CliError } from '../shared/errors.mjs';
|
|
11
11
|
import {
|
|
12
12
|
DB_SCHEMA_VERSION,
|
|
13
|
-
execWithBusyRetry
|
|
13
|
+
execWithBusyRetry,
|
|
14
|
+
readSchemaVersion
|
|
14
15
|
} from './schema.mjs';
|
|
15
16
|
import { initSchemaWithBackup } from './migration-backup.mjs';
|
|
16
17
|
import { readProjectRegistry } from '../runtime/projects.mjs';
|
|
17
18
|
import { resolveProjectDatabase, secureExistingProjectStateDirectory } from '../runtime/project-path.mjs';
|
|
19
|
+
import { projectStateDir, legacyProjectStateDir } from '../runtime/paths.mjs';
|
|
20
|
+
import { privateProjectStateDir, ensurePrivateProjectStateDir } from '../runtime/private-state.mjs';
|
|
21
|
+
import { unsafeDirectoryAcl } from '../runtime/project-trust.mjs';
|
|
22
|
+
import { assertSelectedCwdSnapshot } from '../process/selected-cwd-identity.mjs';
|
|
18
23
|
|
|
19
24
|
const MIGRATION_FAILURE_COOLDOWN_MS = 5 * 60 * 1000;
|
|
20
25
|
|
|
@@ -25,48 +30,110 @@ function managedDatabase(ctx) {
|
|
|
25
30
|
try { canonicalRoot = fs.realpathSync.native(requestedRoot); } catch {}
|
|
26
31
|
const requestedDb = path.resolve(ctx.dbPath);
|
|
27
32
|
let canonicalCandidate = requestedDb;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
let parent = path.dirname(requestedDb);
|
|
34
|
+
const missingParents = [];
|
|
35
|
+
for (;;) {
|
|
36
|
+
try {
|
|
37
|
+
canonicalCandidate = path.join(fs.realpathSync.native(parent),
|
|
38
|
+
...missingParents, path.basename(requestedDb));
|
|
39
|
+
break;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error?.code !== 'ENOENT' || path.dirname(parent) === parent) break;
|
|
42
|
+
missingParents.unshift(path.basename(parent));
|
|
43
|
+
parent = path.dirname(parent);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const candidates = new Set([requestedDb, canonicalCandidate]);
|
|
47
|
+
try { candidates.add(fs.realpathSync.native(requestedDb)); } catch {}
|
|
48
|
+
const within = (parent, candidate) => {
|
|
49
|
+
const relative = path.relative(parent, candidate);
|
|
50
|
+
return relative !== '' && relative !== '..' &&
|
|
51
|
+
!relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
52
|
+
};
|
|
53
|
+
const managed = [requestedRoot, canonicalRoot].some(root =>
|
|
54
|
+
[legacyProjectStateDir(root), privateProjectStateDir(root)].some(stateDir =>
|
|
55
|
+
[...candidates].some(db => within(stateDir, db))));
|
|
56
|
+
if (managed) return true;
|
|
57
|
+
// A context may have captured A's local or private .hello-cc DB before its
|
|
58
|
+
// root alias was repointed to B. A reserved state path is never an explicit
|
|
59
|
+
// external DB just because it no longer belongs to the current root.
|
|
60
|
+
const reservedStatePath = db => path.dirname(db).split(path.sep).some(segment =>
|
|
61
|
+
segment.toLowerCase() === '.hello-cc');
|
|
62
|
+
if ([...candidates].some(reservedStatePath)) {
|
|
63
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN',
|
|
64
|
+
`Private database path is not bound to the current project root: ${requestedDb}`);
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
37
67
|
}
|
|
38
68
|
|
|
39
|
-
function
|
|
69
|
+
function secureOwnedSqliteFile(file, { create = false, readOnly = false, optional = false } = {}) {
|
|
70
|
+
const changed = () => new CliError('PROJECT_PATH_FORBIDDEN', `SQLite file changed during validation: ${file}`);
|
|
71
|
+
const safeStat = stat => stat && !stat.isSymbolicLink() && stat.isFile() && stat.nlink === 1 &&
|
|
72
|
+
(typeof process.getuid !== 'function' || stat.uid === process.getuid()) &&
|
|
73
|
+
(process.platform === 'win32' || (stat.mode & 0o022) === 0) &&
|
|
74
|
+
!unsafeDirectoryAcl(file, stat);
|
|
75
|
+
const stillSame = (current, original) => safeStat(current) &&
|
|
76
|
+
current.dev === original.dev && current.ino === original.ino;
|
|
77
|
+
const recheck = () => {
|
|
78
|
+
try { return fs.lstatSync(file); }
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!optional || error?.code !== 'ENOENT') throw error;
|
|
81
|
+
// An optional WAL/SHM/journal may disappear as SQLite closes its last
|
|
82
|
+
// connection. Do not accept a different file rebuilt at the same path.
|
|
83
|
+
try { fs.lstatSync(file); }
|
|
84
|
+
catch (again) {
|
|
85
|
+
if (again?.code === 'ENOENT') return null;
|
|
86
|
+
throw again;
|
|
87
|
+
}
|
|
88
|
+
throw changed();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
40
91
|
let stat;
|
|
41
|
-
try { stat = fs.lstatSync(
|
|
92
|
+
try { stat = fs.lstatSync(file); }
|
|
42
93
|
catch (error) {
|
|
43
94
|
if (error?.code !== 'ENOENT') throw error;
|
|
44
|
-
if (!create)
|
|
95
|
+
if (!create) return false;
|
|
45
96
|
try {
|
|
46
|
-
const fd = fs.openSync(
|
|
97
|
+
const fd = fs.openSync(file, fs.constants.O_RDWR | fs.constants.O_CREAT |
|
|
47
98
|
fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
48
99
|
fs.closeSync(fd);
|
|
49
100
|
} catch (creationError) {
|
|
50
101
|
if (creationError?.code !== 'EEXIST') throw creationError;
|
|
51
102
|
}
|
|
52
|
-
stat = fs.lstatSync(
|
|
103
|
+
stat = fs.lstatSync(file);
|
|
53
104
|
}
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
105
|
+
if (!safeStat(stat)) {
|
|
106
|
+
// ACL inspection invokes ls and may outlive a retiring optional sidecar.
|
|
107
|
+
// Missing is safe to skip; a rebuilt or otherwise unsafe file is not.
|
|
108
|
+
if (optional && !recheck()) return false;
|
|
109
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN', `SQLite file must be an owned non-writable regular file: ${file}`);
|
|
57
110
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
111
|
+
// Never open/close an existing SQLite file outside SQLite: on POSIX, close()
|
|
112
|
+
// can release locks held by another SQLite connection in this process.
|
|
113
|
+
const current = recheck();
|
|
114
|
+
if (!current) return false;
|
|
115
|
+
if (!stillSame(current, stat)) throw changed();
|
|
116
|
+
if (!readOnly && process.platform !== 'win32' && (current.mode & 0o777) !== 0o600) {
|
|
117
|
+
try { fs.chmodSync(file, 0o600); }
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (!optional || error?.code !== 'ENOENT') throw error;
|
|
120
|
+
if (!recheck()) return false;
|
|
121
|
+
throw changed();
|
|
68
122
|
}
|
|
69
|
-
|
|
123
|
+
const tightened = recheck();
|
|
124
|
+
if (!tightened) return false;
|
|
125
|
+
if (!stillSame(tightened, stat) || (tightened.mode & 0o777) !== 0o600) throw changed();
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function secureManagedDatabaseFile(dbPath, create, readOnly = false) {
|
|
131
|
+
if (!secureOwnedSqliteFile(dbPath, { create, readOnly })) {
|
|
132
|
+
throw new CliError('NOT_FOUND', `Project database does not exist: ${dbPath}`);
|
|
133
|
+
}
|
|
134
|
+
for (const suffix of ['-wal', '-shm', '-journal']) {
|
|
135
|
+
secureOwnedSqliteFile(`${dbPath}${suffix}`, { readOnly, optional: true });
|
|
136
|
+
}
|
|
70
137
|
}
|
|
71
138
|
|
|
72
139
|
export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogText }) {
|
|
@@ -74,11 +141,25 @@ export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogTe
|
|
|
74
141
|
const migratedRegisteredProjectDbs = new Set();
|
|
75
142
|
|
|
76
143
|
function connect(ctx, options = {}) {
|
|
144
|
+
const existingSchema = options.existingSchema === true;
|
|
145
|
+
if (existingSchema && options.create !== false) {
|
|
146
|
+
throw new TypeError('existingSchema requires create: false');
|
|
147
|
+
}
|
|
148
|
+
if (ctx.initialRootIdentity) assertSelectedCwdSnapshot(ctx.initialRootIdentity);
|
|
77
149
|
let dbPath = ctx.dbPath;
|
|
78
150
|
if (managedDatabase(ctx)) {
|
|
151
|
+
const rootWasMissing = options.create !== false && !fs.existsSync(ctx.root);
|
|
152
|
+
const pendingDefault = rootWasMissing ? path.join(privateProjectStateDir(ctx.root), 'mesh.db') : null;
|
|
79
153
|
if (options.create !== false && !fs.existsSync(ctx.root)) {
|
|
80
154
|
fs.mkdirSync(ctx.root, { recursive: true });
|
|
81
155
|
}
|
|
156
|
+
if (rootWasMissing && path.resolve(dbPath) === pendingDefault) {
|
|
157
|
+
dbPath = path.join(projectStateDir(ctx.root), 'mesh.db');
|
|
158
|
+
}
|
|
159
|
+
if (rootWasMissing && path.resolve(ctx.dbPath) === pendingDefault &&
|
|
160
|
+
projectStateDir(ctx.root) === privateProjectStateDir(ctx.root)) {
|
|
161
|
+
ensurePrivateProjectStateDir(ctx.root, { create: true });
|
|
162
|
+
}
|
|
82
163
|
const resolved = resolveProjectDatabase({
|
|
83
164
|
root: ctx.root,
|
|
84
165
|
db: dbPath,
|
|
@@ -94,26 +175,43 @@ export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogTe
|
|
|
94
175
|
}
|
|
95
176
|
if (options.create !== false) fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
96
177
|
}
|
|
97
|
-
const db = new DatabaseSync(dbPath, { timeout: 5000 });
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
178
|
+
const db = new DatabaseSync(dbPath, { timeout: existingSchema ? 0 : 5000 });
|
|
179
|
+
try {
|
|
180
|
+
if (existingSchema) {
|
|
181
|
+
// Shutdown may abandon advisory detachment when another writer owns
|
|
182
|
+
// the DB. Never migrate, change journal mode, or retry while exiting.
|
|
183
|
+
db.exec('PRAGMA busy_timeout = 0; PRAGMA foreign_keys = ON;');
|
|
184
|
+
if (readSchemaVersion(db) !== DB_SCHEMA_VERSION) {
|
|
185
|
+
throw new CliError('DB_SCHEMA_NOT_CURRENT', 'Shutdown requires an existing current database schema');
|
|
186
|
+
}
|
|
187
|
+
return db;
|
|
188
|
+
}
|
|
189
|
+
db.exec('PRAGMA busy_timeout = 5000;');
|
|
190
|
+
execWithBusyRetry(db, 'PRAGMA journal_mode = WAL;', { ignoreBusy: true });
|
|
191
|
+
db.exec('PRAGMA synchronous = NORMAL;');
|
|
192
|
+
db.exec('PRAGMA wal_autocheckpoint = 1000;');
|
|
193
|
+
db.exec('PRAGMA foreign_keys = ON;');
|
|
194
|
+
initSchemaWithBackup(db, dbPath, {
|
|
195
|
+
beforePostMigrationIndexes: dedupePeerBindings
|
|
196
|
+
});
|
|
197
|
+
if (options.migrateRegistered !== false) migrateRegisteredProjectDbs(ctx);
|
|
198
|
+
return db;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
try { db.close(); } catch {}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
108
203
|
}
|
|
109
204
|
|
|
110
205
|
function connectReadOnly(ctx) {
|
|
111
|
-
|
|
206
|
+
if (ctx.initialRootIdentity) assertSelectedCwdSnapshot(ctx.initialRootIdentity);
|
|
207
|
+
const managed = managedDatabase(ctx);
|
|
208
|
+
const dbPath = managed
|
|
112
209
|
? resolveProjectDatabase({ root: ctx.root, db: ctx.dbPath, createStateDir: false }).db
|
|
113
210
|
: ctx.dbPath;
|
|
114
211
|
if (!fs.existsSync(dbPath) || !fs.statSync(dbPath).isFile()) {
|
|
115
212
|
throw new CliError('NOT_FOUND', `Project database does not exist: ${dbPath}`);
|
|
116
213
|
}
|
|
214
|
+
if (managed) secureManagedDatabaseFile(dbPath, false, true);
|
|
117
215
|
return new DatabaseSync(dbPath, { timeout: 5000, readOnly: true });
|
|
118
216
|
}
|
|
119
217
|
|
|
@@ -128,7 +226,7 @@ export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogTe
|
|
|
128
226
|
try {
|
|
129
227
|
resolved = resolveProjectDatabase({
|
|
130
228
|
root: project.root,
|
|
131
|
-
db: project.db || path.join(project.root, '
|
|
229
|
+
db: project.db || path.join(projectStateDir(project.root), 'mesh.db'),
|
|
132
230
|
createStateDir: false
|
|
133
231
|
});
|
|
134
232
|
} catch (err) {
|
package/lib/db/schema.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { aggregateCleanupFailure } from '../shared/cleanup-error.mjs';
|
|
2
|
+
import { automaticResultKey } from '../shared/automatic-result-key.mjs';
|
|
2
3
|
import { CliError } from '../shared/errors.mjs';
|
|
3
4
|
|
|
4
5
|
export const DB_SCHEMA_VERSION = 7;
|
|
6
|
+
const AUTOMATIC_RESULT_SCRUB_KEY = 'automatic_result_content_scrub_v1';
|
|
5
7
|
|
|
6
8
|
const sleepBuffer = new SharedArrayBuffer(4);
|
|
7
9
|
const sleepView = new Int32Array(sleepBuffer);
|
|
@@ -125,6 +127,63 @@ export function initSchema(db, {
|
|
|
125
127
|
runSchemaMigrations(db);
|
|
126
128
|
if (typeof beforePostMigrationIndexes === 'function') beforePostMigrationIndexes(db);
|
|
127
129
|
createPostMigrationIndexes(db);
|
|
130
|
+
scrubAutomaticTaskResults(db);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function scrubAutomaticTaskResults(db) {
|
|
134
|
+
const marker = db.prepare('SELECT 1 FROM meta WHERE key = ?');
|
|
135
|
+
if (marker.get(AUTOMATIC_RESULT_SCRUB_KEY)) return;
|
|
136
|
+
tx(db, () => {
|
|
137
|
+
if (marker.get(AUTOMATIC_RESULT_SCRUB_KEY)) return;
|
|
138
|
+
const read = db.prepare("SELECT id,payload,created_at FROM events WHERE type = 'task.result.recorded' AND id > ? ORDER BY id LIMIT 100");
|
|
139
|
+
const update = db.prepare('UPDATE events SET payload = ? WHERE id = ?');
|
|
140
|
+
let lastId = 0;
|
|
141
|
+
while (true) {
|
|
142
|
+
const rows = read.all(lastId);
|
|
143
|
+
if (!rows.length) break;
|
|
144
|
+
for (const row of rows) {
|
|
145
|
+
let record;
|
|
146
|
+
try { record = JSON.parse(row.payload); } catch { continue; }
|
|
147
|
+
if (record?.source !== 'executor' || record.stage !== 'local' ||
|
|
148
|
+
!['command', 'diff'].includes(record.kind) ||
|
|
149
|
+
typeof record.executor_id !== 'string' || !record.executor_id ||
|
|
150
|
+
typeof record.thread_id !== 'string' || !record.thread_id ||
|
|
151
|
+
typeof record.turn_id !== 'string' || !record.turn_id ||
|
|
152
|
+
typeof record.result_key !== 'string') continue;
|
|
153
|
+
// Legacy Web observer keys identify these records more precisely than
|
|
154
|
+
// source/kind/turn metadata, which an explicit result may also carry.
|
|
155
|
+
const commandKey = `${record.executor_id}:${record.turn_id}:${record.item_id}:command`;
|
|
156
|
+
const diffPrefix = `${record.executor_id}:${record.turn_id}:diff:`;
|
|
157
|
+
const legacyCommand = record.kind === 'command' && typeof record.item_id === 'string' &&
|
|
158
|
+
record.item_id && record.result_key === commandKey;
|
|
159
|
+
const legacyDiff = record.kind === 'diff' && record.title === 'Turn changes awaiting review' &&
|
|
160
|
+
record.result_key.startsWith(diffPrefix) &&
|
|
161
|
+
/^[a-f0-9]{64}$/.test(record.result_key.slice(diffPrefix.length));
|
|
162
|
+
if (!legacyCommand && !legacyDiff) continue;
|
|
163
|
+
const executorId = typeof record.executor_id === 'string' ? record.executor_id : null;
|
|
164
|
+
const turnId = typeof record.turn_id === 'string' ? record.turn_id : null;
|
|
165
|
+
const itemId = typeof record.item_id === 'string' ? record.item_id : null;
|
|
166
|
+
// Distinct old diff hashes may become the same structural key. Keep the
|
|
167
|
+
// historical rows; result-key lookup already chooses the latest ID.
|
|
168
|
+
const resultKey = automaticResultKey({ executorId,
|
|
169
|
+
threadId: typeof record.thread_id === 'string' ? record.thread_id : null,
|
|
170
|
+
turnId, kind: record.kind, itemId: record.kind === 'command' ? itemId : null });
|
|
171
|
+
update.run(JSON.stringify({
|
|
172
|
+
result_id: record.result_id, result_key: resultKey,
|
|
173
|
+
peer: record.peer, task_id: record.task_id, stage: 'local', status: record.status,
|
|
174
|
+
kind: record.kind, source: 'executor',
|
|
175
|
+
title: record.kind === 'command' ? 'Command execution' : 'Turn changes awaiting review',
|
|
176
|
+
details: '', evidence: [], command: '', diff: '',
|
|
177
|
+
executor_id: executorId, thread_id: record.thread_id, turn_id: turnId,
|
|
178
|
+
item_id: record.kind === 'command' ? itemId : null,
|
|
179
|
+
exit_code: Number.isSafeInteger(record.exit_code) ? record.exit_code : null,
|
|
180
|
+
created_at: row.created_at
|
|
181
|
+
}), row.id);
|
|
182
|
+
}
|
|
183
|
+
lastId = rows.at(-1).id;
|
|
184
|
+
}
|
|
185
|
+
db.prepare('INSERT INTO meta(key,value) VALUES (?,?)').run(AUTOMATIC_RESULT_SCRUB_KEY, '1');
|
|
186
|
+
});
|
|
128
187
|
}
|
|
129
188
|
|
|
130
189
|
function createBaseSchema(db) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { CODEX_INTERACTION_METHODS } from '../integrations/codex-interactions.mjs';
|
|
2
|
+
|
|
3
|
+
// A bounded upgrade gate, not a JSON Schema validator or a runtime permission
|
|
4
|
+
// check. Generated types remain the authoritative version-specific artifacts.
|
|
5
|
+
export const CODEX_CLIENT_METHODS = Object.freeze([
|
|
6
|
+
'initialize', 'thread/start', 'thread/resume', 'thread/read', 'thread/list',
|
|
7
|
+
'thread/fork', 'turn/start', 'turn/steer', 'turn/interrupt',
|
|
8
|
+
'mcpServerStatus/list', 'mcpServer/tool/call', 'account/read', 'account/rateLimits/read'
|
|
9
|
+
]);
|
|
10
|
+
export const CODEX_NOTIFICATION_METHODS = Object.freeze([
|
|
11
|
+
'thread/started', 'thread/status/changed', 'turn/started', 'turn/completed',
|
|
12
|
+
'turn/plan/updated', 'turn/diff/updated', 'item/started', 'item/completed',
|
|
13
|
+
'item/agentMessage/delta', 'serverRequest/resolved', 'error',
|
|
14
|
+
'account/updated', 'account/login/completed', 'account/rateLimits/updated'
|
|
15
|
+
]);
|
|
16
|
+
const ACCOUNT_SCHEMA_FILES = ['GetAccountParams', 'GetAccountResponse', 'GetAccountRateLimitsResponse',
|
|
17
|
+
'AccountUpdatedNotification', 'AccountLoginCompletedNotification', 'AccountRateLimitsUpdatedNotification'];
|
|
18
|
+
export const CODEX_PROTOCOL_SCHEMA_FILES = Object.freeze([
|
|
19
|
+
'ClientRequest', 'ClientNotification', 'ServerRequest', 'ServerNotification',
|
|
20
|
+
'CommandExecutionRequestApprovalResponse', 'FileChangeRequestApprovalResponse',
|
|
21
|
+
'PermissionsRequestApprovalResponse', 'ToolRequestUserInputResponse',
|
|
22
|
+
'McpServerElicitationRequestParams', 'McpServerElicitationRequestResponse',
|
|
23
|
+
...ACCOUNT_SCHEMA_FILES
|
|
24
|
+
]);
|
|
25
|
+
// Official account schemas are generated only in the versioned v2 directory.
|
|
26
|
+
export const CODEX_PROTOCOL_SCHEMA_PATHS = Object.freeze(Object.fromEntries(CODEX_PROTOCOL_SCHEMA_FILES
|
|
27
|
+
.map(name => [name, (ACCOUNT_SCHEMA_FILES.includes(name) ? 'v2/' : '') + name + '.json'])));
|
|
28
|
+
|
|
29
|
+
function methods(schema) {
|
|
30
|
+
return (schema?.oneOf || []).flatMap(row => row.properties?.method?.enum || []);
|
|
31
|
+
}
|
|
32
|
+
function enumValues(schema, document, seen = new Set()) {
|
|
33
|
+
if (!schema || typeof schema !== 'object') return [];
|
|
34
|
+
if (schema.$ref) {
|
|
35
|
+
const ref = schema.$ref;
|
|
36
|
+
if (seen.has(ref) || !ref.startsWith('#/definitions/')) return [];
|
|
37
|
+
const next = new Set(seen); next.add(ref);
|
|
38
|
+
return enumValues(document.definitions?.[ref.slice('#/definitions/'.length)], document, next);
|
|
39
|
+
}
|
|
40
|
+
return [...(schema.enum || []), ...(Object.hasOwn(schema, 'const') ? [schema.const] : []),
|
|
41
|
+
...['oneOf', 'anyOf', 'allOf'].flatMap(key => (schema[key] || []).flatMap(row => enumValues(row, document, seen)))];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function inspectCodexProtocol(schemas) {
|
|
45
|
+
const checks = [];
|
|
46
|
+
const check = (contract, ok) => checks.push({ contract, status: ok ? 'pass' : 'fail' });
|
|
47
|
+
for (const [file, expected] of [
|
|
48
|
+
['ClientRequest', CODEX_CLIENT_METHODS], ['ClientNotification', ['initialized']],
|
|
49
|
+
['ServerRequest', [...CODEX_INTERACTION_METHODS]], ['ServerNotification', CODEX_NOTIFICATION_METHODS]
|
|
50
|
+
]) {
|
|
51
|
+
const available = methods(schemas[file]);
|
|
52
|
+
for (const method of expected) check(`${file}: ${method}`, available.includes(method));
|
|
53
|
+
}
|
|
54
|
+
for (const [file, fields] of [
|
|
55
|
+
['CommandExecutionRequestApprovalResponse', ['decision']],
|
|
56
|
+
['FileChangeRequestApprovalResponse', ['decision']],
|
|
57
|
+
['PermissionsRequestApprovalResponse', ['permissions', 'scope']],
|
|
58
|
+
['ToolRequestUserInputResponse', ['answers']],
|
|
59
|
+
['McpServerElicitationRequestResponse', ['action', 'content']]
|
|
60
|
+
]) {
|
|
61
|
+
const schema = schemas[file];
|
|
62
|
+
check(`${file}: response fields`, fields.every(field => Object.hasOwn(schema?.properties || {}, field)));
|
|
63
|
+
// HCC never supplies unknown required response fields. Optional extensions
|
|
64
|
+
// are compatible with this gate; new required fields demand adapter review.
|
|
65
|
+
check(`${file}: no new required response fields`, Boolean(schema) && (schema.required || []).every(field => fields.includes(field)));
|
|
66
|
+
}
|
|
67
|
+
for (const [file, field, expected] of [
|
|
68
|
+
['CommandExecutionRequestApprovalResponse', 'decision', ['accept', 'decline', 'cancel']],
|
|
69
|
+
['FileChangeRequestApprovalResponse', 'decision', ['accept', 'decline', 'cancel']],
|
|
70
|
+
['PermissionsRequestApprovalResponse', 'scope', ['turn', 'session']],
|
|
71
|
+
['McpServerElicitationRequestResponse', 'action', ['accept', 'decline', 'cancel']]
|
|
72
|
+
]) {
|
|
73
|
+
const values = enumValues(schemas[file]?.properties?.[field], schemas[file] || {});
|
|
74
|
+
check(`${file}: ${field} choices`, expected.every(value => values.includes(value)));
|
|
75
|
+
}
|
|
76
|
+
const elicitation = schemas.McpServerElicitationRequestParams;
|
|
77
|
+
check('MCP elicitation: thread and server identity',
|
|
78
|
+
['threadId', 'serverName'].every(field => elicitation?.required?.includes(field)) &&
|
|
79
|
+
Object.hasOwn(elicitation?.properties || {}, 'turnId'));
|
|
80
|
+
for (const [mode, fields] of [['form', ['message', 'mode', 'requestedSchema']], ['url', ['message', 'mode', 'elicitationId', 'url']]]) {
|
|
81
|
+
check(`MCP elicitation: ${mode} identity and payload`, Boolean(elicitation?.oneOf?.some(row =>
|
|
82
|
+
row.properties?.mode?.enum?.includes(mode) && fields.every(field => row.required?.includes(field) && Object.hasOwn(row.properties || {}, field)))));
|
|
83
|
+
}
|
|
84
|
+
check('Account read: explicit nonrefreshing option', schemas.GetAccountParams?.properties?.refreshToken?.type === 'boolean' &&
|
|
85
|
+
(schemas.GetAccountParams?.required || []).every(field => field === 'refreshToken'));
|
|
86
|
+
check('Account read: auth requirement is explicit', schemas.GetAccountResponse?.required?.includes('requiresOpenaiAuth') &&
|
|
87
|
+
schemas.GetAccountResponse?.properties?.requiresOpenaiAuth?.type === 'boolean');
|
|
88
|
+
check('Account read: recognized account types', ['apiKey', 'chatgpt', 'amazonBedrock'].every(type =>
|
|
89
|
+
schemas.GetAccountResponse?.definitions?.Account?.oneOf?.some(row => row.properties?.type?.enum?.includes(type))));
|
|
90
|
+
check('Account limits: historical and multi-bucket fields', ['rateLimits', 'rateLimitsByLimitId'].every(field =>
|
|
91
|
+
Object.hasOwn(schemas.GetAccountRateLimitsResponse?.properties || {}, field)));
|
|
92
|
+
check('Account limits: window percentage', schemas.GetAccountRateLimitsResponse?.definitions?.RateLimitWindow?.required?.includes('usedPercent') &&
|
|
93
|
+
['integer', 'number'].includes(schemas.GetAccountRateLimitsResponse?.definitions?.RateLimitWindow?.properties?.usedPercent?.type));
|
|
94
|
+
check('Account notifications: status and quota fields', Object.hasOwn(schemas.AccountUpdatedNotification?.properties || {}, 'authMode') &&
|
|
95
|
+
Object.hasOwn(schemas.AccountUpdatedNotification?.properties || {}, 'planType') && schemas.AccountRateLimitsUpdatedNotification?.required?.includes('rateLimits'));
|
|
96
|
+
check('Account notification: correlated login completion', schemas.AccountLoginCompletedNotification?.required?.includes('success') &&
|
|
97
|
+
Object.hasOwn(schemas.AccountLoginCompletedNotification?.properties || {}, 'loginId'));
|
|
98
|
+
const unsupportedServerRequests = methods(schemas.ServerRequest).filter(method => !CODEX_INTERACTION_METHODS.has(method)).sort();
|
|
99
|
+
return {
|
|
100
|
+
status: checks.every(entry => entry.status === 'pass') ? 'compatible' : 'incompatible',
|
|
101
|
+
checks, unsupportedServerRequests,
|
|
102
|
+
scope: 'Method presence, response fields and choices, MCP form/URL identity, and read-only account/quota fields; not full schema or runtime validation.',
|
|
103
|
+
inferenceCalled: false, accountAccessed: false
|
|
104
|
+
};
|
|
105
|
+
}
|
package/lib/guidance.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { resolveProjectDatabase } from './runtime/project-path.mjs';
|
|
2
|
+
import { writeProjectGuidanceFiles, removeProjectGuidanceFiles } from './process/pinned-guidance.mjs';
|
|
3
|
+
import { writePinnedProjectGuidance } from './process/pinned-project-state.mjs';
|
|
4
|
+
import { captureSelectedCwdIdentity } from './process/selected-cwd-identity.mjs';
|
|
3
5
|
|
|
4
6
|
export function guidanceMarkdown() {
|
|
5
7
|
return `# hello-cc Coordination Rules
|
|
@@ -83,41 +85,24 @@ Before stopping:
|
|
|
83
85
|
`;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
export function writeGuidance(root) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
: `${existing.trimEnd()}\n${clause}`;
|
|
102
|
-
fs.writeFileSync(target, updated);
|
|
88
|
+
export function writeGuidance(root, options = {}) {
|
|
89
|
+
const selectedRoot = captureSelectedCwdIdentity(root, options.expectedSnapshot);
|
|
90
|
+
let selectedState;
|
|
91
|
+
try {
|
|
92
|
+
const stateDir = resolveProjectDatabase({ root, createStateDir: true }).stateDir;
|
|
93
|
+
selectedState = captureSelectedCwdIdentity(stateDir);
|
|
94
|
+
selectedRoot.assertUnchanged();
|
|
95
|
+
const content = guidanceMarkdown();
|
|
96
|
+
writeProjectGuidanceFiles(root, content, { ...options, expectedBinding: selectedRoot });
|
|
97
|
+
selectedRoot.assertUnchanged();
|
|
98
|
+
return writePinnedProjectGuidance(stateDir, content, selectedState,
|
|
99
|
+
{ spawnProcess: options.stateSpawnProcess });
|
|
100
|
+
} finally {
|
|
101
|
+
selectedState?.release();
|
|
102
|
+
selectedRoot.release();
|
|
103
103
|
}
|
|
104
|
-
return guidePath;
|
|
105
104
|
}
|
|
106
105
|
|
|
107
|
-
export function removeGuidanceBlocks(root) {
|
|
108
|
-
|
|
109
|
-
for (const file of ['CLAUDE.md', 'AGENTS.md']) {
|
|
110
|
-
const target = path.join(root, file);
|
|
111
|
-
if (!fs.existsSync(target)) continue;
|
|
112
|
-
const existing = fs.readFileSync(target, 'utf8');
|
|
113
|
-
const updated = existing
|
|
114
|
-
.replace(/\n?<!-- hello-cc:start -->[\s\S]*?<!-- hello-cc:end -->\n?/g, '\n')
|
|
115
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
116
|
-
.trimEnd();
|
|
117
|
-
if (updated !== existing) {
|
|
118
|
-
fs.writeFileSync(target, updated ? `${updated}\n` : '');
|
|
119
|
-
changed.push(target);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return changed;
|
|
106
|
+
export function removeGuidanceBlocks(root, options = {}) {
|
|
107
|
+
return removeProjectGuidanceFiles(root, options);
|
|
123
108
|
}
|