@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
package/lib/runtime/state.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
4
5
|
import { compareProcessIdentity, inspectProcessIdentity } from '../process/identity.mjs';
|
|
5
6
|
import { CliError } from '../shared/errors.mjs';
|
|
6
7
|
import { withFileLock } from '../shared/file-lock.mjs';
|
|
7
8
|
import { writeJsonSafe } from '../shared/json-file.mjs';
|
|
9
|
+
import { readPrivateTextFile } from './private-file.mjs';
|
|
10
|
+
import { resolveProjectDatabase, secureExistingProjectStateDirectory } from './project-path.mjs';
|
|
8
11
|
import {
|
|
9
|
-
globalRuntimePath
|
|
10
|
-
runtimePath
|
|
12
|
+
globalRuntimePath
|
|
11
13
|
} from './paths.mjs';
|
|
12
14
|
import { runtimeHttpRequest } from '../web/runtime.mjs';
|
|
13
15
|
import { API_VERSION } from '../web/api-version.mjs';
|
|
@@ -16,6 +18,18 @@ const DEFAULT_PRODUCT_NAME = 'hello-cc';
|
|
|
16
18
|
const DEFAULT_CLI_NAME = 'hcc';
|
|
17
19
|
export const RUNTIME_POINTER_UNKNOWN_GRACE_MS = 120_000;
|
|
18
20
|
|
|
21
|
+
function validatedProjectRuntimeFile(root, { create = false } = {}) {
|
|
22
|
+
const state = create
|
|
23
|
+
? resolveProjectDatabase({
|
|
24
|
+
root, db: path.join(path.resolve(root), '.hello-cc', 'runtime.json'), createStateDir: true
|
|
25
|
+
})
|
|
26
|
+
: secureExistingProjectStateDirectory(root);
|
|
27
|
+
// Validate this target, not an unused default mesh.db (custom/external DBs
|
|
28
|
+
// remain supported). Keep the returned state route instead of resolving a
|
|
29
|
+
// mutable root alias again after validation.
|
|
30
|
+
return path.join(state.stateDir, 'runtime.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
function completeProcessIdentity(value) {
|
|
20
34
|
return compareProcessIdentity(value, value) === 'live';
|
|
21
35
|
}
|
|
@@ -77,11 +91,15 @@ export function reclaimRuntimePointerFiles(files, {
|
|
|
77
91
|
reclaimUnknown = true,
|
|
78
92
|
dryRun = false,
|
|
79
93
|
onReclaim = null,
|
|
94
|
+
expectedIdentity = null,
|
|
80
95
|
withLock = withFileLock
|
|
81
96
|
} = {}) {
|
|
82
97
|
if (typeof reclaimUnknown !== 'boolean') {
|
|
83
98
|
throw new TypeError('reclaimUnknown must be a boolean');
|
|
84
99
|
}
|
|
100
|
+
if (expectedIdentity !== null && !completeProcessIdentity(expectedIdentity)) {
|
|
101
|
+
throw new TypeError('expectedIdentity must be a complete process identity');
|
|
102
|
+
}
|
|
85
103
|
let blocked = false;
|
|
86
104
|
let reclaimed = 0;
|
|
87
105
|
const outcomes = [];
|
|
@@ -107,6 +125,16 @@ export function reclaimRuntimePointerFiles(files, {
|
|
|
107
125
|
const raw = fs.readFileSync(file, 'utf8');
|
|
108
126
|
let runtime = null;
|
|
109
127
|
try { runtime = JSON.parse(raw); } catch {}
|
|
128
|
+
if (expectedIdentity !== null) {
|
|
129
|
+
const currentIdentity = runtimeIdentityEvidence(runtime).identity;
|
|
130
|
+
if (Number(runtime?.pid) !== expectedIdentity.pid ||
|
|
131
|
+
compareProcessIdentity(expectedIdentity, currentIdentity) !== 'live' ||
|
|
132
|
+
currentIdentity.commandHash !== expectedIdentity.commandHash) {
|
|
133
|
+
blocked = true;
|
|
134
|
+
record({ state: 'unknown', action: 'blocked', reason: 'owner_changed' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
110
138
|
const timing = runtimePointerAge(stat, nowMs(), unknownGraceMs);
|
|
111
139
|
const evidence = classifyRuntimePointer(runtime, {
|
|
112
140
|
inspect,
|
|
@@ -200,9 +228,10 @@ function runtimeMetadataMatchesPointer(runtime, metadata) {
|
|
|
200
228
|
|
|
201
229
|
export function readGlobalRuntimeFile() {
|
|
202
230
|
const file = globalRuntimePath();
|
|
203
|
-
|
|
231
|
+
const raw = readPrivateTextFile(file);
|
|
232
|
+
if (raw === null) return null;
|
|
204
233
|
try {
|
|
205
|
-
return JSON.parse(
|
|
234
|
+
return JSON.parse(raw);
|
|
206
235
|
} catch {
|
|
207
236
|
// Do not delete on a parse failure: a torn/partial write must not orphan a
|
|
208
237
|
// healthy runtime pointer. Leave it for health probing / restart to reclaim.
|
|
@@ -217,7 +246,7 @@ export function writeGlobalRuntime(runtime) {
|
|
|
217
246
|
}
|
|
218
247
|
|
|
219
248
|
export function writeRuntime(ctx, runtime) {
|
|
220
|
-
const file =
|
|
249
|
+
const file = validatedProjectRuntimeFile(ctx.root, { create: true });
|
|
221
250
|
withFileLock(file, (lockedFile) => writeJsonSafe(lockedFile, runtime, { mode: 0o600 }));
|
|
222
251
|
return file;
|
|
223
252
|
}
|
|
@@ -235,10 +264,11 @@ export function readRuntime(ctx, opts = {}) {
|
|
|
235
264
|
source: 'env'
|
|
236
265
|
};
|
|
237
266
|
}
|
|
238
|
-
const file =
|
|
239
|
-
|
|
267
|
+
const file = validatedProjectRuntimeFile(ctx.root);
|
|
268
|
+
const raw = readPrivateTextFile(file);
|
|
269
|
+
if (raw !== null) {
|
|
240
270
|
try {
|
|
241
|
-
const runtime = JSON.parse(
|
|
271
|
+
const runtime = JSON.parse(raw);
|
|
242
272
|
if (!runtime.base_url) throw new Error('missing base_url');
|
|
243
273
|
if (!runtimePointerMatchesCurrentProcess(runtime, { inspect: inspectRuntimeProcessIdentity })) {
|
|
244
274
|
throw new Error('runtime process identity does not match');
|
|
@@ -264,9 +294,9 @@ export function readRuntime(ctx, opts = {}) {
|
|
|
264
294
|
}
|
|
265
295
|
|
|
266
296
|
export function readRuntimeFile(ctx) {
|
|
267
|
-
const file =
|
|
268
|
-
|
|
269
|
-
return JSON.parse(
|
|
297
|
+
const file = validatedProjectRuntimeFile(ctx.root);
|
|
298
|
+
const raw = readPrivateTextFile(file);
|
|
299
|
+
return raw === null ? null : JSON.parse(raw);
|
|
270
300
|
}
|
|
271
301
|
|
|
272
302
|
export async function probeRuntime(runtime) {
|
|
@@ -306,30 +336,42 @@ export async function readHealthyGlobalRuntime() {
|
|
|
306
336
|
}
|
|
307
337
|
}
|
|
308
338
|
|
|
309
|
-
export function clearRuntime(ctx, pid = process.pid
|
|
310
|
-
|
|
311
|
-
|
|
339
|
+
export function clearRuntime(ctx, pid = process.pid, {
|
|
340
|
+
nonblocking = false, clearGlobal = true, expectedIdentity = null,
|
|
341
|
+
deadline = Infinity, monotonicNow = () => performance.now()
|
|
342
|
+
} = {}) {
|
|
343
|
+
if (expectedIdentity !== null && !completeProcessIdentity(expectedIdentity)) {
|
|
344
|
+
throw new TypeError('expectedIdentity must be a complete process identity');
|
|
345
|
+
}
|
|
346
|
+
function clearOwnedFile(file) {
|
|
347
|
+
if (monotonicNow() >= deadline) return;
|
|
312
348
|
withFileLock(file, (lockedFile) => {
|
|
313
|
-
if (
|
|
349
|
+
if (monotonicNow() >= deadline) return;
|
|
350
|
+
ctx.rootIdentity?.assertUnchanged();
|
|
351
|
+
const raw = readPrivateTextFile(lockedFile);
|
|
352
|
+
if (raw === null) return;
|
|
314
353
|
try {
|
|
315
|
-
const runtime = JSON.parse(
|
|
354
|
+
const runtime = JSON.parse(raw);
|
|
355
|
+
if (expectedIdentity !== null) {
|
|
356
|
+
const currentIdentity = runtimeIdentityEvidence(runtime).identity;
|
|
357
|
+
if (runtime.pid !== expectedIdentity.pid ||
|
|
358
|
+
compareProcessIdentity(expectedIdentity, currentIdentity) !== 'live' ||
|
|
359
|
+
currentIdentity.commandHash !== expectedIdentity.commandHash) return;
|
|
360
|
+
}
|
|
316
361
|
if (runtime.pid === pid) fs.rmSync(lockedFile, { force: true });
|
|
317
362
|
} catch {
|
|
318
363
|
// Leave an unparseable pointer in place: only remove when we can confirm
|
|
319
364
|
// it belongs to this pid, so a torn write never deletes another runtime's file.
|
|
320
365
|
}
|
|
321
|
-
});
|
|
366
|
+
}, { nonblocking });
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
if (monotonicNow() >= deadline) return;
|
|
370
|
+
ctx.rootIdentity?.assertUnchanged();
|
|
371
|
+
clearOwnedFile(validatedProjectRuntimeFile(ctx.root));
|
|
322
372
|
} catch {}
|
|
323
|
-
|
|
373
|
+
if (!clearGlobal || monotonicNow() >= deadline) return;
|
|
324
374
|
try {
|
|
325
|
-
|
|
326
|
-
if (!fs.existsSync(lockedFile)) return;
|
|
327
|
-
try {
|
|
328
|
-
const runtime = JSON.parse(fs.readFileSync(lockedFile, 'utf8'));
|
|
329
|
-
if (runtime.pid === pid) fs.rmSync(lockedFile, { force: true });
|
|
330
|
-
} catch {
|
|
331
|
-
// Same as above: do not blind-delete a torn global runtime pointer.
|
|
332
|
-
}
|
|
333
|
-
});
|
|
375
|
+
clearOwnedFile(globalRuntimePath());
|
|
334
376
|
} catch {}
|
|
335
377
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function automaticResultKey({ executorId, threadId, turnId, kind, itemId = null }) {
|
|
4
|
+
const identity = [executorId, threadId, turnId, kind, itemId];
|
|
5
|
+
return `automatic:${kind}:${createHash('sha256').update(JSON.stringify(identity)).digest('hex')}`;
|
|
6
|
+
}
|
package/lib/shared/json-file.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
@@ -8,11 +9,59 @@ export function readJsonSafe(filePath) {
|
|
|
8
9
|
|
|
9
10
|
export function writeJsonSafe(filePath, data, opts = {}) {
|
|
10
11
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
11
|
-
const tmpPath = filePath
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const tmpPath = `${filePath}.tmp.${process.pid}.${randomBytes(16).toString('hex')}`;
|
|
13
|
+
const contents = JSON.stringify(data, null, 2) + '\n';
|
|
14
|
+
let fd = null;
|
|
15
|
+
let opened = null;
|
|
16
|
+
let renamed = false;
|
|
17
|
+
try {
|
|
18
|
+
fd = fs.openSync(tmpPath, fs.constants.O_WRONLY | fs.constants.O_CREAT |
|
|
19
|
+
fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
20
|
+
opened = fs.fstatSync(fd);
|
|
21
|
+
if (!opened.isFile() || opened.nlink !== 1 ||
|
|
22
|
+
(typeof process.geteuid === 'function' && opened.uid !== process.geteuid())) {
|
|
23
|
+
throw new Error(`unsafe JSON temporary file: ${tmpPath}`);
|
|
24
|
+
}
|
|
25
|
+
if (process.platform !== 'win32') fs.fchmodSync(fd, 0o600);
|
|
26
|
+
const privateStat = fs.fstatSync(fd);
|
|
27
|
+
if (!privateStat.isFile() || privateStat.nlink !== 1 ||
|
|
28
|
+
privateStat.dev !== opened.dev || privateStat.ino !== opened.ino ||
|
|
29
|
+
(typeof process.geteuid === 'function' && privateStat.uid !== process.geteuid()) ||
|
|
30
|
+
(process.platform !== 'win32' && (privateStat.mode & 0o777) !== 0o600)) {
|
|
31
|
+
throw new Error(`unsafe JSON temporary file: ${tmpPath}`);
|
|
32
|
+
}
|
|
33
|
+
fs.writeFileSync(fd, contents, 'utf8');
|
|
34
|
+
if (opts.mode !== undefined && opts.mode !== 0o600 && process.platform !== 'win32') {
|
|
35
|
+
fs.fchmodSync(fd, opts.mode);
|
|
36
|
+
}
|
|
37
|
+
const ready = fs.fstatSync(fd);
|
|
38
|
+
if (!ready.isFile() || ready.nlink !== 1 ||
|
|
39
|
+
ready.dev !== opened.dev || ready.ino !== opened.ino ||
|
|
40
|
+
(typeof process.geteuid === 'function' && ready.uid !== process.geteuid()) ||
|
|
41
|
+
(process.platform !== 'win32' && (ready.mode & 0o777) !== (opts.mode ?? 0o600))) {
|
|
42
|
+
throw new Error(`unsafe JSON temporary file: ${tmpPath}`);
|
|
43
|
+
}
|
|
44
|
+
fs.fsyncSync(fd);
|
|
45
|
+
fs.closeSync(fd);
|
|
46
|
+
fd = null;
|
|
47
|
+
const current = fs.lstatSync(tmpPath);
|
|
48
|
+
if (!current.isFile() || current.nlink !== 1 ||
|
|
49
|
+
current.dev !== opened.dev || current.ino !== opened.ino ||
|
|
50
|
+
(typeof process.geteuid === 'function' && current.uid !== process.geteuid()) ||
|
|
51
|
+
(process.platform !== 'win32' && (current.mode & 0o777) !== (opts.mode ?? 0o600))) {
|
|
52
|
+
throw new Error(`JSON temporary file changed before rename: ${tmpPath}`);
|
|
53
|
+
}
|
|
54
|
+
fs.renameSync(tmpPath, filePath);
|
|
55
|
+
renamed = true;
|
|
56
|
+
} finally {
|
|
57
|
+
if (fd !== null) fs.closeSync(fd);
|
|
58
|
+
if (!renamed && opened) {
|
|
59
|
+
try {
|
|
60
|
+
const current = fs.lstatSync(tmpPath);
|
|
61
|
+
if (current.dev === opened.dev && current.ino === opened.ino) fs.unlinkSync(tmpPath);
|
|
62
|
+
} catch {
|
|
63
|
+
// Preserve the write failure. Any remaining temp file is private.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
17
66
|
}
|
|
18
67
|
}
|
package/lib/shared/redact.mjs
CHANGED
|
@@ -6,6 +6,7 @@ function isSecretKey(key) {
|
|
|
6
6
|
return normalized === 'authorization' ||
|
|
7
7
|
normalized === 'cookie' ||
|
|
8
8
|
normalized === 'setcookie' ||
|
|
9
|
+
/^hccsid(?:v2https?\d+)?$/.test(normalized) ||
|
|
9
10
|
normalized.includes('secret') ||
|
|
10
11
|
normalized.endsWith('password') ||
|
|
11
12
|
normalized.endsWith('credential') ||
|
|
@@ -21,7 +22,7 @@ function redactString(value) {
|
|
|
21
22
|
.replace(/\bBearer\s+[^\s,;]+/gi, `Bearer ${REDACTED}`)
|
|
22
23
|
.replace(/(--token(?:=|\s+))(?:"[^"]*"|'[^']*'|[^\s]+)/gi, `$1${REDACTED}`)
|
|
23
24
|
.replace(/([?&](?:token|access_token|api_key)=)[^&#\s]*/gi, `$1${REDACTED}`)
|
|
24
|
-
.replace(/(hcc_sid
|
|
25
|
+
.replace(/(hcc_sid(?:_v2_https?_\d+)?=)[^;\s]*/gi, `$1${REDACTED}`);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export function redactCliArgs(args) {
|
package/lib/ui/help.mjs
CHANGED
|
@@ -21,7 +21,8 @@ Commands:
|
|
|
21
21
|
down Stop the running hello-cc web runtime
|
|
22
22
|
update Update the global npm install of hello-cc
|
|
23
23
|
uninstall Remove hooks, shims, and optional project data
|
|
24
|
-
init Initialize
|
|
24
|
+
init Initialize private managed mesh.db and guidance
|
|
25
|
+
migrate-state --offline --yes Copy stopped legacy state to private per-user storage
|
|
25
26
|
register --peer ID Register or update a peer session identity
|
|
26
27
|
join --peer ID Register this shell and print eval-able env
|
|
27
28
|
env --peer ID Print eval-able HCC_PEER/HCC_ROOT/HCC_DB exports
|
|
@@ -405,9 +406,10 @@ Usage:
|
|
|
405
406
|
Stops the current project runtime and removes user-level hello-cc integrations:
|
|
406
407
|
Claude/Codex hooks, claude/codex shims, and the shell PATH entry for shims.
|
|
407
408
|
|
|
408
|
-
With --purge --yes, also removes
|
|
409
|
-
|
|
410
|
-
|
|
409
|
+
With --purge --yes, also removes the selected managed project state directory
|
|
410
|
+
(project-local for stable roots, otherwise under ~/.hello-cc/projects/) and
|
|
411
|
+
hello-cc blocks from CLAUDE.md and AGENTS.md. An old directory retained by
|
|
412
|
+
offline migration is not removed.
|
|
411
413
|
`);
|
|
412
414
|
},
|
|
413
415
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CliError } from '../shared/errors.mjs';
|
|
4
|
+
import { tx } from '../db/schema.mjs';
|
|
5
|
+
|
|
6
|
+
const META_KEY = 'web_agent_defaults_v1';
|
|
7
|
+
const PROVIDERS = ['codex', 'claude', 'dsh'];
|
|
8
|
+
|
|
9
|
+
function invalid(message) { return new CliError('BAD_REQUEST', message); }
|
|
10
|
+
function plainObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
11
|
+
function exactKeys(value, keys) {
|
|
12
|
+
return plainObject(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
13
|
+
}
|
|
14
|
+
function initialDefaults() {
|
|
15
|
+
return { revision: 0, defaultProvider: 'codex', providers: Object.fromEntries(PROVIDERS.map(provider => [provider, { model: null, cwd: '.' }])) };
|
|
16
|
+
}
|
|
17
|
+
function normalizeModel(value) {
|
|
18
|
+
if (value === null) return null;
|
|
19
|
+
if (typeof value !== 'string' || value.length > 256 || /[\x00-\x1f\x7f]/.test(value)) throw invalid('Model must be null or a name of at most 256 characters without control characters');
|
|
20
|
+
return value.trim() || null;
|
|
21
|
+
}
|
|
22
|
+
function relativeCwd(value) {
|
|
23
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 4096 || /[\\:\x00-\x1f\x7f]/.test(value) ||
|
|
24
|
+
path.posix.isAbsolute(value.trim()) || path.win32.isAbsolute(value.trim()) || value.trim().split('/').includes('..')) {
|
|
25
|
+
throw invalid('Default working directory must be a project-relative directory, using . for the project root');
|
|
26
|
+
}
|
|
27
|
+
return path.posix.normalize(value.trim()).replace(/\/$/, '') || '.';
|
|
28
|
+
}
|
|
29
|
+
function assertRoot(ctx) { ctx.rootIdentity?.assertUnchanged(); }
|
|
30
|
+
function existingCwd(ctx, relative) {
|
|
31
|
+
assertRoot(ctx);
|
|
32
|
+
let root, target;
|
|
33
|
+
try {
|
|
34
|
+
root = fs.realpathSync(ctx.root); target = fs.realpathSync(path.resolve(root, relative));
|
|
35
|
+
if (!fs.statSync(target).isDirectory()) throw new Error('not a directory');
|
|
36
|
+
} catch { throw invalid('Default working directory must already exist inside the selected project'); }
|
|
37
|
+
assertRoot(ctx);
|
|
38
|
+
const fromRoot = path.relative(root, target);
|
|
39
|
+
if (fromRoot === '..' || fromRoot.startsWith('..' + path.sep) || path.isAbsolute(fromRoot)) {
|
|
40
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN', 'Default working directory must stay inside the selected project');
|
|
41
|
+
}
|
|
42
|
+
const canonical = fromRoot.split(path.sep).join('/') || '.';
|
|
43
|
+
if (relativeCwd(canonical) !== canonical) throw invalid('The resolved default directory cannot be represented as a stable project-relative setting');
|
|
44
|
+
return canonical;
|
|
45
|
+
}
|
|
46
|
+
function validateDocument(value) {
|
|
47
|
+
if (!exactKeys(value, ['revision', 'defaultProvider', 'providers']) ||
|
|
48
|
+
!Number.isSafeInteger(value.revision) || value.revision < 0 || Object.is(value.revision, -0) ||
|
|
49
|
+
!PROVIDERS.includes(value.defaultProvider) || !exactKeys(value.providers, PROVIDERS)) {
|
|
50
|
+
throw invalid('Agent defaults require a safe revision, a native provider and complete codex, claude and dsh settings');
|
|
51
|
+
}
|
|
52
|
+
const providers = {};
|
|
53
|
+
for (const provider of PROVIDERS) {
|
|
54
|
+
const setting = value.providers[provider];
|
|
55
|
+
if (!exactKeys(setting, ['model', 'cwd'])) throw invalid('Each provider setting accepts only model and cwd');
|
|
56
|
+
providers[provider] = { model: normalizeModel(setting.model), cwd: relativeCwd(setting.cwd) };
|
|
57
|
+
}
|
|
58
|
+
return { revision: value.revision, defaultProvider: value.defaultProvider, providers };
|
|
59
|
+
}
|
|
60
|
+
function storedDefaults(db) {
|
|
61
|
+
const row = db.prepare('SELECT value FROM meta WHERE key=?').get(META_KEY);
|
|
62
|
+
if (!row) return initialDefaults();
|
|
63
|
+
try {
|
|
64
|
+
if (typeof row.value !== 'string' || row.value.length > 16384) throw new Error('invalid settings');
|
|
65
|
+
const value = validateDocument(JSON.parse(row.value));
|
|
66
|
+
if (value.revision === 0) throw new Error('invalid saved revision');
|
|
67
|
+
return value;
|
|
68
|
+
} catch {
|
|
69
|
+
throw new CliError('AGENT_DEFAULTS_INVALID', 'Saved Agent defaults are invalid; inspect the project configuration before saving or creating a worker');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** HCC project settings only. Provider accounts, processes and config files are never touched. */
|
|
74
|
+
export function createAgentDefaults({ connectWebProject }) {
|
|
75
|
+
function readAgentDefaults(ctx) {
|
|
76
|
+
assertRoot(ctx);
|
|
77
|
+
const db = connectWebProject(ctx);
|
|
78
|
+
try { const value = storedDefaults(db); assertRoot(ctx); return value; }
|
|
79
|
+
finally { db.close(); }
|
|
80
|
+
}
|
|
81
|
+
function saveAgentDefaults(ctx, input) {
|
|
82
|
+
const requested = validateDocument(input);
|
|
83
|
+
if (requested.revision === Number.MAX_SAFE_INTEGER) throw invalid('Agent defaults revision is exhausted');
|
|
84
|
+
assertRoot(ctx);
|
|
85
|
+
const db = connectWebProject(ctx);
|
|
86
|
+
try {
|
|
87
|
+
return tx(db, () => {
|
|
88
|
+
assertRoot(ctx);
|
|
89
|
+
const current = storedDefaults(db);
|
|
90
|
+
if (requested.revision !== current.revision) throw new CliError('AGENT_DEFAULTS_CONFLICT', 'Agent defaults changed in another view; reload them before saving your draft');
|
|
91
|
+
const providers = {};
|
|
92
|
+
for (const provider of PROVIDERS) providers[provider] = { model: requested.providers[provider].model,
|
|
93
|
+
cwd: existingCwd(ctx, requested.providers[provider].cwd) };
|
|
94
|
+
const saved = { revision: current.revision + 1, defaultProvider: requested.defaultProvider, providers };
|
|
95
|
+
assertRoot(ctx);
|
|
96
|
+
db.prepare('INSERT INTO meta(key,value) VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value')
|
|
97
|
+
.run(META_KEY, JSON.stringify(saved));
|
|
98
|
+
assertRoot(ctx);
|
|
99
|
+
return saved;
|
|
100
|
+
});
|
|
101
|
+
} finally { db.close(); }
|
|
102
|
+
}
|
|
103
|
+
function resolveNativeDefaults(ctx, input) {
|
|
104
|
+
const saved = readAgentDefaults(ctx), kind = input.kind === undefined ? saved.defaultProvider : input.kind;
|
|
105
|
+
if (!PROVIDERS.includes(kind)) throw invalid('Use native provider codex, claude or dsh');
|
|
106
|
+
const setting = saved.providers[kind], model = input.model === undefined ? setting.model : normalizeModel(input.model);
|
|
107
|
+
return { ...input, kind, cwd: input.cwd === undefined ? setting.cwd : input.cwd, model: model === null ? undefined : model };
|
|
108
|
+
}
|
|
109
|
+
return { readAgentDefaults, saveAgentDefaults, resolveNativeDefaults };
|
|
110
|
+
}
|
package/lib/web/auto-attach.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { adoptionPaused } from './adoption-state.mjs';
|
|
|
11
11
|
|
|
12
12
|
export function createAutoAttach(deps) {
|
|
13
13
|
const {
|
|
14
|
-
ctx, sessions,
|
|
14
|
+
ctx, sessions, isStopping = () => false,
|
|
15
15
|
connectWebProject, now, addEvent,
|
|
16
16
|
reconcileRunningPeerBindings,
|
|
17
17
|
ACTIVE_PEER_TTL, redactedLogText, sessionsForProject,
|
|
@@ -66,6 +66,7 @@ export function createAutoAttach(deps) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function reconcileRunningBindings(projectCtx = ctx, panes = null) {
|
|
69
|
+
if (isStopping()) return null;
|
|
69
70
|
const db = connectWebProject(projectCtx);
|
|
70
71
|
try {
|
|
71
72
|
const tmuxPanes = Array.isArray(panes) ? panes : listTmuxPanesOnce();
|
|
@@ -86,7 +87,7 @@ export function createAutoAttach(deps) {
|
|
|
86
87
|
let autoAttachScanInFlight = false;
|
|
87
88
|
|
|
88
89
|
function scanAndAttachDetectedPeers() {
|
|
89
|
-
if (autoAttachScanInFlight) return;
|
|
90
|
+
if (isStopping() || autoAttachScanInFlight) return;
|
|
90
91
|
autoAttachScanInFlight = true;
|
|
91
92
|
let db = null;
|
|
92
93
|
try {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Server authority; browser drafts/selection/scroll positions are separate. */
|
|
2
|
+
export type Provider = 'codex' | 'claude' | 'dsh' | 'shell';
|
|
3
|
+
export interface Capabilities { send?: boolean; steer?: boolean; interrupt?: boolean; approval?: boolean; commands?: boolean; [name: string]: boolean | undefined }
|
|
4
|
+
export interface Approval { id: string; type: string; turnId?: string; requestId?: string; status?: string }
|
|
5
|
+
export interface Delivery { message_id: string; submission_id?: string; turn_id?: string; state: string; error?: string }
|
|
6
|
+
export interface Item { id: string; type: string; text?: string; status?: string }
|
|
7
|
+
export interface Turn { id: string; status: string; items: Item[] }
|
|
8
|
+
export interface Session { id: string; root: string; type: 'native' | 'app-server' | 'tmux' | 'pty'; kind: Provider; status: string; executor_id?: string; capabilities?: Capabilities }
|
|
9
|
+
export interface PaneView { project: string; session: string | null; draft: string; scrollTop: number; selectedTab: 'conversation' | 'trace' }
|
|
10
|
+
export interface StateSyncFrame { type: 'state_sync'; protocol: 1; mode: 'snapshot' | 'patch'; channel: 'codex' | 'native'; root: string; sessionId: string; executorId: string; generation: string; revision: number; baseRevision?: number; state?: unknown; operations?: unknown[] }
|