@logicseek/hello-cc 1.0.2 → 1.0.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 +34 -0
- package/docs/dsh.md +3 -3
- package/docs/dsh.zh-CN.md +3 -3
- package/lib/cli/app.mjs +1 -1
- package/lib/cli/commands/hook.mjs +3 -1
- package/lib/cli/commands/lifecycle.mjs +14 -3
- package/lib/cli/commands/query.mjs +15 -10
- package/lib/cli/commands/run.mjs +7 -6
- package/lib/db/connection.mjs +82 -9
- package/lib/discover.mjs +4 -4
- package/lib/integrations/hooks.mjs +132 -40
- package/lib/integrations/peers/identity.mjs +42 -7
- package/lib/runtime/native/service.mjs +107 -24
- package/lib/runtime/native/store.mjs +63 -6
- package/lib/runtime/project-path.mjs +90 -7
- package/lib/shared/file-lock.mjs +62 -17
- package/lib/shared/socket-lock-worker.mjs +15 -6
- package/lib/web/codex-app-server.mjs +19 -1
- package/lib/web/codex-sessions.mjs +2 -0
- package/lib/web/cookie-auth.mjs +1 -1
- package/lib/web/http-routes.mjs +11 -7
- package/lib/web/http.mjs +20 -2
- package/lib/web/native-sessions.mjs +13 -6
- package/lib/web/peer-actions.mjs +27 -27
- package/lib/web/runtime-main.mjs +20 -3
- package/lib/web/runtime.mjs +18 -4
- package/lib/web/session-serialize.mjs +1 -1
- package/lib/web/startup.mjs +84 -10
- package/lib/web/task-results.mjs +8 -5
- package/lib/web/terminal-assets.mjs +18 -0
- package/lib/web/tmux-stream.mjs +3 -4
- package/lib/web/ui-codex.mjs +137 -20
- package/lib/web/ui-interactions.mjs +37 -3
- package/lib/web/ui-native.mjs +21 -9
- package/lib/web/ui-template.mjs +8 -2
- package/package.json +1 -1
- package/scripts/regression.mjs +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,40 @@ the release description from the current changelog section. Use
|
|
|
16
16
|
`npm run release:github` with `GH_TOKEN` or `GITHUB_TOKEN` only for local
|
|
17
17
|
backfills.
|
|
18
18
|
|
|
19
|
+
## 1.0.3
|
|
20
|
+
|
|
21
|
+
### Summary
|
|
22
|
+
|
|
23
|
+
This maintenance release keeps DeepSeek Harness on the normal npm `latest`
|
|
24
|
+
channel and repairs the CLI, native-session and Web regressions identified while
|
|
25
|
+
archiving 1.0.2. It retains the verified Cordis cleanup and recovery behavior.
|
|
26
|
+
|
|
27
|
+
### Highlights
|
|
28
|
+
|
|
29
|
+
- Preserve provider process identity during registration and discovery; update
|
|
30
|
+
owned hooks without discarding unrelated hooks or damaged user configuration.
|
|
31
|
+
- Keep explicit hook database routing and quote generated terminal arguments.
|
|
32
|
+
- Preserve the recorded origin of native submissions across restarts; peer
|
|
33
|
+
messages cannot gain local-user authority from their sender name or text.
|
|
34
|
+
- Keep managed state directories and database files private without replacing
|
|
35
|
+
existing data or changing external database parent permissions. Terminal stream
|
|
36
|
+
buffers follow each session's project.
|
|
37
|
+
- Bound streaming updates and long-message rendering while preserving complete
|
|
38
|
+
copied content, reader position, approval details and session ownership checks.
|
|
39
|
+
- Isolate request parsing failures, retain local/proxy access contracts, serialize
|
|
40
|
+
Web startup and resolve terminal assets with npm dependency hoisting.
|
|
41
|
+
- Native runtime ownership uses alternate ports for identifiable unrelated local
|
|
42
|
+
listeners while keeping one owner per project. Silent or legacy listeners are
|
|
43
|
+
rejected conservatively, and ownership remains until shutdown completes.
|
|
44
|
+
|
|
45
|
+
### Validation
|
|
46
|
+
|
|
47
|
+
The full restored regression suite and the installed official Harness package
|
|
48
|
+
checks run against this composed release. The frozen file manifest and verification
|
|
49
|
+
record live under `docs/verification/`; those source-only records are excluded
|
|
50
|
+
from the npm package. Publication, current-device activation and real-model
|
|
51
|
+
acceptance are recorded separately from source and CI checks.
|
|
52
|
+
|
|
19
53
|
## 1.0.2
|
|
20
54
|
|
|
21
55
|
### Summary
|
package/docs/dsh.md
CHANGED
|
@@ -8,7 +8,7 @@ The tested runtime is `@deepseek-ai/dsh@0.2.0-rc.2`, with matching plugin versio
|
|
|
8
8
|
|
|
9
9
|
Supported hosts are Linux and macOS; on Windows use WSL. Native Windows shells are not supported. Device acceptance must be recorded separately from container verification.
|
|
10
10
|
|
|
11
|
-
This integration
|
|
11
|
+
This integration has been included since hello-cc 1.0.2 on the normal npm `latest` channel. With Node.js 24+, install it using `npm install -g @logicseek/hello-cc`. To test a source checkout instead, use:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
node /absolute/path/to/hello-cc/bin/hcc.mjs --root /path/to/project dsh setup --mode cordis
|
|
@@ -113,7 +113,7 @@ Project overlays work directly from source. The managed manifest records the Cor
|
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
115
|
npm pack --ignore-scripts
|
|
116
|
-
dsh plugin --profile web add /absolute/path/to/logicseek-hello-cc-1.0.
|
|
116
|
+
dsh plugin --profile web add /absolute/path/to/logicseek-hello-cc-1.0.3.tgz
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
Harness requires explicit approval for native dependency builds. If add returns `ERR_PNPM_IGNORED_BUILDS`, merge the following into the selected profile's `pnpm-workspace.yaml` and rerun the same add command. The default path is `~/.dsh/profiles/web/pnpm-workspace.yaml`; use the configured DSH_HOME when different. Preserve existing settings and approve only the candidate's pinned native dependency:
|
|
@@ -123,7 +123,7 @@ allowBuilds:
|
|
|
123
123
|
node-pty@1.2.0-beta.15: true
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
Harness selects bundles through the profile's `dsh.profile.bundles`; do not also load the project Cordis/hooks overlay. Actual npm installation, official add/repeated add/remove, profile resolution and real-model collaboration calls have passed. The actual macOS desktop profile was installed and hot-loaded on 2026-10-02. Its paired ACP worker completed real collaboration calls using the existing DeepSeek route. The initially configured desktop sub2api model returned HTTP 403 because that account only accepts official Codex clients. After the desktop default was observed to have changed to official DeepSeek, a new session completed hcc_state, hcc_message_send and a real completed turn with that existing selection. The original sub2api route restriction remains. The 1.0.
|
|
126
|
+
Harness selects bundles through the profile's `dsh.profile.bundles`; do not also load the project Cordis/hooks overlay. Actual npm installation, official add/repeated add/remove, profile resolution and real-model collaboration calls have passed. The actual macOS desktop profile was installed and hot-loaded on 2026-10-02. Its paired ACP worker completed real collaboration calls using the existing DeepSeek route. The initially configured desktop sub2api model returned HTTP 403 because that account only accepts official Codex clients. After the desktop default was observed to have changed to official DeepSeek, a new session completed hcc_state, hcc_message_send and a real completed turn with that existing selection. The original sub2api route restriction remains. The 1.0.3 release uses the normal npm latest channel. Harness manages profile package removal; project-overlay integration is disabled with `setup --mode off`.
|
|
127
127
|
|
|
128
128
|
See Cordis/native acceptance (source checkout: `docs/verification/2026-10-02-dsh-cordis-native.md`), bridge acceptance (source checkout: `docs/verification/2026-10-02-dsh-official-bridge.md`), and the integration plan (source checkout: `docs/plans/2026-10-02-dsh-integration.md`). Real-model checks used temporary projects and owned processes, without replacing existing user sessions. The isolated order fixture passed artifact/evidence/handoff/lock-release/task-completion checks. See the Mac device receipt (source checkout: `docs/verification/2026-10-02-dsh-device-install.md`). Publication, deployment, other-device installation and genuine business acceptance remain separate delivery stages.
|
|
129
129
|
|
package/docs/dsh.zh-CN.md
CHANGED
|
@@ -8,7 +8,7 @@ DeepSeek Harness(`dsh`)可以与 Claude Code、Codex 共用 hello-cc 的项
|
|
|
8
8
|
|
|
9
9
|
支持 Linux 和 macOS;Windows 使用 WSL,不支持原生 Windows shell。容器验证与实际设备验收分别记录。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
此接入自 hello-cc 1.0.2 起提供,沿用 npm 的 latest 标签。在 Node.js 24+ 环境执行 `npm install -g @logicseek/hello-cc` 安装。验证源码时,用本仓库的 CLI:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
node /absolute/path/to/hello-cc/bin/hcc.mjs --root /path/to/project dsh setup --mode cordis
|
|
@@ -115,7 +115,7 @@ hcc native start --peer dsh-reviewer --provider dsh --binary /absolute/path/to/d
|
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
117
|
npm pack --ignore-scripts
|
|
118
|
-
dsh plugin --profile web add /absolute/path/to/logicseek-hello-cc-1.0.
|
|
118
|
+
dsh plugin --profile web add /absolute/path/to/logicseek-hello-cc-1.0.3.tgz
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
Harness 的 pnpm 策略要求明确允许原生依赖构建。若首次 add 返回 `ERR_PNPM_IGNORED_BUILDS`,在所选 profile 的 `pnpm-workspace.yaml` 中合并以下配置,然后重跑同一 add 命令;默认位置是 `~/.dsh/profiles/web/pnpm-workspace.yaml`,自定义 DSH_HOME 时以该目录为准。保留文件中已有设置,只批准候选包使用的固定版本:
|
|
@@ -125,7 +125,7 @@ allowBuilds:
|
|
|
125
125
|
node-pty@1.2.0-beta.15: true
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
安装后由 Harness profile 的 `dsh.profile.bundles` 选择包;不要再同时加载项目 Cordis/hooks overlay。bundle 已通过实际 npm 安装、官方 add/重复 add/remove、隔离 profile 加载和真实模型工具调用。2026-10-02 已安装到本机真实 desktop profile 并热加载,配套 ACP 使用既有 DeepSeek 路由完成真实工具调用;初次桌面 sub2api 模型返回账户仅允许 Codex 官方客户端的 403;随后桌面默认模型已变为官方 DeepSeek,沿用当时配置的新建会话完成 hcc_state、hcc_message_send 和真实 completed 回合。原 sub2api 路由限制仍存在。1.0.
|
|
128
|
+
安装后由 Harness profile 的 `dsh.profile.bundles` 选择包;不要再同时加载项目 Cordis/hooks overlay。bundle 已通过实际 npm 安装、官方 add/重复 add/remove、隔离 profile 加载和真实模型工具调用。2026-10-02 已安装到本机真实 desktop profile 并热加载,配套 ACP 使用既有 DeepSeek 路由完成真实工具调用;初次桌面 sub2api 模型返回账户仅允许 Codex 官方客户端的 403;随后桌面默认模型已变为官方 DeepSeek,沿用当时配置的新建会话完成 hcc_state、hcc_message_send 和真实 completed 回合。原 sub2api 路由限制仍存在。1.0.3 沿用 npm latest 标签发布。profile 插件安装/移除由 Harness 管理,项目 overlay 则使用 `setup --mode off` 关闭。
|
|
129
129
|
|
|
130
130
|
验证记录见 Cordis/native 验收 (源码目录: `docs/verification/2026-10-02-dsh-cordis-native.md`)、兼容桥记录 (源码目录: `docs/verification/2026-10-02-dsh-official-bridge.md`) 和详细接入计划 (源码目录: `docs/plans/2026-10-02-dsh-integration.md`)。真实模型验收在临时项目和自有进程中完成;既有会话与模型凭据保留;本机实际安装记录见 Mac 接入验收 (源码目录: `docs/verification/2026-10-02-dsh-device-install.md`)。Linux 容器已验证,实际 WSL/员工设备和真实业务签收尚未完成;隔离订单样例已完成报告、证据、交接、解锁与任务结束验证。
|
|
131
131
|
|
package/lib/cli/app.mjs
CHANGED
|
@@ -303,7 +303,7 @@ const { cmdInit, cmdRegister, cmdEnv, cmdJoin } = createLifecycleCommands({
|
|
|
303
303
|
connect, now, tx, addEvent, printResult, CliError, parseOpts, intOpt,
|
|
304
304
|
registerProjectActivity, resolveCurrentPeer, resolveTargetPeer,
|
|
305
305
|
upsertPeer, upsertCanonicalPeerBinding, storedPeerIdentity, detectBranch,
|
|
306
|
-
writeGuidance, helpEnv, helpJoin, shellExports, path, process
|
|
306
|
+
readAncestorCliInfo, writeGuidance, helpEnv, helpJoin, shellExports, path, process
|
|
307
307
|
});
|
|
308
308
|
|
|
309
309
|
const { cmdTeam } = createTeamCommands({
|
|
@@ -89,13 +89,15 @@ async function cmdHook(ctx, args) {
|
|
|
89
89
|
const dshProject = isDsh && !nativeOwner ? resolveProjectDatabase({ root: hccRoot, createStateDir: false }) : null;
|
|
90
90
|
const nativeProject = nativeOwner ? resolveProjectDatabase({ root: hccRoot,
|
|
91
91
|
db: process.env.HCC_DB, createStateDir: false }) : null;
|
|
92
|
+
const ordinaryDb = (ctx.root && path.resolve(ctx.root) === hccRoot ? ctx.dbPath : null) ||
|
|
93
|
+
process.env.HCC_DB;
|
|
92
94
|
if (nativeProject && !fs.existsSync(nativeProject.db)) {
|
|
93
95
|
throw new CliError('NATIVE_HOOK_OWNERSHIP_MISMATCH', 'Native hook database must already be owned by its runtime');
|
|
94
96
|
}
|
|
95
97
|
const hookCtx = {
|
|
96
98
|
...ctx,
|
|
97
99
|
root: nativeProject?.root || dshProject?.root || hccRoot,
|
|
98
|
-
dbPath: nativeProject?.db || dshProject?.db || path.join(hccRoot, '.hello-cc', 'mesh.db')
|
|
100
|
+
dbPath: nativeProject?.db || dshProject?.db || (ordinaryDb ? path.resolve(ordinaryDb) : path.join(hccRoot, '.hello-cc', 'mesh.db'))
|
|
99
101
|
};
|
|
100
102
|
if (!nativeOwner) registerProjectActivity(hookCtx);
|
|
101
103
|
|
|
@@ -4,10 +4,21 @@ export function createLifecycleCommands(deps) {
|
|
|
4
4
|
const {
|
|
5
5
|
connect, now, addEvent, printResult, CliError, parseOpts, intOpt,
|
|
6
6
|
registerProjectActivity, resolveCurrentPeer, resolveTargetPeer,
|
|
7
|
-
upsertPeer, upsertCanonicalPeerBinding, detectBranch,
|
|
7
|
+
upsertPeer, upsertCanonicalPeerBinding, detectBranch, readAncestorCliInfo,
|
|
8
8
|
writeGuidance, helpEnv, helpJoin, shellExports, path, process
|
|
9
9
|
} = deps;
|
|
10
10
|
|
|
11
|
+
function registrationPid(opts) {
|
|
12
|
+
if (opts.pid !== undefined) return intOpt(opts, 'pid', process.ppid);
|
|
13
|
+
const ancestor = readAncestorCliInfo();
|
|
14
|
+
if (['claude', 'codex'].includes(ancestor?.kind) &&
|
|
15
|
+
(!opts.kind || opts.kind === ancestor.kind) &&
|
|
16
|
+
Number.isSafeInteger(ancestor.pid) && ancestor.pid > 1) {
|
|
17
|
+
return ancestor.pid;
|
|
18
|
+
}
|
|
19
|
+
return process.ppid;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
async function cmdInit(ctx, args) {
|
|
12
23
|
registerProjectActivity(ctx);
|
|
13
24
|
const opts = parseOpts(args, { booleans: ['no-guidance'] });
|
|
@@ -34,7 +45,7 @@ export function createLifecycleCommands(deps) {
|
|
|
34
45
|
role: opts.role || '',
|
|
35
46
|
worktree: path.resolve(opts.worktree || ctx.cwd),
|
|
36
47
|
branch: opts.branch || detectBranch(ctx.cwd),
|
|
37
|
-
pid:
|
|
48
|
+
pid: registrationPid(opts),
|
|
38
49
|
status: opts.status || 'idle',
|
|
39
50
|
capabilities: Array.isArray(opts.cap) ? opts.cap.join(',') : (opts.cap || opts.capabilities || '')
|
|
40
51
|
};
|
|
@@ -67,7 +78,7 @@ export function createLifecycleCommands(deps) {
|
|
|
67
78
|
role: opts.role || 'peer',
|
|
68
79
|
worktree: path.resolve(opts.worktree || ctx.cwd),
|
|
69
80
|
branch: opts.branch || detectBranch(ctx.cwd),
|
|
70
|
-
pid:
|
|
81
|
+
pid: registrationPid(opts),
|
|
71
82
|
status: opts.status || 'working',
|
|
72
83
|
capabilities: Array.isArray(opts.cap) ? opts.cap.join(',') : (opts.cap || opts.capabilities || 'manual-shell')
|
|
73
84
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Coordination query commands extracted from bin/hcc.mjs: peers, status, state, prompt.
|
|
2
2
|
|
|
3
|
+
import { shellQuoteArg } from '../../format.mjs';
|
|
4
|
+
|
|
3
5
|
export function createQueryCommands(deps) {
|
|
4
6
|
const {
|
|
5
7
|
connect, now, printResult, table, CliError, ACTIVE_PEER_TTL,
|
|
@@ -63,7 +65,10 @@ export function createQueryCommands(deps) {
|
|
|
63
65
|
const peer = required(opts, 'peer', 'HCC_PEER');
|
|
64
66
|
const kind = opts.kind || 'codex';
|
|
65
67
|
const role = opts.role || 'peer';
|
|
66
|
-
const cmd =
|
|
68
|
+
const cmd = ['node', commandPath(), '--root', ctx.root, '--db', ctx.dbPath].map(shellQuoteArg).join(' ');
|
|
69
|
+
const quotedPeer = shellQuoteArg(peer);
|
|
70
|
+
const quotedKind = shellQuoteArg(kind);
|
|
71
|
+
const quotedRole = shellQuoteArg(role);
|
|
67
72
|
const text = `You are ${peer}, a ${kind} ${role} session working in this project.
|
|
68
73
|
|
|
69
74
|
Use hcc as the shared coordination bus for this terminal session. This
|
|
@@ -71,23 +76,23 @@ project uses a flat peer mesh: there is no required main/worker hierarchy.
|
|
|
71
76
|
|
|
72
77
|
Run these commands before changing files:
|
|
73
78
|
|
|
74
|
-
${cmd} register --peer ${
|
|
75
|
-
${cmd} state --peer ${
|
|
76
|
-
${cmd} msg inbox --peer ${
|
|
77
|
-
${cmd} task next --peer ${
|
|
79
|
+
${cmd} register --peer ${quotedPeer} --kind ${quotedKind} --role ${quotedRole}
|
|
80
|
+
${cmd} state --peer ${quotedPeer}
|
|
81
|
+
${cmd} msg inbox --peer ${quotedPeer}
|
|
82
|
+
${cmd} task next --peer ${quotedPeer}
|
|
78
83
|
|
|
79
84
|
Coordination rules:
|
|
80
85
|
- Claim exactly one task before editing.
|
|
81
86
|
- If state shows a current task for ${peer}, continue that task before claiming another pending task.
|
|
82
87
|
- Before editing a file, directory, module, or shared test resource, run:
|
|
83
|
-
${cmd} lock acquire --peer ${
|
|
88
|
+
${cmd} lock acquire --peer ${quotedPeer} --resource <path-or-module> [--scope <scope>] --task <task-id>
|
|
84
89
|
- If a lock is held by another peer, message that peer instead of editing:
|
|
85
|
-
${cmd} msg send --from ${
|
|
90
|
+
${cmd} msg send --from ${quotedPeer} --to <peer-id> --body "<question>"
|
|
86
91
|
- Report progress or requests through msg send.
|
|
87
92
|
- Before stopping, run tests, create a handoff, and release locks:
|
|
88
|
-
${cmd} handoff create --from ${
|
|
89
|
-
${cmd} task done --peer ${
|
|
90
|
-
${cmd} lock release --peer ${
|
|
93
|
+
${cmd} handoff create --from ${quotedPeer} --task <task-id> --summary "<what changed>" --tests "<commands/results>" --risks "<known risks>"
|
|
94
|
+
${cmd} task done --peer ${quotedPeer} --id <task-id> --summary "<done summary>"
|
|
95
|
+
${cmd} lock release --peer ${quotedPeer} --resource <path-or-module> [--scope <scope>]
|
|
91
96
|
`;
|
|
92
97
|
printResult(ctx, { prompt: text }, () => text);
|
|
93
98
|
}
|
package/lib/cli/commands/run.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { CliError } from '../../shared/errors.mjs';
|
|
|
11
11
|
import { tx } from '../../db/schema.mjs';
|
|
12
12
|
import { parseOpts, validateOpts } from '../../cli-args.mjs';
|
|
13
13
|
import { runtimeProcessIdentity } from '../../runtime/state.mjs';
|
|
14
|
+
import { ensurePrivateProjectBufferDirectory } from '../../runtime/project-path.mjs';
|
|
14
15
|
import { withBufferDirectoryLease } from '../../runtime/buffer-directory-lease.mjs';
|
|
15
16
|
import {
|
|
16
17
|
externalBufferEvidence,
|
|
@@ -178,8 +179,7 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
178
179
|
const ptyModule = await import('node-pty');
|
|
179
180
|
const pty = ptyModule.default || ptyModule;
|
|
180
181
|
|
|
181
|
-
const bufsDir =
|
|
182
|
-
fs.mkdirSync(bufsDir, { recursive: true });
|
|
182
|
+
const bufsDir = ensurePrivateProjectBufferDirectory(ctx.root, BUFS_DIR_NAME);
|
|
183
183
|
const outFile = path.join(bufsDir, `${id}.out`);
|
|
184
184
|
const inFile = path.join(bufsDir, `${id}.in`);
|
|
185
185
|
const resizeFile = path.join(bufsDir, `${id}.resize`);
|
|
@@ -219,8 +219,8 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
219
219
|
}
|
|
220
220
|
for (const file of bufferFiles) fs.rmSync(file, { force: true });
|
|
221
221
|
}
|
|
222
|
-
fs.writeFileSync(inFile, '');
|
|
223
|
-
fs.writeFileSync(resizeFile, '');
|
|
222
|
+
fs.writeFileSync(inFile, '', { flag: 'wx', mode: 0o600 });
|
|
223
|
+
fs.writeFileSync(resizeFile, '', { flag: 'wx', mode: 0o600 });
|
|
224
224
|
fs.writeFileSync(metaFile, JSON.stringify({
|
|
225
225
|
id,
|
|
226
226
|
generation: bufferGeneration,
|
|
@@ -231,7 +231,7 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
231
231
|
wrapper_pid: process.pid,
|
|
232
232
|
...(publishingWrapperIdentity ? { wrapper_identity: publishingWrapperIdentity } : {}),
|
|
233
233
|
publishing: true
|
|
234
|
-
}));
|
|
234
|
+
}), { flag: 'wx', mode: 0o600 });
|
|
235
235
|
|
|
236
236
|
const db = connect(ctx);
|
|
237
237
|
try {
|
|
@@ -359,7 +359,8 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
359
359
|
if (!ownsExternalBufferGroup()) {
|
|
360
360
|
throw new CliError('EXTERNAL_SESSION_SUPERSEDED', `External session ${id} was replaced during startup`);
|
|
361
361
|
}
|
|
362
|
-
outFd = fs.openSync(outFile,
|
|
362
|
+
outFd = fs.openSync(outFile, fs.constants.O_WRONLY | fs.constants.O_CREAT |
|
|
363
|
+
fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
363
364
|
});
|
|
364
365
|
} catch (error) {
|
|
365
366
|
await failStartup(error?.code || 'external_buffer_open_failed');
|
package/lib/db/connection.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
8
9
|
import { DatabaseSync } from 'node:sqlite';
|
|
9
10
|
import { CliError } from '../shared/errors.mjs';
|
|
10
11
|
import {
|
|
@@ -13,26 +14,93 @@ import {
|
|
|
13
14
|
} from './schema.mjs';
|
|
14
15
|
import { initSchemaWithBackup } from './migration-backup.mjs';
|
|
15
16
|
import { readProjectRegistry } from '../runtime/projects.mjs';
|
|
16
|
-
import { resolveProjectDatabase } from '../runtime/project-path.mjs';
|
|
17
|
+
import { resolveProjectDatabase, secureExistingProjectStateDirectory } from '../runtime/project-path.mjs';
|
|
17
18
|
|
|
18
19
|
const MIGRATION_FAILURE_COOLDOWN_MS = 5 * 60 * 1000;
|
|
19
20
|
|
|
21
|
+
function managedDatabase(ctx) {
|
|
22
|
+
if (!ctx.root) return false;
|
|
23
|
+
const requestedRoot = path.resolve(ctx.root);
|
|
24
|
+
let canonicalRoot = requestedRoot;
|
|
25
|
+
try { canonicalRoot = fs.realpathSync.native(requestedRoot); } catch {}
|
|
26
|
+
const requestedDb = path.resolve(ctx.dbPath);
|
|
27
|
+
let canonicalCandidate = requestedDb;
|
|
28
|
+
try {
|
|
29
|
+
canonicalCandidate = path.join(fs.realpathSync.native(path.dirname(requestedDb)), path.basename(requestedDb));
|
|
30
|
+
} catch {}
|
|
31
|
+
return [requestedRoot, canonicalRoot].some(root =>
|
|
32
|
+
[requestedDb, canonicalCandidate].some(db => {
|
|
33
|
+
const relative = path.relative(path.join(root, '.hello-cc'), db);
|
|
34
|
+
return relative !== '' && relative !== '..' &&
|
|
35
|
+
!relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function secureManagedDatabaseFile(dbPath, create) {
|
|
40
|
+
let stat;
|
|
41
|
+
try { stat = fs.lstatSync(dbPath); }
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
44
|
+
if (!create) throw new CliError('NOT_FOUND', `Project database does not exist: ${dbPath}`);
|
|
45
|
+
try {
|
|
46
|
+
const fd = fs.openSync(dbPath, fs.constants.O_RDWR | fs.constants.O_CREAT |
|
|
47
|
+
fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
48
|
+
fs.closeSync(fd);
|
|
49
|
+
} catch (creationError) {
|
|
50
|
+
if (creationError?.code !== 'EEXIST') throw creationError;
|
|
51
|
+
}
|
|
52
|
+
stat = fs.lstatSync(dbPath);
|
|
53
|
+
}
|
|
54
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1 ||
|
|
55
|
+
(typeof process.getuid === 'function' && stat.uid !== process.getuid())) {
|
|
56
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN', `Project database must be an owned regular file: ${dbPath}`);
|
|
57
|
+
}
|
|
58
|
+
const fd = fs.openSync(dbPath, fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0));
|
|
59
|
+
try {
|
|
60
|
+
const opened = fs.fstatSync(fd);
|
|
61
|
+
if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino) {
|
|
62
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN', `Project database changed during validation: ${dbPath}`);
|
|
63
|
+
}
|
|
64
|
+
if (process.platform !== 'win32' && (opened.mode & 0o777) !== 0o600) fs.fchmodSync(fd, 0o600);
|
|
65
|
+
const current = fs.lstatSync(dbPath);
|
|
66
|
+
if (current.isSymbolicLink() || current.dev !== stat.dev || current.ino !== stat.ino) {
|
|
67
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN', `Project database changed during validation: ${dbPath}`);
|
|
68
|
+
}
|
|
69
|
+
} finally { fs.closeSync(fd); }
|
|
70
|
+
}
|
|
71
|
+
|
|
20
72
|
export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogText }) {
|
|
21
73
|
let projectMigrationFanoutDepth = 0;
|
|
22
74
|
const migratedRegisteredProjectDbs = new Set();
|
|
23
75
|
|
|
24
76
|
function connect(ctx, options = {}) {
|
|
25
|
-
|
|
26
|
-
|
|
77
|
+
let dbPath = ctx.dbPath;
|
|
78
|
+
if (managedDatabase(ctx)) {
|
|
79
|
+
if (options.create !== false && !fs.existsSync(ctx.root)) {
|
|
80
|
+
fs.mkdirSync(ctx.root, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
const resolved = resolveProjectDatabase({
|
|
83
|
+
root: ctx.root,
|
|
84
|
+
db: dbPath,
|
|
85
|
+
createStateDir: options.create !== false,
|
|
86
|
+
createDatabaseParents: options.create !== false
|
|
87
|
+
});
|
|
88
|
+
dbPath = resolved.db;
|
|
89
|
+
if (options.create === false) secureExistingProjectStateDirectory(resolved.root);
|
|
90
|
+
secureManagedDatabaseFile(dbPath, options.create !== false);
|
|
91
|
+
} else {
|
|
92
|
+
if (options.create === false && !fs.existsSync(dbPath)) {
|
|
93
|
+
throw new CliError('NOT_FOUND', `Project database does not exist: ${dbPath}`);
|
|
94
|
+
}
|
|
95
|
+
if (options.create !== false) fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
27
96
|
}
|
|
28
|
-
|
|
29
|
-
const db = new DatabaseSync(ctx.dbPath, { timeout: 5000 });
|
|
97
|
+
const db = new DatabaseSync(dbPath, { timeout: 5000 });
|
|
30
98
|
db.exec('PRAGMA busy_timeout = 5000;');
|
|
31
99
|
execWithBusyRetry(db, 'PRAGMA journal_mode = WAL;', { ignoreBusy: true });
|
|
32
100
|
db.exec('PRAGMA synchronous = NORMAL;');
|
|
33
101
|
db.exec('PRAGMA wal_autocheckpoint = 1000;');
|
|
34
102
|
db.exec('PRAGMA foreign_keys = ON;');
|
|
35
|
-
initSchemaWithBackup(db,
|
|
103
|
+
initSchemaWithBackup(db, dbPath, {
|
|
36
104
|
beforePostMigrationIndexes: dedupePeerBindings
|
|
37
105
|
});
|
|
38
106
|
if (options.migrateRegistered !== false) migrateRegisteredProjectDbs(ctx);
|
|
@@ -40,10 +108,13 @@ export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogTe
|
|
|
40
108
|
}
|
|
41
109
|
|
|
42
110
|
function connectReadOnly(ctx) {
|
|
43
|
-
|
|
44
|
-
|
|
111
|
+
const dbPath = managedDatabase(ctx)
|
|
112
|
+
? resolveProjectDatabase({ root: ctx.root, db: ctx.dbPath, createStateDir: false }).db
|
|
113
|
+
: ctx.dbPath;
|
|
114
|
+
if (!fs.existsSync(dbPath) || !fs.statSync(dbPath).isFile()) {
|
|
115
|
+
throw new CliError('NOT_FOUND', `Project database does not exist: ${dbPath}`);
|
|
45
116
|
}
|
|
46
|
-
return new DatabaseSync(
|
|
117
|
+
return new DatabaseSync(dbPath, { timeout: 5000, readOnly: true });
|
|
47
118
|
}
|
|
48
119
|
|
|
49
120
|
function migrateRegisteredProjectDbs(ctx) {
|
|
@@ -77,6 +148,8 @@ export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogTe
|
|
|
77
148
|
} catch {}
|
|
78
149
|
let db = null;
|
|
79
150
|
try {
|
|
151
|
+
secureExistingProjectStateDirectory(root);
|
|
152
|
+
secureManagedDatabaseFile(dbPath, false);
|
|
80
153
|
db = new DatabaseSync(dbPath, { timeout: 5000 });
|
|
81
154
|
db.exec('PRAGMA busy_timeout = 5000;');
|
|
82
155
|
execWithBusyRetry(db, 'PRAGMA journal_mode = WAL;', { ignoreBusy: true });
|
package/lib/discover.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import fs from 'node:fs';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import os from 'node:os';
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { providerSessionPeerId } from './core/peers/session.mjs';
|
|
14
15
|
|
|
15
16
|
// ─── Root detection ──────────────────────────────────────────────────────────
|
|
16
17
|
|
|
@@ -33,9 +34,8 @@ function fsExists(p) {
|
|
|
33
34
|
* Derive a stable peer ID.
|
|
34
35
|
*
|
|
35
36
|
* Stability contract:
|
|
36
|
-
* -
|
|
37
|
-
* -
|
|
38
|
-
* - fresh session → <kind>-<sessionId[:8]> (unique per session)
|
|
37
|
+
* - provider session ID/name → the same full-value hash used by hooks/shims
|
|
38
|
+
* - fresh session → the same stable provider identity
|
|
39
39
|
*/
|
|
40
40
|
export function derivePeerId(kind, sessionId, resumeId, explicitPeer = null) {
|
|
41
41
|
if (explicitPeer) return explicitPeer;
|
|
@@ -44,7 +44,7 @@ export function derivePeerId(kind, sessionId, resumeId, explicitPeer = null) {
|
|
|
44
44
|
if (process.env.HCC_PEER) return process.env.HCC_PEER;
|
|
45
45
|
|
|
46
46
|
const base = resumeId || sessionId;
|
|
47
|
-
if (base
|
|
47
|
+
if (base) return providerSessionPeerId(kind, base);
|
|
48
48
|
|
|
49
49
|
// Fallback: tty device number or pid
|
|
50
50
|
const ttyNum = getTtyNum();
|