carthagent 1.0.7 → 1.1.0-preview.12
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/README.md +80 -79
- package/bin/ctg.mjs +14 -732
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +314 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +177 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1171 -0
- package/lib/opencode/cli.mjs +252 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +8 -4
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +298 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/tui/framing.mjs +1 -80
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +197 -0
- package/plugins/carthagent/src/server/delivery.ts +217 -106
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +58 -30
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +39 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +492 -0
- package/plugins/carthagent/src/ui/client.ts +125 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +232 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +31 -0
- package/plugins/carthagent/src/ui/native-review.mjs +154 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +122 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
- package/plugins/carthagent/src/ui/voyages.tsx +127 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +60 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +258 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery.test.mjs +22 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +53 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +426 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +31 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +267 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +75 -0
- package/tests/native-ship-rpc.test.mjs +71 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +395 -0
- package/tests/native-voyages-ui.test.mjs +277 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +546 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +230 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +36 -0
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +43 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +876 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +100 -18
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +83 -61
- package/tools/test-native-outcomes.mjs +3 -1
- package/tools/test-native-package.mjs +206 -38
- package/tools/test-native-performance.mjs +26 -0
- package/tools/test-native-product.mjs +90 -0
- package/tools/test-native-pty.mjs +34 -9
- package/tools/test-native-public-repo.mjs +224 -0
- package/tools/test-native-refresh-reentry.mjs +106 -0
- package/tools/test-native-report.mjs +27 -20
- package/tools/test-native-review.mjs +75 -0
- package/tools/test-native-runtime.mjs +120 -0
- package/tools/test-native-ship-models.mjs +176 -0
- package/tools/test-native-ship-walkthrough.mjs +96 -0
- package/tools/test-native-slash-commands.mjs +125 -0
- package/tools/test-native-slash-commands.py +242 -0
- package/tools/test-native-swe-host.mjs +224 -55
- package/tools/test-native-tasks.mjs +77 -0
- package/tools/test-native-terminal-performance.py +887 -0
- package/tools/test-native-terminal-readiness.sh +85 -0
- package/tools/test-native-terminal-semantics-installed.mjs +38 -0
- package/tools/test-native-visual-clear-installed.mjs +48 -0
- package/tools/test-owned-host-controls.mjs +238 -0
- package/tools/test-owned-opencode-runtime.mjs +35 -0
- package/tools/test-owned-visual-clear-env.py +120 -0
- package/tools/test-owned-visual-clear.py +373 -0
- package/tools/verify-draft-runtime-gate.mjs +138 -0
- package/tools/verify-owned-runtime-release.mjs +114 -0
- package/tools/verify-release-version.mjs +21 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
4
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { once } from 'node:events';
|
|
7
|
+
import { nativeFixture, streamText } from './native-fixture.mjs';
|
|
8
|
+
import { stopService } from '../lib/opencode/service.mjs';
|
|
9
|
+
|
|
10
|
+
const root = resolve(import.meta.dirname, '..');
|
|
11
|
+
let verdict = 'APPROVE';
|
|
12
|
+
let reviews = 0;
|
|
13
|
+
let mutateSource = false;
|
|
14
|
+
const host = await nativeFixture({ respond(body, response) {
|
|
15
|
+
const text = JSON.stringify(body.messages);
|
|
16
|
+
const snapshot = text.match(/Snapshot(?: SHA-256)?: ([a-f0-9]+)/)?.[1];
|
|
17
|
+
const head = text.match(/HEAD: ([a-f0-9]+)/)?.[1];
|
|
18
|
+
if (!snapshot || !head || !text.includes('REVIEW_JSON:')) return streamText(response, 'NATIVE_DEFAULT_EXECUTABLE_OK');
|
|
19
|
+
reviews++;
|
|
20
|
+
if (mutateSource) execFileSync(process.execPath, ['-e', 'require("node:fs").writeFileSync("mutated.txt", "changed during review")'], { cwd: mutationWorkspace });
|
|
21
|
+
const findings = verdict === 'CHANGES-REQUESTED' ? [{ file: 'fixture.txt', line: 1, severity: 'blocking', issue: 'Fixture content needs repair.', fix: 'Repair fixture content.' }] : [];
|
|
22
|
+
return streamText(response, verdict === 'MISSING' ? 'No verdict was supplied.' : `Native reviewer fixture.\nREVIEW_JSON: ${JSON.stringify({ version: 1, snapshot, head, findings })}\nVERDICT: ${verdict}`);
|
|
23
|
+
} });
|
|
24
|
+
const profileRoot = join(host.temp, 'cli-profile');
|
|
25
|
+
const mutationWorkspace = join(host.temp, 'mutation-workspace');
|
|
26
|
+
const env = Object.fromEntries(['PATH', 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT', 'LANG', 'LC_ALL'].filter(key => process.env[key]).map(key => [key, process.env[key]]));
|
|
27
|
+
Object.assign(env, {
|
|
28
|
+
HOME: join(host.temp, 'cli-home'), USERPROFILE: join(host.temp, 'cli-home'),
|
|
29
|
+
CTG_OPENCODE_HOME: profileRoot,
|
|
30
|
+
CARTHAGENT_CONFIG: join(host.temp, 'carthagent-config.json'),
|
|
31
|
+
});
|
|
32
|
+
if (host.runtimeIdentity.kind === 'ctg-owned') env.CTG_OWNED_OPENCODE_RUNTIME = dirname(dirname(dirname(host.runtimeIdentity.binary)));
|
|
33
|
+
else env.CTG_OPENCODE_BIN = host.runtimeIdentity.binary;
|
|
34
|
+
async function cli(args, cwd = host.cwd) {
|
|
35
|
+
const child = spawn(process.execPath, [join(root, 'bin/ctg.mjs'), ...args], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
36
|
+
let output = '';
|
|
37
|
+
child.stdout.on('data', chunk => { output += chunk; });
|
|
38
|
+
child.stderr.on('data', chunk => { output += chunk; });
|
|
39
|
+
const timer = setTimeout(() => child.kill('SIGTERM'), 45_000);
|
|
40
|
+
try { const [code] = await once(child, 'close'); return { code, output }; }
|
|
41
|
+
finally { clearTimeout(timer); }
|
|
42
|
+
}
|
|
43
|
+
function git(cwd, ...args) { return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); }
|
|
44
|
+
async function baseline(cwd) {
|
|
45
|
+
await mkdir(cwd, { recursive: true });
|
|
46
|
+
git(cwd, 'init', '--initial-branch=main');
|
|
47
|
+
await writeFile(join(cwd, 'fixture.txt'), 'baseline\n');
|
|
48
|
+
await writeFile(join(cwd, 'task.txt'), 'Review the exact fixture content.\n');
|
|
49
|
+
git(cwd, 'add', 'fixture.txt', 'task.txt');
|
|
50
|
+
git(cwd, '-c', 'user.name=openhands', '-c', 'user.email=openhands@all-hands.dev', 'commit', '-m', 'Native executable fixture\n\nCo-authored-by: openhands <openhands@all-hands.dev>');
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await mkdir(join(profileRoot, 'config/opencode'), { recursive: true });
|
|
54
|
+
const config = JSON.parse(await readFile(join(host.temp, 'config/opencode/opencode.json'), 'utf8'));
|
|
55
|
+
config.providers.lattice = { ...config.providers['ctg-fixture'], models: { 'gpt-6-luna': config.providers['ctg-fixture'].models.fixture } };
|
|
56
|
+
await writeFile(join(profileRoot, 'config/opencode/opencode.json'), JSON.stringify(config));
|
|
57
|
+
const result = await cli(['-p', '--no-guide', '--no-delivery', '--model', 'ctg-fixture/fixture', 'Native default executable fixture']);
|
|
58
|
+
assert.equal(result.code, 0, result.output);
|
|
59
|
+
assert.match(result.output, /NATIVE_DEFAULT_EXECUTABLE_OK/);
|
|
60
|
+
const models = await cli(['--list-models', 'ctg-fixture']);
|
|
61
|
+
assert.equal(models.code, 0, models.output);
|
|
62
|
+
assert.match(models.output, /fixture/);
|
|
63
|
+
const account = await cli(['account']);
|
|
64
|
+
assert.equal(account.code, 1, account.output);
|
|
65
|
+
assert.match(account.output, /not signed in|connect|login/i);
|
|
66
|
+
await baseline(host.cwd);
|
|
67
|
+
const reviewArgs = ['review', 'workspace', '--request-file', 'task.txt', '--timeout', '20'];
|
|
68
|
+
const approved = await cli(reviewArgs);
|
|
69
|
+
assert.equal(approved.code, 0, approved.output);
|
|
70
|
+
assert.match(approved.output, /VERDICT: APPROVE/);
|
|
71
|
+
const calls = reviews;
|
|
72
|
+
const duplicate = await cli(reviewArgs);
|
|
73
|
+
assert.equal(duplicate.code, 2, duplicate.output);
|
|
74
|
+
assert.match(duplicate.output, /already approved/i);
|
|
75
|
+
assert.equal(reviews, calls);
|
|
76
|
+
await writeFile(join(host.cwd, 'fixture.txt'), 'changed source\n');
|
|
77
|
+
verdict = 'CHANGES-REQUESTED';
|
|
78
|
+
const changed = await cli(reviewArgs);
|
|
79
|
+
assert.equal(changed.code, 1, changed.output);
|
|
80
|
+
assert.match(changed.output, /VERDICT: CHANGES-REQUESTED/);
|
|
81
|
+
await writeFile(join(host.cwd, 'fixture.txt'), 'third source\n');
|
|
82
|
+
verdict = 'MISSING';
|
|
83
|
+
const missing = await cli(reviewArgs);
|
|
84
|
+
assert.equal(missing.code, 2, missing.output);
|
|
85
|
+
assert.match(missing.output, /VERDICT|structured/i);
|
|
86
|
+
const beforeLimit = reviews;
|
|
87
|
+
const limited = await cli(reviewArgs);
|
|
88
|
+
assert.equal(limited.code, 2, limited.output);
|
|
89
|
+
assert.match(limited.output, /round limit/i);
|
|
90
|
+
assert.equal(reviews, beforeLimit);
|
|
91
|
+
await baseline(mutationWorkspace);
|
|
92
|
+
verdict = 'APPROVE';
|
|
93
|
+
mutateSource = true;
|
|
94
|
+
const mutated = await cli(reviewArgs, mutationWorkspace);
|
|
95
|
+
assert.equal(mutated.code, 2, mutated.output);
|
|
96
|
+
assert.match(mutated.output, /source.*(?:changed|moved)|snapshot changed/i);
|
|
97
|
+
const stopped = await cli(['service', 'stop']);
|
|
98
|
+
assert.equal(stopped.code, 0, stopped.output);
|
|
99
|
+
const restarted = await cli(['native', '-p', '--no-guide', '--no-delivery', '--model', 'ctg-fixture/fixture', 'Native alias after service restart']);
|
|
100
|
+
assert.equal(restarted.code, 0, restarted.output);
|
|
101
|
+
assert.match(restarted.output, /NATIVE_DEFAULT_EXECUTABLE_OK/);
|
|
102
|
+
console.log(JSON.stringify({ runtime: host.runtimeIdentity.runtimeVersion, executable: 'default ctg without opt-in', headless: 'passed', modelDiscovery: 'passed', accountFailClosed: 'passed', reviewApprovalChangesAndMalformedVerdict: 'passed', persistedReviewAdmission: 'passed', reviewSourceMutationRejected: 'passed', explicitServiceStopAndRestart: 'passed', reviewRequests: reviews }));
|
|
103
|
+
} finally {
|
|
104
|
+
await stopService({ root: profileRoot }).catch(() => {});
|
|
105
|
+
await host.close();
|
|
106
|
+
}
|
|
@@ -2,19 +2,37 @@
|
|
|
2
2
|
/** Offline real-host test. Uses a disposable HOME and never discovers the user's service. */
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
|
|
5
|
-
import { join, resolve } from 'node:path';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { once } from 'node:events';
|
|
9
9
|
import { randomBytes } from 'node:crypto';
|
|
10
10
|
import { OpenCode } from '@opencode/client';
|
|
11
|
+
import { resolveOpenCode, runtimeIdentity as getRuntimeIdentity } from '../lib/opencode/runtime.mjs';
|
|
11
12
|
import { createNativeFixture, toolCall, assistantText } from './native-fixture.mjs';
|
|
13
|
+
import { loadNativeTestRuntime, preflightNativeTestRuntimeArtifact } from './native-runtime-identity.mjs';
|
|
14
|
+
import { waitForPluginActive } from './native-plugin-readiness.mjs';
|
|
12
15
|
|
|
13
16
|
const root = resolve(import.meta.dirname, '..');
|
|
14
17
|
const plugin = resolve(process.argv[2] ?? join(root, 'plugins/carthagent'));
|
|
15
18
|
const productionPlugin = plugin.endsWith('/.ctg-production-plugin') ? resolve(plugin, '../plugins/carthagent/src/index.ts') : plugin.endsWith('/plugins/carthagent') ? resolve(plugin, 'src/index.ts') : plugin;
|
|
16
19
|
const archiveFixture = resolve(root, 'tests/fixtures/archive-plugin');
|
|
17
|
-
const
|
|
20
|
+
const runtimeOptions = {};
|
|
21
|
+
if (process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST) {
|
|
22
|
+
runtimeOptions.runtimeReleaseManifestPath = process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST;
|
|
23
|
+
}
|
|
24
|
+
if (process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT) {
|
|
25
|
+
await preflightNativeTestRuntimeArtifact({
|
|
26
|
+
ownedRoot: process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT,
|
|
27
|
+
expectedManifestPath: process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST,
|
|
28
|
+
});
|
|
29
|
+
runtimeOptions.acquireOwnedRuntime = async () => process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT;
|
|
30
|
+
}
|
|
31
|
+
const binary = await resolveOpenCode(runtimeOptions);
|
|
32
|
+
const productionIdentity = await getRuntimeIdentity(binary, runtimeOptions);
|
|
33
|
+
const ownedRoot = productionIdentity.kind === 'ctg-owned' ? dirname(dirname(dirname(binary))) : undefined;
|
|
34
|
+
const expectedManifest = ownedRoot ? JSON.parse(await readFile(process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST ?? join(root, 'lib/opencode/runtime-release.json'), 'utf8')) : undefined;
|
|
35
|
+
const runtimeIdentity = await loadNativeTestRuntime({ binary, ownedRoot, expectedManifest });
|
|
18
36
|
const temp = await mkdtemp(join(tmpdir(), 'ctg-host-smoke-'));
|
|
19
37
|
const password = randomBytes(24).toString('hex');
|
|
20
38
|
let child;
|
|
@@ -44,7 +62,7 @@ const Observer = { id: 'carthagent-delivery-observer', methods: { state: {
|
|
|
44
62
|
} }, events: {} };
|
|
45
63
|
export default { id: 'carthagent', async setup(context) {
|
|
46
64
|
const monitored = { ...context,
|
|
47
|
-
tool: { ...context.tool, hook: async (name, callback) => context.tool.hook(name, async event => { if (name === 'execute.before') { const entry = { hook: name, sessionID: event.sessionID, tool: event.tool, id: event.id, messageID: event.messageID }; if (invocations.some(previous => previous.sessionID === entry.sessionID && previous.tool === entry.tool && previous.id === entry.id && previous.messageID === entry.messageID)) duplicateInvocations.push(entry); invocations.push(entry); } await callback(event); }) },
|
|
65
|
+
tool: { ...context.tool, hook: async (name, callback) => context.tool.hook(name, async event => { if (name === 'execute.before') { const entry = { hook: name, sessionID: event.sessionID, tool: event.tool, agent: event.agent, id: event.id, messageID: event.messageID }; if (invocations.some(previous => previous.sessionID === entry.sessionID && previous.tool === entry.tool && previous.id === entry.id && previous.messageID === entry.messageID)) duplicateInvocations.push(entry); invocations.push(entry); } await callback(event); }) },
|
|
48
66
|
shell: { ...context.shell, hook: async (name, callback) => context.shell.hook(name, async event => { if (name === 'create.before') invocations.push({ hook: 'shell.' + name, command: event.command }); await callback(event); }) },
|
|
49
67
|
session: { ...context.session, interrupt: async input => { interrupts.push(input); return context.session.interrupt(input); } },
|
|
50
68
|
};
|
|
@@ -74,6 +92,8 @@ export default { id: 'carthagent', async setup(context) {
|
|
|
74
92
|
} else if (JSON.stringify(latestUser).includes('MIXED_DISABLED')) {
|
|
75
93
|
if (tools.includes('shell') && toolResults === 0) toolCall(response, { name: 'shell', arguments: { command: "printf disabled-session > mixed-disabled.txt" } });
|
|
76
94
|
else assistantText(response, 'DISABLED_SESSION_SHELL_OK');
|
|
95
|
+
} else if (JSON.stringify(latestUser).includes('MODE_SHIP_PARITY')) {
|
|
96
|
+
assistantText(response, 'MODE_SHIP_GUIDANCE_OK');
|
|
77
97
|
} else if (scenario === 'budget' && toolResults >= 6) {
|
|
78
98
|
toolCall(response, { name: 'execute', arguments: { code: 'return await tools.carthagent["delivery_status"]({})' } });
|
|
79
99
|
} else if (tools.includes('execute') && toolResults === 0) {
|
|
@@ -148,26 +168,20 @@ export default { id: 'carthagent', async setup(context) {
|
|
|
148
168
|
// multi-turn offline workflow; give each host call its own bounded signal.
|
|
149
169
|
const request = { signal: AbortSignal.timeout(120_000) };
|
|
150
170
|
const info = await client.server.info(request);
|
|
151
|
-
assert.equal(info.version,
|
|
171
|
+
assert.equal(info.version, runtimeIdentity.runtimeVersion);
|
|
152
172
|
const unauthorized = await fetch(`${endpoint}/api/info`, { signal: request.signal });
|
|
153
173
|
assert.equal(unauthorized.status, 401);
|
|
154
174
|
await client.session.create({ location: { directory: cwd }, title: 'CTG host smoke' }, request);
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
await new Promise((done) => setTimeout(done, 200));
|
|
161
|
-
} while (Date.now() < deadline);
|
|
162
|
-
const nativePlugin = plugins.data.find((entry) => entry.id === 'carthagent');
|
|
163
|
-
const archivePlugin = plugins.data.find((entry) => entry.id === 'ctg-archive-fixture');
|
|
164
|
-
assert.ok(nativePlugin, 'Carthagent plugin must become visible');
|
|
165
|
-
assert.equal(nativePlugin.state.status, 'active');
|
|
166
|
-
assert.equal(archivePlugin?.state.status, 'active', 'archive registration fixture must become visible');
|
|
175
|
+
const location = { directory: cwd };
|
|
176
|
+
const listPlugins = async (timeoutMs) => client.plugin.list({ location }, { signal: AbortSignal.timeout(timeoutMs) });
|
|
177
|
+
const readinessOptions = { listPlugins, timeoutMs: 15_000, hostOutput: () => output };
|
|
178
|
+
const nativePlugin = await waitForPluginActive('carthagent', readinessOptions);
|
|
179
|
+
await waitForPluginActive('ctg-archive-fixture', readinessOptions);
|
|
167
180
|
const health = await client.rpc.call({
|
|
168
181
|
rpcID: 'carthagent', method: 'health', input: {}, location: { directory: cwd },
|
|
169
182
|
}, request);
|
|
170
|
-
|
|
183
|
+
assert.equal(health.output.hostVersion, runtimeIdentity.runtimeVersion);
|
|
184
|
+
console.log(JSON.stringify({ version: info.version, compatibilityVersion: runtimeIdentity.compatibilityVersion, plugin: nativePlugin, health }, null, 2));
|
|
171
185
|
assert.equal(health.output.directory, cwd);
|
|
172
186
|
assert.equal(health.output.protocol, 1);
|
|
173
187
|
await assert.rejects(client.rpc.call({
|
|
@@ -290,7 +304,75 @@ export default { id: 'carthagent', async setup(context) {
|
|
|
290
304
|
assert.ok(disabledDiagnostics.output.budget.limits.tools === 10);
|
|
291
305
|
}
|
|
292
306
|
}
|
|
293
|
-
|
|
307
|
+
if (scenario === 'standard') {
|
|
308
|
+
const agents = (await client.agent.list({ location: { directory: cwd } }, request)).data;
|
|
309
|
+
assert.ok(agents.some(agent => agent.id === 'ship' && agent.name === 'Ship' && agent.mode === 'primary'));
|
|
310
|
+
assert.ok(agents.some(agent => agent.id === 'lite' && agent.name === 'Lite' && agent.mode === 'primary'));
|
|
311
|
+
const modeSession = await client.session.create({ location: { directory: cwd }, title: 'Native mode parity', agent: 'lite' }, request);
|
|
312
|
+
await client.session.switchModel({ sessionID: modeSession.id, model: { providerID: 'fixture', id: 'fixture' } }, request);
|
|
313
|
+
const first = observed.length;
|
|
314
|
+
await client.session.prompt({ sessionID: modeSession.id, text: 'MIXED_DISABLED native mode parity: execute directly.' }, request);
|
|
315
|
+
await client.session.wait({ sessionID: modeSession.id }, { signal: AbortSignal.timeout(45_000) });
|
|
316
|
+
assert.equal(observed.slice(first).some(call => JSON.stringify(call.messages).includes('Standard delivery:')), false, 'Lite has no delivery guidance');
|
|
317
|
+
const liteHistory = await client.message.list({ sessionID: modeSession.id }, request);
|
|
318
|
+
assert.match(JSON.stringify(liteHistory.data), /DISABLED_SESSION_SHELL_OK/);
|
|
319
|
+
await client.session.switchAgent({ sessionID: modeSession.id, agent: 'ship' }, request);
|
|
320
|
+
const second = observed.length;
|
|
321
|
+
await client.session.prompt({ sessionID: modeSession.id, text: 'MODE_SHIP_PARITY inspect the workspace.' }, request);
|
|
322
|
+
await client.session.wait({ sessionID: modeSession.id }, { signal: AbortSignal.timeout(45_000) });
|
|
323
|
+
assert.ok(observed.slice(second).some(call => JSON.stringify(call.messages).includes('Standard delivery:')), 'switching to Ship enables the harness on the next request');
|
|
324
|
+
await client.session.switchAgent({ sessionID: modeSession.id, agent: 'lite' }, request);
|
|
325
|
+
assert.equal((await client.session.get({ sessionID: modeSession.id }, request)).agent, 'lite');
|
|
326
|
+
const beforeCommands = fixture.calls;
|
|
327
|
+
await client.session.command({ sessionID: modeSession.id, name: 'delivery-budget-status', text: '' }, request);
|
|
328
|
+
await client.session.command({ sessionID: modeSession.id, name: 'delivery-budget-reset', text: '' }, request);
|
|
329
|
+
assert.equal(fixture.calls, beforeCommands, 'budget commands must not dispatch provider requests');
|
|
330
|
+
const planHistory = await client.message.list({ sessionID: session.id }, request);
|
|
331
|
+
const snapshots = planHistory.data.flatMap(message => message.type === 'assistant' ? message.content.filter(part => part.type === 'tool' && part.state.status === 'completed' && part.state.metadata?.deliveryPlan) : []);
|
|
332
|
+
assert.ok(snapshots.length > 0, 'real Code Mode results persist delivery plan snapshots for the graph');
|
|
333
|
+
if (process.env.CTG_NATIVE_PLAN_PTY === '1') {
|
|
334
|
+
assert.ok(ownedRoot, 'graph rendering acceptance requires a verified owned runtime');
|
|
335
|
+
const probe = spawn(process.env.PYTHON ?? 'python3', ['-c', `
|
|
336
|
+
import importlib.util, json, os, sys
|
|
337
|
+
from pathlib import Path
|
|
338
|
+
root, binary = sys.argv[1:]
|
|
339
|
+
spec = importlib.util.spec_from_file_location('slash_probe', Path(root) / 'tools/test-native-slash-commands.py')
|
|
340
|
+
module = importlib.util.module_from_spec(spec)
|
|
341
|
+
spec.loader.exec_module(module)
|
|
342
|
+
fixture = json.load(sys.stdin)
|
|
343
|
+
home = Path(fixture['temp']) / 'graph-tui-home'
|
|
344
|
+
env = module.fixture_environment(os.environ, home, '', '')
|
|
345
|
+
env = {key: value for key, value in env.items() if key in module.TUI_ENV_KEYS}
|
|
346
|
+
env['OPENCODE_SERVER_PASSWORD'] = fixture['password']
|
|
347
|
+
env['OPENCODE_SERVER_USERNAME'] = 'opencode'
|
|
348
|
+
tui = module.Tui(binary, fixture, fixture['sessionID'], env)
|
|
349
|
+
try:
|
|
350
|
+
tui.wait_for(lambda: '┃' in tui.screen.visible(), 'session composer')
|
|
351
|
+
if 'getting started' in tui.screen.visible().lower():
|
|
352
|
+
os.write(tui.master, b'\x1b')
|
|
353
|
+
tui.drain(.3)
|
|
354
|
+
tui.submit_slash('/graph', marker='SHIP PLAN')
|
|
355
|
+
tui.wait_for(lambda: 'Create fixture file' in tui.screen.visible(), 'authoritative delivery step in plan graph')
|
|
356
|
+
print(json.dumps({'graph': 'passed', 'source': 'actual Code Mode delivery snapshot', 'step': 'Create fixture file'}))
|
|
357
|
+
finally:
|
|
358
|
+
tui.stop()
|
|
359
|
+
`, root, binary], {
|
|
360
|
+
cwd: root,
|
|
361
|
+
env: Object.fromEntries(['PATH', 'LANG', 'LC_ALL', 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT'].filter(key => process.env[key]).map(key => [key, process.env[key]])),
|
|
362
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
363
|
+
});
|
|
364
|
+
let probeOutput = '';
|
|
365
|
+
probe.stdout.on('data', chunk => { probeOutput += chunk; });
|
|
366
|
+
probe.stderr.on('data', chunk => { probeOutput += chunk; });
|
|
367
|
+
probe.stdin.end(JSON.stringify({ temp, cwd, baseUrl: endpoint, password, sessionID: session.id }));
|
|
368
|
+
const timer = setTimeout(() => probe.kill('SIGTERM'), 45_000);
|
|
369
|
+
const [code] = await once(probe, 'exit');
|
|
370
|
+
clearTimeout(timer);
|
|
371
|
+
assert.equal(code, 0, probeOutput.replaceAll(password, '[redacted]'));
|
|
372
|
+
console.log(probeOutput.trim());
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
console.log(`Native plugin and validated RPC passed against OpenCode ${runtimeIdentity.runtimeVersion} (compatibility ${runtimeIdentity.compatibilityVersion}).`);
|
|
294
376
|
} catch (error) {
|
|
295
377
|
console.error(output.replaceAll(password, '[redacted]'));
|
|
296
378
|
throw error;
|
|
@@ -19,6 +19,10 @@ try {
|
|
|
19
19
|
await host.outcome(unrelated.id);
|
|
20
20
|
const result = await runNativeEvaluation({ client, directory, prompt:'Run bounded fixture task.', provider:model.providerID, model:model.id, timeoutSeconds:30, maxTurns:4, maxCost:1 });
|
|
21
21
|
assert.equal(result.runtime,'opencode-2');
|
|
22
|
+
const hostInfo = await client.server.info(request());
|
|
23
|
+
assert.equal(result.compatibilityVersion,'2.0.32');
|
|
24
|
+
assert.equal(result.runtimeVersion,hostInfo.version,'evaluation records the actual connected server version');
|
|
25
|
+
assert.equal(result.opencodeVersion,hostInfo.version,'legacy version field is not normalized to the compatibility version');
|
|
22
26
|
assert.equal(result.exitCode,0,JSON.stringify(result));
|
|
23
27
|
assert.equal(result.outcome,'succeeded');
|
|
24
28
|
assert.equal(result.turns,1,JSON.stringify(result));
|
|
@@ -36,11 +40,11 @@ try {
|
|
|
36
40
|
assert.equal(failed.turns,1,JSON.stringify(failed));
|
|
37
41
|
assert.equal(failed.productiveTurns,0,JSON.stringify(failed));
|
|
38
42
|
assert.ok(failed.providerErrors >= 1,JSON.stringify(failed));
|
|
39
|
-
assert.equal(host.requests.length,
|
|
43
|
+
assert.equal(host.requests.length,3,'an unrelated native session is outside the evaluation-only budget guard');
|
|
40
44
|
const mismatched = await runNativeEvaluation({client,directory,prompt:'This must fail the budget handshake.',provider:model.providerID,model:model.id,timeoutSeconds:30,maxTurns:5,maxCost:1});
|
|
41
45
|
assert.equal(mismatched.exitCode,1,JSON.stringify(mismatched));
|
|
42
46
|
assert.equal(mismatched.outcome,'failed',JSON.stringify(mismatched));
|
|
43
47
|
assert.ok(mismatched.errors.some(error=>/budget handshake/i.test(error)),JSON.stringify(mismatched));
|
|
44
|
-
assert.equal(host.requests.length,
|
|
45
|
-
console.log(JSON.stringify({host:
|
|
48
|
+
assert.equal(host.requests.length,3,'mismatched turn limit must be rejected before provider admission');
|
|
49
|
+
console.log(JSON.stringify({host:{compatibilityVersion:result.compatibilityVersion,runtimeVersion:result.runtimeVersion},outcome:result.outcome,failureOutcome:failed.outcome,turns:result.turns,cost:result.cost,isolatedFromConcurrentSession:true}));
|
|
46
50
|
} finally { await host.close(); }
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Strictly identified owned-runtime paired glyph rendering proof. */
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { dirname, join, resolve } from 'node:path';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { resolveOpenCode, runtimeIdentity } from '../lib/opencode/runtime.mjs';
|
|
12
|
+
import { loadNativeTestRuntime, preflightNativeTestRuntimeArtifact } from './native-runtime-identity.mjs';
|
|
13
|
+
|
|
14
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
const installedDefault = process.argv.includes('--installed-default');
|
|
16
|
+
assert.ok(!installedDefault || !process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT,
|
|
17
|
+
'installed acceptance must use the package default runtime resolver, not CTG_TEST_RUNTIME_ARTIFACT_ROOT');
|
|
18
|
+
assert.ok(!installedDefault || !process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST,
|
|
19
|
+
'installed acceptance must use its packaged trusted release manifest');
|
|
20
|
+
assert.ok(!installedDefault || !process.env.CTG_OWNED_OPENCODE_RUNTIME,
|
|
21
|
+
'installed acceptance must not select a developer-owned runtime artifact');
|
|
22
|
+
const ownedRoot = process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT ?? process.env.CTG_OWNED_OPENCODE_RUNTIME;
|
|
23
|
+
const trustedManifestPath = process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST ?? join(root, 'lib/opencode/runtime-release.json');
|
|
24
|
+
assert.ok(!process.env.CTG_OPENCODE_BIN, 'Unset CTG_OPENCODE_BIN; this proof uses the production owned-runtime resolver');
|
|
25
|
+
assert.ok(!process.env.CTG_TEST_RUNTIME_SHA256, 'Do not override trusted executable digest in glyph acceptance');
|
|
26
|
+
if (process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT) {
|
|
27
|
+
assert.ok(process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST,
|
|
28
|
+
'Explicit test artifact requires a trusted CTG_TEST_RUNTIME_RELEASE_MANIFEST');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let preflight;
|
|
32
|
+
let runtimeOptions = { runtimeReleaseManifestPath: trustedManifestPath };
|
|
33
|
+
if (ownedRoot) {
|
|
34
|
+
preflight = await preflightNativeTestRuntimeArtifact({ ownedRoot, expectedManifestPath: trustedManifestPath });
|
|
35
|
+
runtimeOptions = { ...runtimeOptions, ownedRuntime: ownedRoot };
|
|
36
|
+
} else {
|
|
37
|
+
// Packaged/default owned-runtime selection is made by the normal resolver.
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const binary = await resolveOpenCode(runtimeOptions);
|
|
41
|
+
const productIdentity = await runtimeIdentity(binary, runtimeOptions);
|
|
42
|
+
assert.equal(productIdentity.kind, 'ctg-owned', 'glyph acceptance requires the production resolver to select CTG-owned runtime');
|
|
43
|
+
const resolvedOwnedRoot = ownedRoot ?? dirname(dirname(dirname(binary)));
|
|
44
|
+
const identity = await loadNativeTestRuntime({
|
|
45
|
+
binary,
|
|
46
|
+
ownedRoot: resolvedOwnedRoot,
|
|
47
|
+
expectedManifest: preflight?.expectedManifest ?? JSON.parse(await readFile(trustedManifestPath, 'utf8')),
|
|
48
|
+
});
|
|
49
|
+
assert.equal(resolve(identity.binary), resolve(binary));
|
|
50
|
+
if (installedDefault) {
|
|
51
|
+
assert.equal(productIdentity.kind, 'ctg-owned', 'installed glyph acceptance requires the default resolver to select CTG-owned runtime');
|
|
52
|
+
assert.equal(identity.binarySha256, productIdentity.binarySha256, 'independent identity must match default resolver byte identity');
|
|
53
|
+
}
|
|
54
|
+
if (preflight) {
|
|
55
|
+
assert.equal(identity.binarySha256, preflight.identity.binarySha256);
|
|
56
|
+
assert.equal(productIdentity.binarySha256, preflight.identity.binarySha256);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const artifact = process.env.CTG_GLYPH_ARTIFACT;
|
|
60
|
+
const scratch = await mkdtemp(join(tmpdir(), 'ctg-glyph-proof-'));
|
|
61
|
+
const ptyArtifact = join(scratch, 'pty.json');
|
|
62
|
+
const pythonCandidates = [process.env.PYTHON,
|
|
63
|
+
join(root, '.venv', process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python'),
|
|
64
|
+
process.platform === 'win32' ? 'python.exe' : 'python3', 'python'].filter(Boolean);
|
|
65
|
+
const findExecutable = candidate => {
|
|
66
|
+
if (existsSync(candidate)) return candidate;
|
|
67
|
+
return (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':')
|
|
68
|
+
.map(directory => join(directory, candidate)).find(existsSync) ?? null;
|
|
69
|
+
};
|
|
70
|
+
const python = pythonCandidates.map(findExecutable).find(Boolean);
|
|
71
|
+
if (!python) throw new Error('Python was not found; set PYTHON to an interpreter path');
|
|
72
|
+
const args = [join(root, 'tools/native-glyph-pty.py'), '--binary', binary];
|
|
73
|
+
args.push('--artifact', ptyArtifact);
|
|
74
|
+
const probeEnv = Object.fromEntries(['PATH', 'LANG', 'LC_ALL'].filter(key => process.env[key])
|
|
75
|
+
.map(key => [key, process.env[key]]));
|
|
76
|
+
const probe = spawnSync(python, args, { cwd: root, encoding: 'utf8', timeout: 120_000, env: probeEnv });
|
|
77
|
+
if (probe.stderr) process.stderr.write(probe.stderr);
|
|
78
|
+
if (probe.status !== 0) {
|
|
79
|
+
if (probe.stdout) process.stderr.write(probe.stdout);
|
|
80
|
+
await rm(scratch, { recursive: true, force: true });
|
|
81
|
+
assert.equal(probe.status, 0, `PTY glyph probe failed with status ${probe.status}`);
|
|
82
|
+
}
|
|
83
|
+
const binaryHash = createHash('sha256').update(await readFile(binary)).digest('hex');
|
|
84
|
+
const proof = {
|
|
85
|
+
runtime: { binary, binarySha256: binaryHash, compatibilityVersion: identity.compatibilityVersion,
|
|
86
|
+
runtimeVersion: identity.runtimeVersion, sourceRevision: identity.sourceRevision,
|
|
87
|
+
patchSha256: identity.patchSha256, lockSha256: identity.lockSha256,
|
|
88
|
+
productResolverKind: productIdentity.kind, productResolverVersion: productIdentity.version },
|
|
89
|
+
glyphProof: JSON.parse(await readFile(ptyArtifact, 'utf8')),
|
|
90
|
+
};
|
|
91
|
+
const rendered = JSON.stringify(proof, null, 2) + '\n';
|
|
92
|
+
process.stdout.write(rendered);
|
|
93
|
+
if (artifact) await (await import('node:fs/promises')).writeFile(resolve(artifact), rendered, 'utf8');
|
|
94
|
+
await rm(scratch, { recursive: true, force: true });
|