@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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Test-only preload for the shim re-entry regression. Record fixed stage names
|
|
2
|
+
// and numeric metadata, never argv, environment values, headers or response data.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import diagnostics from 'node:diagnostics_channel';
|
|
5
|
+
|
|
6
|
+
const MAX_TRACE_BYTES = 16 * 1024;
|
|
7
|
+
const MAX_REENTRY_OUTPUT_BYTES = 4096;
|
|
8
|
+
const COMMANDS = new Set(['shim-ensure', 'find-root', 'peer-attach', 'peer-start']);
|
|
9
|
+
const EVENTS = new Set(['start', 'exit', 'request', 'headers', 'complete', 'error']);
|
|
10
|
+
const ROUTES = new Set(['/api/runtime', '/api/sessions/attach', '/api/sessions']);
|
|
11
|
+
|
|
12
|
+
// Only for this fixture's ordinary, shell-redirected output. A final read may
|
|
13
|
+
// observe a write missed by the last poll, but must not accept a late write.
|
|
14
|
+
// This is not a timing proof for arbitrary or deliberately backdated files.
|
|
15
|
+
export function readOwnedReentryOutputAtDeadline(file, expected, deadline, observedAt = Date.now()) {
|
|
16
|
+
const result = { matched: false, classification: 'missing', observed_at_ms: observedAt,
|
|
17
|
+
mtime_ms: null, size_bytes: null, bytes_read: 0 };
|
|
18
|
+
let fd;
|
|
19
|
+
try {
|
|
20
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
|
|
21
|
+
const before = fs.fstatSync(fd);
|
|
22
|
+
result.mtime_ms = before.mtimeMs;
|
|
23
|
+
result.size_bytes = before.size;
|
|
24
|
+
if (!before.isFile()) { result.classification = 'not_regular'; return result; }
|
|
25
|
+
if (before.size > MAX_REENTRY_OUTPUT_BYTES) { result.classification = 'too_large'; return result; }
|
|
26
|
+
const bytes = Buffer.alloc(before.size);
|
|
27
|
+
result.bytes_read = fs.readSync(fd, bytes, 0, bytes.length, 0);
|
|
28
|
+
const after = fs.fstatSync(fd);
|
|
29
|
+
result.mtime_ms = after.mtimeMs;
|
|
30
|
+
result.size_bytes = after.size;
|
|
31
|
+
if (before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || before.size !== after.size) {
|
|
32
|
+
result.classification = 'changed_during_read';
|
|
33
|
+
} else if (after.mtimeMs > deadline) {
|
|
34
|
+
result.classification = 'late';
|
|
35
|
+
} else if (result.bytes_read !== after.size || bytes.toString('utf8').trim() !== expected) {
|
|
36
|
+
result.classification = 'mismatch';
|
|
37
|
+
} else {
|
|
38
|
+
result.matched = true;
|
|
39
|
+
result.classification = 'matched_before_deadline';
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error.code !== 'ENOENT') result.classification = 'read_error';
|
|
44
|
+
return result;
|
|
45
|
+
} finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function classifyReentryCommand(argv, expectedEntry) {
|
|
49
|
+
if (!expectedEntry || argv[1] !== expectedEntry) return null;
|
|
50
|
+
if (argv[2] === 'shim' && argv[3] === 'ensure') return 'shim-ensure';
|
|
51
|
+
if (argv[2] === 'find-root') return 'find-root';
|
|
52
|
+
if (argv[2] === 'peer' && argv[3] === 'attach') return 'peer-attach';
|
|
53
|
+
if (argv[2] === 'peer' && argv[3] === 'start') return 'peer-start';
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readReentryTrace(file) {
|
|
58
|
+
let fd;
|
|
59
|
+
try {
|
|
60
|
+
fd = fs.openSync(file, 'r');
|
|
61
|
+
const bytes = Buffer.alloc(Math.min(fs.fstatSync(fd).size, MAX_TRACE_BYTES));
|
|
62
|
+
fs.readSync(fd, bytes, 0, bytes.length, 0);
|
|
63
|
+
return bytes.toString('utf8').split('\n').flatMap(line => {
|
|
64
|
+
let row;
|
|
65
|
+
try { row = JSON.parse(line); } catch { return []; }
|
|
66
|
+
if (!COMMANDS.has(row?.command) || !EVENTS.has(row?.event) ||
|
|
67
|
+
!Number.isSafeInteger(row.at) || !Number.isSafeInteger(row.pid)) return [];
|
|
68
|
+
const safe = { at: row.at, pid: row.pid, command: row.command, event: row.event };
|
|
69
|
+
if (ROUTES.has(row.route)) safe.route = row.route;
|
|
70
|
+
if (Number.isInteger(row.status) && row.status >= 0 && row.status <= 599) safe.status = row.status;
|
|
71
|
+
return [safe];
|
|
72
|
+
}).slice(-64);
|
|
73
|
+
} catch { return []; }
|
|
74
|
+
finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const traceFile = process.env.HCC_REGRESSION_REENTRY_TRACE;
|
|
78
|
+
const command = classifyReentryCommand(process.argv, process.env.HCC_REGRESSION_REENTRY_BIN);
|
|
79
|
+
if (traceFile && command) {
|
|
80
|
+
const record = (event, route, status) => {
|
|
81
|
+
let fd;
|
|
82
|
+
try {
|
|
83
|
+
fd = fs.openSync(traceFile, 'a', 0o600);
|
|
84
|
+
if (fs.fstatSync(fd).size >= MAX_TRACE_BYTES) return;
|
|
85
|
+
fs.writeSync(fd, JSON.stringify({ at: Date.now(), pid: process.pid, command, event,
|
|
86
|
+
...(ROUTES.has(route) ? { route } : {}),
|
|
87
|
+
...(Number.isInteger(status) ? { status } : {}) }) + '\n');
|
|
88
|
+
} catch { /* Diagnostics must not change the tested command's result. */ }
|
|
89
|
+
finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
90
|
+
};
|
|
91
|
+
record('start');
|
|
92
|
+
process.once('exit', code => record('exit', undefined, code));
|
|
93
|
+
// The isolated regression runtime uses HTTP/fetch. These passive Undici
|
|
94
|
+
// channels preserve the real request implementation and its original budget.
|
|
95
|
+
for (const [channel, event] of [
|
|
96
|
+
['undici:request:create', 'request'],
|
|
97
|
+
['undici:request:headers', 'headers'],
|
|
98
|
+
['undici:request:trailers', 'complete'],
|
|
99
|
+
['undici:request:error', 'error']
|
|
100
|
+
]) {
|
|
101
|
+
diagnostics.subscribe(channel, ({ request, response }) => {
|
|
102
|
+
if (ROUTES.has(request?.path)) record(event, request.path, response?.statusCode);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Public CI annotations deliberately exclude receipt messages, URLs and paths.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const MAX_RECEIPT_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
const PHASES = new Set(['source-browser', 'installed-package', 'installed-browser']);
|
|
8
|
+
const OUTCOMES = new Set(['success', 'failure', 'skipped', 'cancelled']);
|
|
9
|
+
const SCRIPTS = new Set([
|
|
10
|
+
'web-workbench-acceptance.mjs', 'web-workbench-installed-acceptance.mjs',
|
|
11
|
+
'web-session-tools-checks.mjs', 'web-file-preview-checks.mjs'
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/** Retain one bounded local acceptance-script location, never the surrounding stack. */
|
|
15
|
+
export function acceptanceSourceLocation(stack) {
|
|
16
|
+
if (typeof stack !== 'string' || stack.length > 65536) return 'unavailable';
|
|
17
|
+
for (const line of stack.split('\n').slice(1, 33)) {
|
|
18
|
+
const frame = line.trim();
|
|
19
|
+
if (!frame.startsWith('at ')) continue;
|
|
20
|
+
const location = frame.match(/\(([^()\r\n]+)\)$/)?.[1] || frame.slice(3).replace(/^async /, '');
|
|
21
|
+
const match = location.match(/^(?:file:\/\/\/|\/|[A-Za-z]:[\\/])[^?#\r\n]*[\\/]scripts[\\/]([^\\/:]+):([1-9]\d{0,5}):([1-9]\d{0,5})$/);
|
|
22
|
+
if (match && SCRIPTS.has(match[1])) return `${match[1]}:${Number(match[2])}:${Number(match[3])}`;
|
|
23
|
+
}
|
|
24
|
+
return 'unavailable';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readReceipt(filename) {
|
|
28
|
+
let fd;
|
|
29
|
+
try {
|
|
30
|
+
fd = fs.openSync(filename, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | fs.constants.O_NOFOLLOW);
|
|
31
|
+
const stat = fs.fstatSync(fd);
|
|
32
|
+
if (!stat.isFile() || stat.size > MAX_RECEIPT_BYTES) return { status: 'invalid' };
|
|
33
|
+
const bytes = Buffer.alloc(stat.size);
|
|
34
|
+
if (fs.readSync(fd, bytes, 0, bytes.length, 0) !== bytes.length) return { status: 'invalid' };
|
|
35
|
+
const value = JSON.parse(bytes.toString('utf8'));
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return { status: 'invalid' };
|
|
37
|
+
return { status: 'present', value };
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return { status: error.code === 'ENOENT' ? 'missing' : 'invalid' };
|
|
40
|
+
} finally {
|
|
41
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function annotation(phase, receipt) {
|
|
46
|
+
if (!PHASES.has(phase)) throw new TypeError('Invalid acceptance diagnostics phase');
|
|
47
|
+
const value = receipt.value;
|
|
48
|
+
const result = value?.success === true ? 'success' : value?.success === false ? 'failure' : 'unknown';
|
|
49
|
+
const count = Array.isArray(value?.checks) && value.checks.length <= 10000 ? value.checks.length : 'unknown';
|
|
50
|
+
const location = acceptanceSourceLocation(value?.failure?.stack);
|
|
51
|
+
// Every interpolated string is fixed vocabulary, a bounded number or a
|
|
52
|
+
// whitelisted basename. Receipt text cannot introduce workflow commands.
|
|
53
|
+
return `::error title=Web acceptance diagnostic::phase=${phase} step=failure receipt=${receipt.status} result=${result} completed_checks=${count} source=${location}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function webAcceptanceDiagnostics({ outputDir, sourceOutcome, installedOutcome }) {
|
|
57
|
+
if (typeof outputDir !== 'string' || !outputDir || !OUTCOMES.has(sourceOutcome) || !OUTCOMES.has(installedOutcome)) {
|
|
58
|
+
throw new TypeError('Invalid acceptance diagnostics options');
|
|
59
|
+
}
|
|
60
|
+
const lines = [];
|
|
61
|
+
if (sourceOutcome === 'failure') lines.push(annotation('source-browser', readReceipt(path.join(outputDir, 'source', 'evidence.json'))));
|
|
62
|
+
if (installedOutcome === 'failure') {
|
|
63
|
+
lines.push(annotation('installed-package', readReceipt(path.join(outputDir, 'installed', 'evidence.json'))));
|
|
64
|
+
// The wrapper captures child stderr in a log. Read the child's fixed
|
|
65
|
+
// receipt independently, even when the parent receipt is unavailable.
|
|
66
|
+
const browser = readReceipt(path.join(outputDir, 'installed', 'browser', 'evidence.json'));
|
|
67
|
+
if (browser.status !== 'missing') lines.push(annotation('installed-browser', browser));
|
|
68
|
+
}
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function main(args) {
|
|
73
|
+
try {
|
|
74
|
+
const options = new Map();
|
|
75
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
76
|
+
if (!['--output-dir', '--source-outcome', '--installed-outcome'].includes(args[index]) || !args[index + 1] || options.has(args[index])) {
|
|
77
|
+
throw new TypeError('Invalid acceptance diagnostics arguments');
|
|
78
|
+
}
|
|
79
|
+
options.set(args[index], args[index + 1]);
|
|
80
|
+
}
|
|
81
|
+
const lines = webAcceptanceDiagnostics({ outputDir: options.get('--output-dir'), sourceOutcome: options.get('--source-outcome'), installedOutcome: options.get('--installed-outcome') });
|
|
82
|
+
if (lines.length) process.stdout.write(lines.join('\n') + '\n');
|
|
83
|
+
} catch {
|
|
84
|
+
// A diagnostics failure must not expose a filesystem error or obscure the
|
|
85
|
+
// acceptance step's existing failure with a second thrown exception.
|
|
86
|
+
process.stdout.write('::error title=Web acceptance diagnostic::phase=diagnostics receipt=unavailable\n');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) main(process.argv.slice(2));
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Opt-in rendered acceptance with a synthetic Codex JSON-RPC fixture.
|
|
2
|
+
// No model call, real account read/login, original credential access or publication.
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import net from 'node:net';
|
|
8
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
12
|
+
import { initSchema } from '../lib/db/schema.mjs';
|
|
13
|
+
import { createEventHelpers } from '../lib/db/events.mjs';
|
|
14
|
+
import { createPeerHelpers } from '../lib/core/peers/peer-helpers.mjs';
|
|
15
|
+
import { createPeerBindingStore } from '../lib/db/stores/peers.mjs';
|
|
16
|
+
import { createMessageStore } from '../lib/core/coordination/messages.mjs';
|
|
17
|
+
import { inspectProcessIdentity } from '../lib/process/identity.mjs';
|
|
18
|
+
import { createCodexAdapter } from '../lib/integrations/native/codex.mjs';
|
|
19
|
+
import { startNativeService } from '../lib/runtime/native/service.mjs';
|
|
20
|
+
import { nativeRequest } from '../lib/runtime/native/client.mjs';
|
|
21
|
+
|
|
22
|
+
const args=process.argv.slice(2);
|
|
23
|
+
const option=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback;
|
|
24
|
+
if(!args.includes('--run')) {
|
|
25
|
+
console.log('Usage: node scripts/web-codex-account-acceptance.mjs --run [--output NEW_FILE]');
|
|
26
|
+
console.log('Requires HCC_ACCEPTANCE_PLAYWRIGHT; optional HCC_ACCEPTANCE_CHROME / HCC_ACCEPTANCE_TMUX. Uses isolated synthetic accounts only.');
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
assert.ok(process.env.HCC_ACCEPTANCE_PLAYWRIGHT,'Set HCC_ACCEPTANCE_PLAYWRIGHT to an installed Playwright module');
|
|
30
|
+
const repo=fileURLToPath(new URL('..',import.meta.url));
|
|
31
|
+
const directory=fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(),'hcc-account-web-')));
|
|
32
|
+
fs.chmodSync(directory,0o700);
|
|
33
|
+
const root=path.join(directory,'project'),home=path.join(directory,'home'),bin=path.join(directory,'bin'),codexHome=path.join(directory,'codex-home');
|
|
34
|
+
for(const folder of [root,home,bin,codexHome,path.join(root,'.hello-cc')])fs.mkdirSync(folder,{recursive:true,mode:0o700});
|
|
35
|
+
const output=path.resolve(option('--output',path.join(directory,'evidence.json')));
|
|
36
|
+
assert.ok(!fs.existsSync(output),'Use a new output path; existing receipts are preserved');
|
|
37
|
+
const digest=file=>createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
38
|
+
const protectedHome=process.env.CODEX_HOME||path.join(os.homedir(),'.codex');
|
|
39
|
+
const protectedFiles=[path.join(protectedHome,'config.toml'),path.join(protectedHome,'auth.json'),path.join(repo,'.git/index'),
|
|
40
|
+
...['.zshrc','.bashrc','.bash_profile','.profile'].map(name=>path.join(os.homedir(),name))];
|
|
41
|
+
const protectedHashes=protectedFiles.map(file=>[file,fs.existsSync(file)?digest(file):null]);
|
|
42
|
+
const evidence={startedAt:new Date().toISOString(),sourceRoot:repo,directory,browser:'Browser plugin not available; installed Chrome and Playwright',
|
|
43
|
+
providerMode:'synthetic official App Server JSON-RPC fixture',modelInferenceCalled:false,externalAccountAccessed:false,
|
|
44
|
+
checks:[],screenshots:[],pageErrors:[],consoleErrors:[],consoleWarnings:[],viewports:[{width:1440,height:1000},{width:390,height:844},{width:1280,height:900}],completed:false};
|
|
45
|
+
evidence.sourceFiles=Object.fromEntries(['lib/integrations/codex-account.mjs','lib/integrations/native/codex.mjs','lib/runtime/native/service.mjs',
|
|
46
|
+
'lib/web/codex-app-server.mjs','lib/web/codex-sessions.mjs','lib/web/native-sessions.mjs','lib/web/http-routes.mjs',
|
|
47
|
+
'lib/web/ui-codex-account.mjs','lib/web/ui-codex.mjs','lib/web/ui-native.mjs','scripts/web-codex-account-acceptance.mjs'].map(file=>[file,digest(path.join(repo,file))]));
|
|
48
|
+
const tmux=process.env.HCC_ACCEPTANCE_TMUX||'/opt/homebrew/bin/tmux',socket='hcc-account-'+randomUUID();
|
|
49
|
+
const quote=value=>"'"+value.replaceAll("'","'\\''")+"'";
|
|
50
|
+
fs.writeFileSync(path.join(bin,'tmux'),'#!/bin/sh\nexec '+quote(tmux)+' -L '+quote(socket)+' "$@"\n',{mode:0o700});
|
|
51
|
+
const wireLog=path.join(directory,'wire.jsonl'),fixtureFile=path.join(codexHome,'account-fixture.json');
|
|
52
|
+
const fixture=(account,limits,extra={})=>fs.writeFileSync(fixtureFile,JSON.stringify({account,limits,...extra}),{mode:0o600});
|
|
53
|
+
const signedIn={requiresOpenaiAuth:true,account:{type:'chatgpt',planType:'plus',email:'fixture-private-email',accessToken:'fixture-private-token'}};
|
|
54
|
+
const limits={rateLimits:{limitId:'codex',primary:{usedPercent:42,windowDurationMins:300,resetsAt:1800000000},
|
|
55
|
+
secondary:{usedPercent:10,windowDurationMins:10080},credits:{hasCredits:true,unlimited:false,balance:'fixture-private-balance'}}};
|
|
56
|
+
fixture(signedIn,limits);
|
|
57
|
+
fs.writeFileSync(path.join(bin,'codex'),'#!'+process.execPath+'\n'+String.raw`
|
|
58
|
+
const fs=require('node:fs'),readline=require('node:readline'),path=require('node:path');
|
|
59
|
+
const fixtureFile=path.join(process.env.CODEX_HOME,'account-fixture.json');
|
|
60
|
+
const send=value=>process.stdout.write(JSON.stringify(value)+'\n');
|
|
61
|
+
const thread={id:'fixture-thread-'+process.pid,cwd:process.cwd(),status:{type:'idle'},turns:[]};
|
|
62
|
+
let turn=null;
|
|
63
|
+
readline.createInterface({input:process.stdin}).on('line',line=>{
|
|
64
|
+
const message=JSON.parse(line); if(!message.method || message.id===undefined)return;
|
|
65
|
+
fs.appendFileSync(process.env.HCC_ACCOUNT_FIXTURE_WIRE,JSON.stringify({pid:process.pid,method:message.method,params:message.method.startsWith('account/')?message.params:{}})+'\n');
|
|
66
|
+
const fixture=JSON.parse(fs.readFileSync(fixtureFile,'utf8'));
|
|
67
|
+
let result={};
|
|
68
|
+
if(message.method==='initialize')result={userAgent:'codex-account-fixture',codexHome:process.env.CODEX_HOME};
|
|
69
|
+
else if(['thread/start','thread/resume','thread/read'].includes(message.method))result={thread};
|
|
70
|
+
else if(message.method==='thread/list')result={data:[thread],nextCursor:null};
|
|
71
|
+
else if(message.method==='turn/start') {turn={id:'fixture-turn-'+process.pid,status:'inProgress',items:[]};thread.turns=[turn];thread.status={type:'active',activeFlags:[]};result={turn};}
|
|
72
|
+
else if(message.method==='turn/steer')result={turnId:turn?.id};
|
|
73
|
+
else if(message.method==='turn/interrupt') {if(turn){turn.status='interrupted';thread.status={type:'idle'};send({method:'turn/completed',params:{threadId:thread.id,turn}});}}
|
|
74
|
+
else if(message.method==='account/read')result=fixture.account;
|
|
75
|
+
else if(message.method==='account/rateLimits/read') {
|
|
76
|
+
if(fixture.quotaError){send({id:message.id,error:{code:-32601,message:'fixture-private-error',data:{accessToken:'fixture-private-token'}}});return;}
|
|
77
|
+
result=fixture.limits;
|
|
78
|
+
}
|
|
79
|
+
send({id:message.id,result});
|
|
80
|
+
if(message.method==='turn/start')send({method:'turn/started',params:{threadId:thread.id,turn}});
|
|
81
|
+
});
|
|
82
|
+
`,{mode:0o700});
|
|
83
|
+
const env={...process.env,HOME:home,CODEX_HOME:codexHome,PATH:bin+':'+path.dirname(process.execPath)+':'+process.env.PATH,
|
|
84
|
+
HCC_ACCOUNT_FIXTURE_WIRE:wireLog,HCC_WEB_TOKEN:randomUUID(),HCC_SHIM_ENSURED:'1',HCC_SHIM_NO_ATTACH:'1',HCC_NO_AUTO_INSTALL_TMUX:'1'};
|
|
85
|
+
for(const key of Object.keys(env))if(key.startsWith('HCC_')&&!['HCC_ACCOUNT_FIXTURE_WIRE','HCC_WEB_TOKEN','HCC_SHIM_ENSURED','HCC_SHIM_NO_ATTACH','HCC_NO_AUTO_INSTALL_TMUX'].includes(key))delete env[key];
|
|
86
|
+
function hcc(...params){const result=spawnSync(process.execPath,[path.join(repo,'bin/hcc.mjs'),'--root',root,...params],{env,encoding:'utf8',timeout:20000});assert.equal(result.status,0,result.stderr||result.stdout);return result.stdout;}
|
|
87
|
+
const ctx={root,dbPath:path.join(root,'.hello-cc','mesh.db')};
|
|
88
|
+
const connect=()=>{const db=new DatabaseSync(ctx.dbPath);initSchema(db);return db;};
|
|
89
|
+
const events=createEventHelpers(),bindings=createPeerBindingStore(events),peers=createPeerHelpers({...events,now:()=>Math.floor(Date.now()/1000)});
|
|
90
|
+
const deps={...events,...bindings,...peers,...createMessageStore(events),connect,detectBranch:()=>'',liveProcessIdentity:pid=>inspectProcessIdentity(pid).identity};
|
|
91
|
+
const api=(method,route,body)=>nativeRequest(ctx,method,route,body,{timeoutMs:30000});
|
|
92
|
+
let service,browser,runtimeStarted=false;
|
|
93
|
+
const check=(name,details={})=>evidence.checks.push({name,status:'pass',...details});
|
|
94
|
+
async function screenshot(page,name){const file=path.join(directory,name+'.png');await page.screenshot({path:file});evidence.screenshots.push(file);}
|
|
95
|
+
async function rendered(page,type){await page.waitForFunction(type=>window.hccHandoff?.session?.type===type,type,{timeout:15000});assert.equal(await page.title(),'hello-cc');assert.equal(new URL(page.url()).hostname,'127.0.0.1');assert.match(await page.locator('body').innerText(),/hello-cc/);assert.equal(await page.locator('vite-error-overlay,#webpack-dev-server-client-overlay').count(),0);}
|
|
96
|
+
async function select(page,id){if(await page.evaluate(()=>window.hccHandoff.active)!==id)await page.locator('#sessions [data-id="'+id+'"] .session-select').click();await page.waitForFunction(id=>window.hccHandoff?.active===id,id);}
|
|
97
|
+
try {
|
|
98
|
+
hcc('up','--no-discover','--no-guidance');
|
|
99
|
+
service=await startNativeService(ctx,deps,{adapterFactory:(provider,options)=>createCodexAdapter({...options,binary:path.join(bin,'codex'),env:{...options.env,...env,HCC_PEER:options.env.HCC_PEER}})});
|
|
100
|
+
const worker=await api('POST','/workers',{peer:'account-native',provider:'codex'});
|
|
101
|
+
await api('POST','/send',{peer:'account-native',from:'shell',submissionId:'local_'+randomUUID(),body:'Synthetic long-running local task'});
|
|
102
|
+
let original;
|
|
103
|
+
for(let attempt=0;attempt<100;attempt++){original=await api('GET','/workers/account-native/state');if(original.snapshot.turnId)break;await new Promise(resolve=>setTimeout(resolve,50));}
|
|
104
|
+
assert.ok(original.snapshot.turnId,'the synthetic local task must be active before opening Web');
|
|
105
|
+
check('native worker started before Web',{owner:original.owner,sessionId:worker.sessionId});
|
|
106
|
+
const port=await new Promise(resolve=>{const listener=net.createServer();listener.listen(0,'127.0.0.1',()=>{const port=listener.address().port;listener.close(()=>resolve(port));});});
|
|
107
|
+
hcc('web','--local','--port',String(port),'--no-discover','--no-guidance');runtimeStarted=true;
|
|
108
|
+
const {chromium}=await import(process.env.HCC_ACCEPTANCE_PLAYWRIGHT);
|
|
109
|
+
browser=await chromium.launch({headless:true,...(process.env.HCC_ACCEPTANCE_CHROME?{executablePath:process.env.HCC_ACCEPTANCE_CHROME}:{})});
|
|
110
|
+
const page=await browser.newPage({viewport:{width:1440,height:1000}});
|
|
111
|
+
const observe=page=>{page.on('pageerror',error=>evidence.pageErrors.push(error.message));page.on('console',message=>{if(message.type()==='error')evidence.consoleErrors.push(message.text());if(message.type()==='warning')evidence.consoleWarnings.push(message.text());});};observe(page);
|
|
112
|
+
const url='http://127.0.0.1:'+port+'/';
|
|
113
|
+
await page.goto(url+'?token='+env.HCC_WEB_TOKEN);await select(page,'account-native');await rendered(page,'native');
|
|
114
|
+
await page.locator('#nativeAccountLabel').click();
|
|
115
|
+
await page.waitForFunction(()=>/Signed in|已登录/.test(document.getElementById('nativeAccountSummary').textContent));
|
|
116
|
+
assert.match(await page.locator('#nativeAccountLimits').innerText(),/42%/);await screenshot(page,'native-account-desktop');
|
|
117
|
+
const refreshed=await api('GET','/workers/account-native/state');
|
|
118
|
+
assert.equal(refreshed.owner,original.owner);assert.equal(refreshed.snapshot.sessionId,original.snapshot.sessionId);assert.equal(refreshed.snapshot.turnId,original.snapshot.turnId);
|
|
119
|
+
check('rendered account read preserves the original local executor and active turn');
|
|
120
|
+
const stale=await page.request.get(url+'api/sessions/account-native/native/account?root='+encodeURIComponent(root)+'&generation='+original.generation+'&owner=old-owner&sessionId='+worker.sessionId,{headers:{'X-HCC-API-Version':'2'}});
|
|
121
|
+
assert.equal(stale.status(),409);check('stale native account identity refused');
|
|
122
|
+
const observer=await browser.newPage({viewport:{width:1280,height:900}});observe(observer);await observer.goto(url+'?token='+env.HCC_WEB_TOKEN);await select(observer,'account-native');await rendered(observer,'native');
|
|
123
|
+
assert.equal(await observer.evaluate(()=>window.hccHandoff.canControl),false);await observer.locator('#nativeAccountLabel').click();
|
|
124
|
+
fixture(signedIn,{rateLimits:{...limits.rateLimits,primary:{...limits.rateLimits.primary,usedPercent:61}}});await observer.locator('#nativeAccountRead').click();
|
|
125
|
+
await observer.waitForFunction(()=>document.getElementById('nativeAccountLimits').textContent.includes('61%'));
|
|
126
|
+
assert.equal(await observer.locator('#nativeSend').isEnabled(),false);check('observer account refresh remains read-only');
|
|
127
|
+
await observer.close();
|
|
128
|
+
await page.setViewportSize({width:390,height:844});
|
|
129
|
+
fixture({account:null,requiresOpenaiAuth:true},{});await page.locator('#nativeAccountRead').click();
|
|
130
|
+
await page.waitForFunction(()=>/sign-in required|需要在本地 Codex 登录/.test(document.getElementById('nativeAccountSummary').textContent));
|
|
131
|
+
await page.waitForFunction(()=>!document.getElementById('nativeAccountRead').disabled);
|
|
132
|
+
await page.locator('#nativeAccountLabel').scrollIntoViewIfNeeded();await screenshot(page,'native-account-mobile-login');
|
|
133
|
+
await page.locator('#nativeAccountRead').scrollIntoViewIfNeeded();assert.equal(await page.locator('#nativeAccountRead').isEnabled(),true);
|
|
134
|
+
assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+2));await screenshot(page,'native-account-mobile-actions');check('mobile local-login state remains usable');
|
|
135
|
+
fixture(signedIn,{}, {quotaError:true});await page.locator('#nativeAccountRead').click();
|
|
136
|
+
await page.waitForFunction(()=>/does not provide|未提供订阅限额/.test(document.getElementById('nativeAccountLimits').textContent));
|
|
137
|
+
await page.waitForFunction(()=>!document.getElementById('nativeAccountRead').disabled);
|
|
138
|
+
assert.equal(await page.locator('#nativeAccountLimits progress').count(),0);await screenshot(page,'native-account-mobile-unknown-limits');check('unsupported quota is displayed as unavailable, not zero');
|
|
139
|
+
fixture({account:null,requiresOpenaiAuth:false},{});await page.setViewportSize({width:1440,height:1000});
|
|
140
|
+
const created=await page.evaluate(async root=>window.hccHandoff.api('/api/sessions?root='+encodeURIComponent(root),{method:'POST',body:JSON.stringify({kind:'codex',transport:'app-server'})}),root);
|
|
141
|
+
const webId=created.session.id;
|
|
142
|
+
await page.evaluate(async id=>{await window.hccHandoff.refreshSessions();window.hccHandoff.openManaged(id);},webId);await rendered(page,'app-server');
|
|
143
|
+
await page.locator('#codexAccountLabel').click();await page.waitForFunction(()=>/Provider-managed|供应商自行管理认证/.test(document.getElementById('codexAccountSummary').textContent));
|
|
144
|
+
const webState=await page.evaluate(async({id,root})=>(await window.hccHandoff.api('/api/sessions/'+id+'/codex/state?root='+encodeURIComponent(root))).state,{id:webId,root});
|
|
145
|
+
const wrongExecutor=await page.request.get(url+'api/sessions/'+webId+'/codex/account?root='+encodeURIComponent(root)+'&executorId=old-executor',{headers:{'X-HCC-API-Version':'2'}});
|
|
146
|
+
assert.equal(wrongExecutor.status(),409);await screenshot(page,'web-owned-account-desktop-provider');check('Web-owned custom provider status and stale executor refusal');
|
|
147
|
+
await page.setViewportSize({width:390,height:844});fixture(signedIn,limits);await page.locator('#codexAccountRead').click();
|
|
148
|
+
await page.waitForFunction(()=>document.getElementById('codexAccountLimits').textContent.includes('42%'));
|
|
149
|
+
await page.waitForFunction(()=>!document.getElementById('codexAccountRead').disabled);await page.locator('#codexAccountLabel').scrollIntoViewIfNeeded();
|
|
150
|
+
assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+2));await screenshot(page,'web-owned-account-mobile-limits');check('Web-owned mobile subscription windows render correctly');
|
|
151
|
+
const publicData=await page.evaluate(()=>JSON.stringify({body:document.body.innerText,storage:{...localStorage},sessionStorage:{...sessionStorage}}));
|
|
152
|
+
assert.doesNotMatch(publicData,/fixture-private-/);
|
|
153
|
+
const db=connect();try{assert.doesNotMatch(JSON.stringify(db.prepare('SELECT payload FROM events').all()),/fixture-private-/);}finally{db.close();}
|
|
154
|
+
const accountWire=fs.readFileSync(wireLog,'utf8').split('\n').filter(Boolean).map(JSON.parse).filter(row=>row.method.startsWith('account/'));
|
|
155
|
+
assert.ok(accountWire.length>0);assert.ok(accountWire.every(row=>['account/read','account/rateLimits/read'].includes(row.method)));
|
|
156
|
+
assert.ok(accountWire.filter(row=>row.method==='account/read').every(row=>row.params.refreshToken===false));
|
|
157
|
+
check('no secrets in rendered state/storage/events and no auth mutation on the wire');
|
|
158
|
+
await page.close();await browser.close();browser=null;
|
|
159
|
+
hcc('down');runtimeStarted=false;
|
|
160
|
+
const returned=await api('GET','/workers/account-native/state');assert.equal(returned.owner,original.owner);assert.equal(returned.snapshot.turnId,original.snapshot.turnId);
|
|
161
|
+
check('Web shutdown leaves original native execution alive');
|
|
162
|
+
assert.deepEqual(evidence.pageErrors,[]);assert.deepEqual(evidence.consoleErrors,[]);assert.deepEqual(evidence.consoleWarnings,[]);evidence.completed=true;
|
|
163
|
+
} catch(error) {
|
|
164
|
+
evidence.error=error.message;process.exitCode=1;
|
|
165
|
+
if(browser)for(const context of browser.contexts())for(const page of context.pages())try{await screenshot(page,'failure-'+evidence.screenshots.length);}catch{}
|
|
166
|
+
} finally {
|
|
167
|
+
await browser?.close();
|
|
168
|
+
if(runtimeStarted)try{hcc('down');}catch{}
|
|
169
|
+
try{await service?.shutdown();evidence.nativeClosed=true;}catch(error){evidence.cleanupError=error.message;process.exitCode=1;evidence.completed=false;}
|
|
170
|
+
try{hcc('down');}catch{}
|
|
171
|
+
spawnSync(tmux,['-L',socket,'kill-server'],{stdio:'ignore'});
|
|
172
|
+
evidence.protectedFilesUnchanged=protectedHashes.every(([file,hash])=>(fs.existsSync(file)?digest(file):null)===hash);
|
|
173
|
+
if(!evidence.protectedFilesUnchanged){process.exitCode=1;evidence.completed=false;}
|
|
174
|
+
for(const folder of [root,home,bin,codexHome])fs.rmSync(folder,{recursive:true,force:true});
|
|
175
|
+
evidence.temporaryHomesRemoved=!fs.existsSync(home)&&!fs.existsSync(codexHome);
|
|
176
|
+
evidence.finishedAt=new Date().toISOString();fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,JSON.stringify(evidence,null,2)+'\n',{mode:0o600});
|
|
177
|
+
console.log(JSON.stringify({evidence:output,completed:evidence.completed,checks:evidence.checks.length,error:evidence.error||null,protectedFilesUnchanged:evidence.protectedFilesUnchanged}));
|
|
178
|
+
}
|