carthagent 1.0.7 → 1.1.0-beta.1
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 +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/beta-rewards-e2e-plan.md +48 -0
- package/docs/beta-rewards-e2e-results.md +84 -0
- 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/launch-readiness-live-results.md +65 -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 +336 -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 +175 -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-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-beta.1.md +60 -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.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +19 -6
- 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 +22 -5
- 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 +299 -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/providers/experiential.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 +243 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- 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 +59 -31
- 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 +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +496 -0
- package/plugins/carthagent/src/ui/client.ts +127 -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 +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -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 +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
- package/plugins/carthagent/src/ui/voyages.tsx +131 -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 +64 -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 +269 -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-review.test.mjs +57 -2
- 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 +55 -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 +525 -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-model-command.test.mjs +67 -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 +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-guidance.test.mjs +16 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -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 +397 -0
- package/tests/native-voyages-ui.test.mjs +307 -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-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +597 -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 +231 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +42 -5
- 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 +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +87 -63
- 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,39 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { OPENCODE_VERSION, CTG_RUNTIME_VERSION, readRuntimeReleaseManifest } from '../lib/opencode/runtime.mjs';
|
|
5
|
+
import { REQUIRED_RUNTIME_TARGETS, validateRuntimeBuildInputs } from './verify-owned-runtime-release.mjs';
|
|
6
|
+
|
|
7
|
+
const root = resolve(import.meta.dirname, '..');
|
|
8
|
+
|
|
9
|
+
export async function readManualRuntimeManifest({ runtimeManifest, manifestSha256: expectedDigest } = {}) {
|
|
10
|
+
if (runtimeManifest && !/^[a-f0-9]{64}$/.test(expectedDigest ?? ''))
|
|
11
|
+
throw Error('An explicit runtime manifest requires its independently supplied SHA-256 digest');
|
|
12
|
+
const manifestPath = runtimeManifest ? resolve(runtimeManifest) : join(root, 'lib/opencode/runtime-release.json');
|
|
13
|
+
const bytes = await readFile(manifestPath);
|
|
14
|
+
const manifestSha256 = createHash('sha256').update(bytes).digest('hex');
|
|
15
|
+
if (expectedDigest && manifestSha256 !== expectedDigest)
|
|
16
|
+
throw Error('Runtime manifest SHA-256 checksum mismatch');
|
|
17
|
+
let manifest;
|
|
18
|
+
if (!runtimeManifest) manifest = await readRuntimeReleaseManifest(manifestPath);
|
|
19
|
+
else {
|
|
20
|
+
manifest = JSON.parse(bytes);
|
|
21
|
+
const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
22
|
+
if (manifest?.version !== 1 || manifest.releaseVersion !== version
|
|
23
|
+
|| manifest.compatibilityVersion !== OPENCODE_VERSION || manifest.runtimeVersion !== CTG_RUNTIME_VERSION
|
|
24
|
+
|| !manifest.assets || typeof manifest.assets !== 'object' || Array.isArray(manifest.assets)
|
|
25
|
+
|| !Object.keys(manifest.assets).length)
|
|
26
|
+
throw Error('Invalid or incompatible manual candidate manifest');
|
|
27
|
+
// Explicit candidates can contain only platforms actually built, never invented release digests.
|
|
28
|
+
for (const [target, asset] of Object.entries(manifest.assets)) {
|
|
29
|
+
if (!Object.hasOwn(REQUIRED_RUNTIME_TARGETS, target) || asset?.name !== REQUIRED_RUNTIME_TARGETS[target][2]
|
|
30
|
+
|| !/^[a-f0-9]{64}$/.test(asset.archiveSha256 ?? '') || !/^[a-f0-9]{64}$/.test(asset.executableSha256 ?? ''))
|
|
31
|
+
throw Error(`Invalid manual candidate asset: ${target}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!manifest) throw Error('Runtime release manifest is missing');
|
|
35
|
+
// Published kits keep their immutable release trust root when development
|
|
36
|
+
// advances the recipe. Explicit candidates must still match that recipe.
|
|
37
|
+
if (runtimeManifest) await validateRuntimeBuildInputs(manifest, root);
|
|
38
|
+
return { manifest, manifestPath, manifestSha256 };
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Only the randomized fixture URL grants authority to stop a browser tree.
|
|
2
|
+
export const stopFixtureBrowser = String.raw`
|
|
3
|
+
$ErrorActionPreference = 'Stop'
|
|
4
|
+
if ([string]::IsNullOrEmpty($env:CTG_NATIVE_FIXTURE_URL)) { throw 'Missing fixture browser URL' }
|
|
5
|
+
function Same-Identity($a, $b) {
|
|
6
|
+
return $a.ProcessId -eq $b.ProcessId -and $null -ne $a.CreationDate -and $a.CreationDate -eq $b.CreationDate
|
|
7
|
+
}
|
|
8
|
+
function Is-FixtureRoot($p) {
|
|
9
|
+
return $p.Name -ieq 'msedge.exe' -and $p.CommandLine -and $p.CommandLine.Contains($env:CTG_NATIVE_FIXTURE_URL)
|
|
10
|
+
}
|
|
11
|
+
$snapshot = @(Get-CimInstance Win32_Process)
|
|
12
|
+
$roots = @($snapshot | Where-Object { Is-FixtureRoot $_ })
|
|
13
|
+
$captured = @{}
|
|
14
|
+
foreach ($root in $roots) {
|
|
15
|
+
if ($null -eq $root.CreationDate) { throw "Missing creation identity for fixture browser PID $($root.ProcessId)" }
|
|
16
|
+
$captured[$root.ProcessId] = $root
|
|
17
|
+
}
|
|
18
|
+
# Capture every transitive descendant, including those without the URL.
|
|
19
|
+
do {
|
|
20
|
+
$added = $false
|
|
21
|
+
foreach ($p in $snapshot) {
|
|
22
|
+
if (!$captured.ContainsKey($p.ProcessId) -and $captured.ContainsKey($p.ParentProcessId)) {
|
|
23
|
+
$parent = $captured[$p.ParentProcessId]
|
|
24
|
+
if ($null -eq $p.CreationDate) { throw "Missing creation identity for descendant PID $($p.ProcessId)" }
|
|
25
|
+
# An older process cannot be a child of this incarnation of its parent PID.
|
|
26
|
+
if ($p.CreationDate -ge $parent.CreationDate) {
|
|
27
|
+
$captured[$p.ProcessId] = $p
|
|
28
|
+
$added = $true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} while ($added)
|
|
33
|
+
$diagnostics = @()
|
|
34
|
+
foreach ($root in $roots) {
|
|
35
|
+
$current = @(Get-CimInstance Win32_Process | Where-Object { Same-Identity $_ $root })
|
|
36
|
+
if ($current.Count -eq 0) { continue }
|
|
37
|
+
if (!(Is-FixtureRoot $current[0])) { throw "Fixture browser URL changed for PID $($root.ProcessId)" }
|
|
38
|
+
# Capture native stderr as diagnostics, not a PowerShell terminating error.
|
|
39
|
+
$savedPreference = $ErrorActionPreference
|
|
40
|
+
$ErrorActionPreference = 'Continue'
|
|
41
|
+
$output = @(& taskkill.exe /PID $root.ProcessId /T /F 2>&1)
|
|
42
|
+
$exitCode = $LASTEXITCODE
|
|
43
|
+
$ErrorActionPreference = $savedPreference
|
|
44
|
+
$diagnostics += "PID $($root.ProcessId), taskkill exit $exitCode : $($output -join '; ')"
|
|
45
|
+
}
|
|
46
|
+
# Exit status alone is insufficient: taskkill can report a child error after
|
|
47
|
+
# stopping the root. Conversely, root disappearance does not prove child exit.
|
|
48
|
+
$deadline = [DateTime]::UtcNow.AddSeconds(5)
|
|
49
|
+
do {
|
|
50
|
+
$alive = @(Get-CimInstance Win32_Process | Where-Object {
|
|
51
|
+
$captured.ContainsKey($_.ProcessId) -and (Same-Identity $_ $captured[$_.ProcessId])
|
|
52
|
+
})
|
|
53
|
+
if ($alive.Count -eq 0) { break }
|
|
54
|
+
if ([DateTime]::UtcNow -ge $deadline) {
|
|
55
|
+
throw "Fixture browser identities still alive: $($alive.ProcessId -join ', '); $($diagnostics -join ' | ')"
|
|
56
|
+
}
|
|
57
|
+
Start-Sleep -Milliseconds 100
|
|
58
|
+
} while ($true)
|
|
59
|
+
`;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
// Never emit free-form log text: even an otherwise harmless line can contain
|
|
5
|
+
// credentials, headers, or a partial secret at the bounded buffer's edge.
|
|
6
|
+
export function sanitizedNativeOutputTail(output) {
|
|
7
|
+
return String(output).slice(-16384).split(/\r?\n/).slice(-20).map(line => ({
|
|
8
|
+
level: line.match(/\b(INFO|WARN|ERROR|DEBUG|TRACE)\b/)?.[1] ?? 'unknown',
|
|
9
|
+
markers: ['shutdown', 'stopping', 'stopped', 'closed', 'exit', 'signal', 'timeout', 'error', 'dispose', 'disposing']
|
|
10
|
+
.filter(marker => new RegExp(`\\b${marker}\\b`, 'i').test(line)),
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function fixtureStopDiagnostics({ child, output, elapsedMs }, { run = promisify(execFile), platform = process.platform } = {}) {
|
|
15
|
+
// Snapshot before the read-only OS query; the child may exit during it.
|
|
16
|
+
const evidence = {
|
|
17
|
+
elapsedMs: Math.round(elapsedMs),
|
|
18
|
+
child: { pid: child.pid, exitCode: child.exitCode, signalCode: child.signalCode, killed: child.killed },
|
|
19
|
+
outputTail: sanitizedNativeOutputTail(output),
|
|
20
|
+
};
|
|
21
|
+
if (!Number.isSafeInteger(child.pid) || child.pid <= 0) {
|
|
22
|
+
evidence.processState = { status: 'no-child-pid' };
|
|
23
|
+
return evidence;
|
|
24
|
+
}
|
|
25
|
+
const windows = platform === 'win32';
|
|
26
|
+
const command = windows ? 'powershell.exe' : 'ps';
|
|
27
|
+
const args = windows
|
|
28
|
+
? ['-NoProfile', '-NonInteractive', '-Command', `$p = Get-Process -Id ${child.pid} -ErrorAction SilentlyContinue; if ($null -ne $p) { Write-Output ("pid={0} responding={1}" -f $p.Id, $p.Responding) }`]
|
|
29
|
+
: ['-p', String(child.pid), '-o', 'pid=,ppid=,stat=,etime=,time='];
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await run(command, args, { timeout: 2000, maxBuffer: 4096, windowsHide: true });
|
|
32
|
+
const state = stdout.trim();
|
|
33
|
+
// Only numeric process metadata and fixed state codes; never argv, names,
|
|
34
|
+
// environment, stderr, database contents, or OS command error messages.
|
|
35
|
+
const valid = windows
|
|
36
|
+
? new RegExp(`^pid=${child.pid} responding=(True|False)$`).test(state)
|
|
37
|
+
: new RegExp(`^${child.pid}\\s+\\d+\\s+[A-Za-z<+Ns]+\\s+[\\d:.-]+\\s+[\\d:.]+$`).test(state);
|
|
38
|
+
evidence.processState = { status: !state ? 'not-found' : valid ? 'observed' : 'unrecognized-output', ...(valid ? { state } : {}) };
|
|
39
|
+
} catch (error) {
|
|
40
|
+
evidence.processState = { status: 'query-failed', exitCode: Number.isInteger(error.code) ? error.code : null, timedOut: error.killed === true };
|
|
41
|
+
}
|
|
42
|
+
return evidence;
|
|
43
|
+
}
|
package/tools/native-fixture.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Disposable real native host and loopback provider for acceptance probes. */
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
|
-
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { once } from 'node:events';
|
|
8
|
-
import { randomBytes } from 'node:crypto';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
9
|
+
import { assertCompletePluginSource, assertPluginCondition, resolveFixtureSourceRoot, verifySnapshotIdentity } from './native-paired-guards.mjs';
|
|
10
|
+
import { loadNativeTestRuntime, NATIVE_TEST_COMPATIBILITY_VERSION } from './native-runtime-identity.mjs';
|
|
11
11
|
|
|
12
12
|
const root = resolve(import.meta.dirname, '..');
|
|
13
13
|
|
|
@@ -59,16 +59,104 @@ export function assistantText(response, text) {
|
|
|
59
59
|
})}\n\n`);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
62
|
+
export function streamText(res, text = 'Fixture response complete.') {
|
|
63
|
+
res.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
64
|
+
const chunk = (delta, finish_reason = null) => `data: ${JSON.stringify({
|
|
65
|
+
id: 'fixture', object: 'chat.completion.chunk', created: 1, model: 'fixture',
|
|
66
|
+
choices: [{ index: 0, delta, finish_reason }],
|
|
67
|
+
...(finish_reason ? { usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 } } : {}),
|
|
68
|
+
})}\n\n`;
|
|
69
|
+
res.write(chunk({ role: 'assistant', content: text }));
|
|
70
|
+
res.end(`${chunk({}, 'stop')}data: [DONE]\n\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function resolveFixtureRuntime({ productRuntime, options = {}, env = process.env, packageRoot = root, testRuntimeLoader = loadNativeTestRuntime } = {}) {
|
|
74
|
+
const ownedRoot = env.CTG_OWNED_OPENCODE_RUNTIME;
|
|
75
|
+
const trustedManifestPath = env.CTG_TEST_RUNTIME_RELEASE_MANIFEST ?? join(packageRoot, 'lib/opencode/runtime-release.json');
|
|
76
|
+
|
|
77
|
+
// A developer-selected staging directory is checked by bytes and trusted
|
|
78
|
+
// release metadata before the product resolver can execute --version.
|
|
79
|
+
if (ownedRoot) {
|
|
80
|
+
const expectedManifest = JSON.parse(await readFile(trustedManifestPath, 'utf8'));
|
|
81
|
+
const executableName = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
|
|
82
|
+
const explicitBinary = env.CTG_OPENCODE_BIN ?? join(resolve(ownedRoot), `cli-${process.platform === 'win32' ? 'windows' : process.platform}-${process.arch}`, 'bin', executableName);
|
|
83
|
+
await testRuntimeLoader({ binary: explicitBinary, ownedRoot, expectedManifest, env });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof productRuntime?.resolveOpenCode === 'function' && typeof productRuntime?.runtimeIdentity === 'function') {
|
|
87
|
+
const binary = await productRuntime.resolveOpenCode(options);
|
|
88
|
+
const selected = await productRuntime.runtimeIdentity(binary, options);
|
|
89
|
+
const selectedRoot = ownedRoot ?? inferOwnedRoot(binary);
|
|
90
|
+
if (selected.kind === 'ctg-owned' && !selectedRoot) {
|
|
91
|
+
throw new Error('Product selected an owned runtime without exposing its owned artifact root to the test identity verifier');
|
|
92
|
+
}
|
|
93
|
+
if (selected.kind === 'upstream-compatible' && (selected.version !== NATIVE_TEST_COMPATIBILITY_VERSION || env.CTG_TEST_RUNTIME_SHA256)) {
|
|
94
|
+
throw new Error('Upstream-compatible native test runtime must be exactly 2.0.32 and cannot use CTG_TEST_RUNTIME_SHA256');
|
|
95
|
+
}
|
|
96
|
+
const expectedManifest = selectedRoot ? JSON.parse(await readFile(trustedManifestPath, 'utf8')) : undefined;
|
|
97
|
+
const verified = await testRuntimeLoader({ binary, ownedRoot: selectedRoot, expectedManifest, env });
|
|
98
|
+
const productIdentity = {
|
|
99
|
+
binary: selected.binaryPath,
|
|
100
|
+
binarySha256: selected.binarySha256,
|
|
101
|
+
runtimeVersion: selected.version,
|
|
102
|
+
sourceRevision: selected.sourceRevision,
|
|
103
|
+
patchSha256: selected.patchSha256,
|
|
104
|
+
lockSha256: selected.lockSha256,
|
|
105
|
+
};
|
|
106
|
+
for (const [key, actual] of Object.entries(productIdentity)) {
|
|
107
|
+
const expected = key === 'sourceRevision' ? verified.sourceRevision
|
|
108
|
+
: key === 'patchSha256' ? verified.patchSha256
|
|
109
|
+
: key === 'lockSha256' ? verified.lockSha256 : verified[key];
|
|
110
|
+
if (expected !== undefined && actual !== expected) {
|
|
111
|
+
throw new Error(`Product runtime identity ${key} disagrees with trusted native-test identity: ${actual} != ${expected}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { binary, runtimeIdentity: Object.freeze({
|
|
115
|
+
...verified,
|
|
116
|
+
kind: selected.kind,
|
|
117
|
+
compatibilityVersion: NATIVE_TEST_COMPATIBILITY_VERSION,
|
|
118
|
+
runtimeVersion: selected.version,
|
|
119
|
+
binarySha256: verified.binarySha256 ?? selected.binarySha256 ?? createHash('sha256').update(await readFile(binary)).digest('hex'),
|
|
120
|
+
...(selected.capabilities ? { capabilities: selected.capabilities } : {}),
|
|
121
|
+
}) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const binary = env.CTG_OPENCODE_BIN ?? (ownedRoot
|
|
125
|
+
? join(resolve(ownedRoot), `cli-${process.platform === 'win32' ? 'windows' : process.platform}-${process.arch}`, 'bin', process.platform === 'win32' ? 'opencode.exe' : 'opencode')
|
|
126
|
+
: join(packageRoot, 'node_modules/@opencode/cli/bin/opencode.exe'));
|
|
127
|
+
const expectedManifest = ownedRoot ? JSON.parse(await readFile(trustedManifestPath, 'utf8')) : undefined;
|
|
128
|
+
const identity = await testRuntimeLoader({ binary, ownedRoot, expectedManifest, env });
|
|
129
|
+
return { binary: identity.binary, runtimeIdentity: Object.freeze({
|
|
130
|
+
...identity,
|
|
131
|
+
compatibilityVersion: identity.compatibilityVersion,
|
|
132
|
+
runtimeVersion: identity.runtimeVersion,
|
|
133
|
+
kind: ownedRoot ? 'ctg-owned' : 'upstream-compatible',
|
|
134
|
+
}) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function nativeFixture({ plugin = join(root, 'plugins/carthagent'), pluginOptions, plugins: additionalPlugins = [], pluginExpected = plugin ? 'active' : 'absent', verifyInventory = false, provenance, respond = (_body, res) => streamText(res), env: extraEnv = {}, preserveTemp = false } = {}) {
|
|
138
|
+
const { OpenCode } = await import('@opencode/client');
|
|
139
|
+
const pluginEntries = [
|
|
140
|
+
...(plugin ? [pluginOptions ? { package: resolve(plugin), options: pluginOptions } : resolve(plugin)] : []),
|
|
141
|
+
...additionalPlugins.map(entry => typeof entry === 'string' ? resolve(entry) : { ...entry, package: resolve(entry.package) }),
|
|
142
|
+
];
|
|
143
|
+
const productRuntime = await import('../lib/opencode/runtime.mjs');
|
|
144
|
+
const { binary: runtimeBinary, runtimeIdentity } = await resolveFixtureRuntime({ productRuntime });
|
|
65
145
|
const temp = await mkdtemp(join(tmpdir(), 'ctg-native-fixture-'));
|
|
66
146
|
const cwd = join(temp, 'project');
|
|
147
|
+
const pluginProbeCwd = verifyInventory ? join(temp, 'project-alpha') : cwd;
|
|
67
148
|
const config = join(temp, 'config/opencode');
|
|
68
149
|
const requests = [];
|
|
69
150
|
const providerErrors = [];
|
|
151
|
+
const hostErrors = [];
|
|
70
152
|
let child;
|
|
153
|
+
let childDetached = false;
|
|
71
154
|
let client;
|
|
155
|
+
let baseUrl;
|
|
156
|
+
let password;
|
|
157
|
+
let pluginObservation;
|
|
158
|
+
let pluginInventory;
|
|
159
|
+
let pluginHealth;
|
|
72
160
|
let output = '';
|
|
73
161
|
const provider = createServer((req, res) => {
|
|
74
162
|
void (async () => {
|
|
@@ -83,19 +171,27 @@ export async function nativeFixture({ plugin = join(root, 'plugins/carthagent'),
|
|
|
83
171
|
res.end('Fixture provider failed');
|
|
84
172
|
});
|
|
85
173
|
});
|
|
86
|
-
|
|
174
|
+
password = randomBytes(24).toString('hex');
|
|
87
175
|
const stop = async () => {
|
|
88
176
|
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
89
177
|
const exited = once(child, 'exit');
|
|
90
|
-
|
|
91
|
-
|
|
178
|
+
try {
|
|
179
|
+
if (childDetached) process.kill(-child.pid, 'SIGTERM');
|
|
180
|
+
else child.kill('SIGTERM');
|
|
181
|
+
} catch { child.kill('SIGTERM'); }
|
|
182
|
+
const timer = setTimeout(() => {
|
|
183
|
+
try {
|
|
184
|
+
if (childDetached) process.kill(-child.pid, 'SIGKILL');
|
|
185
|
+
else child.kill('SIGKILL');
|
|
186
|
+
} catch { child.kill('SIGKILL'); }
|
|
187
|
+
}, 5000);
|
|
92
188
|
try { await exited; } finally { clearTimeout(timer); }
|
|
93
189
|
};
|
|
94
|
-
const close = async () => {
|
|
190
|
+
const close = async ({ preserveTemp: keepTemp = preserveTemp } = {}) => {
|
|
95
191
|
await stop();
|
|
96
192
|
provider.closeAllConnections();
|
|
97
193
|
if (provider.listening) await new Promise(done => provider.close(done));
|
|
98
|
-
await rm(temp, { recursive: true, force: true });
|
|
194
|
+
if (!keepTemp) await rm(temp, { recursive: true, force: true });
|
|
99
195
|
};
|
|
100
196
|
const request = (milliseconds = 30_000) => ({ signal: AbortSignal.timeout(milliseconds) });
|
|
101
197
|
const start = async () => {
|
|
@@ -103,16 +199,26 @@ export async function nativeFixture({ plugin = join(root, 'plugins/carthagent'),
|
|
|
103
199
|
output = '';
|
|
104
200
|
const env = Object.fromEntries(['PATH', 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT', 'LANG', 'LC_ALL']
|
|
105
201
|
.filter(key => process.env[key]).map(key => [key, process.env[key]]));
|
|
202
|
+
if (process.env.NODE_OPTIONS) env.NODE_OPTIONS = process.env.NODE_OPTIONS;
|
|
106
203
|
Object.assign(env, {
|
|
107
|
-
|
|
204
|
+
...extraEnv,
|
|
205
|
+
HOME: join(temp, 'home'), USERPROFILE: join(temp, 'home'),
|
|
206
|
+
CARTHAGENT_CODING_AGENT_DIR: join(temp, 'home/.carthagent/agent'),
|
|
207
|
+
PI_CODING_AGENT_DIR: join(temp, 'home/.carthagent/agent'),
|
|
208
|
+
APPDATA: join(temp, 'appdata'), LOCALAPPDATA: join(temp, 'localappdata'),
|
|
108
209
|
XDG_CONFIG_HOME: join(temp, 'config'), XDG_DATA_HOME: join(temp, 'data'),
|
|
109
210
|
XDG_CACHE_HOME: join(temp, 'cache'), XDG_STATE_HOME: join(temp, 'state'),
|
|
110
211
|
OPENCODE_DB: join(temp, 'database.sqlite'), OPENCODE_SERVER_PASSWORD: password,
|
|
111
|
-
|
|
212
|
+
OPENCODE_CONFIG_PROJECT_DISABLE: 'true',
|
|
112
213
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
214
|
+
if (verifyInventory) {
|
|
215
|
+
env.NODE_PATH = join(root, 'node_modules');
|
|
216
|
+
}
|
|
217
|
+
childDetached = !verifyInventory;
|
|
218
|
+
child = spawn(runtimeBinary, ['serve', '--hostname', '127.0.0.1', '--port', '0',
|
|
219
|
+
...(extraEnv.CTG_NATIVE_DEBUG === '1' ? ['--print-logs', '--log-level', 'debug'] : [])],
|
|
220
|
+
{ cwd, env, stdio: ['ignore', 'pipe', 'pipe'], detached: childDetached });
|
|
221
|
+
baseUrl = await new Promise((done, reject) => {
|
|
116
222
|
const timer = setTimeout(() => reject(Error(`Host startup timeout: ${output}`)), 30_000);
|
|
117
223
|
const collect = chunk => {
|
|
118
224
|
output = (output + chunk).slice(-16384);
|
|
@@ -122,20 +228,77 @@ export async function nativeFixture({ plugin = join(root, 'plugins/carthagent'),
|
|
|
122
228
|
child.stdout.on('data', collect);
|
|
123
229
|
child.stderr.on('data', collect);
|
|
124
230
|
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
125
|
-
child.once('exit', code
|
|
231
|
+
child.once('exit', (code, signal) => {
|
|
232
|
+
if (code !== 0) hostErrors.push({ code, signal, output });
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
reject(Error(`Host exited ${code ?? signal}: ${output}`));
|
|
235
|
+
});
|
|
126
236
|
});
|
|
127
237
|
client = OpenCode.make({ baseUrl, headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` } });
|
|
128
238
|
const version = (await client.server.info(request())).version;
|
|
129
|
-
if (version !==
|
|
239
|
+
if (version !== runtimeIdentity.runtimeVersion) {
|
|
240
|
+
throw Error(`Native fixture runtime identity mismatch: expected ${runtimeIdentity.runtimeVersion}, got ${version}`);
|
|
241
|
+
}
|
|
242
|
+
const location = { directory: pluginProbeCwd };
|
|
243
|
+
if (verifyInventory) {
|
|
244
|
+
await client.session.create({ location, model: { providerID: 'ctg-fixture', id: 'fixture' }, title: 'native plugin activation probe' }, request());
|
|
245
|
+
if (pluginExpected === 'active') {
|
|
246
|
+
assertCompletePluginSource(root);
|
|
247
|
+
const { assertWorkspacePluginResolution } = await import('./native-paired-guards.mjs');
|
|
248
|
+
assertWorkspacePluginResolution(root);
|
|
249
|
+
}
|
|
250
|
+
const deadline = Date.now() + 15_000;
|
|
251
|
+
let inventory;
|
|
252
|
+
while (Date.now() < deadline) {
|
|
253
|
+
inventory = await client.plugin.list({ location }, request());
|
|
254
|
+
pluginInventory = inventory.data;
|
|
255
|
+
try {
|
|
256
|
+
pluginObservation = assertPluginCondition(inventory.data, pluginExpected === 'active');
|
|
257
|
+
break;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (inventory.data.some(item => (item?.id === 'carthagent' ||
|
|
260
|
+
(typeof item?.id === 'string' && item.id.startsWith('carthagent.'))) && item.state?.status === 'error') ||
|
|
261
|
+
pluginExpected === 'absent' || Date.now() + 100 >= deadline) throw error;
|
|
262
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!pluginObservation) throw new Error('Timed out waiting for native plugin inventory condition');
|
|
266
|
+
if (pluginExpected === 'active') {
|
|
267
|
+
const probe = await client.rpc.call({ rpcID: 'carthagent', method: 'health', input: {}, location }, request());
|
|
268
|
+
if (probe.output?.hostVersion !== runtimeIdentity.runtimeVersion || probe.output?.protocol !== 1) {
|
|
269
|
+
throw Error(`Native Carthagent health probe failed: ${JSON.stringify(probe.output)}`);
|
|
270
|
+
}
|
|
271
|
+
pluginHealth = { protocol: probe.output.protocol, hostVersion: probe.output.hostVersion, directoryMatches: probe.output.directory === pluginProbeCwd };
|
|
272
|
+
if (!pluginHealth.directoryMatches) throw Error('Native Carthagent health RPC reported an unexpected fixture directory');
|
|
273
|
+
}
|
|
274
|
+
if (provenance) {
|
|
275
|
+
const { assertDependencyPackageIdentity, hashDependencyInstall, hashDependencyPackages } = await import('./native-paired-guards.mjs');
|
|
276
|
+
verifySnapshotIdentity(provenance, provenance, { verifyArtifactFiles: false });
|
|
277
|
+
const binary = runtimeIdentity.binary;
|
|
278
|
+
if (provenance.binarySha256 !== createHash('sha256').update(await readFile(binary)).digest('hex')) {
|
|
279
|
+
throw new Error('Native fixture binary hash does not match selected executable');
|
|
280
|
+
}
|
|
281
|
+
// Baseline fixtures intentionally pass plugin=null; resolve dependency
|
|
282
|
+
// provenance from this immutable execution snapshot in that condition.
|
|
283
|
+
const sourceRoot = resolveFixtureSourceRoot(plugin, root);
|
|
284
|
+
const dependencyRoot = provenance.dependencyNodeModulesRealpath;
|
|
285
|
+
const dependencies = hashDependencyPackages(dependencyRoot, join(sourceRoot, 'package.json'), sourceRoot);
|
|
286
|
+
if (JSON.stringify(dependencies) !== JSON.stringify(provenance.dependencyPackages)) throw new Error('Native fixture dependency package identity mismatch');
|
|
287
|
+
const installIdentity = hashDependencyInstall(dependencyRoot);
|
|
288
|
+
assertDependencyPackageIdentity({ realpath: provenance.dependencyInstallRealpath, treeSha256: provenance.dependencyInstallSha256 }, installIdentity);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
130
291
|
return client;
|
|
131
292
|
};
|
|
132
293
|
try {
|
|
133
|
-
await Promise.all(['project', 'home', 'config/opencode', 'data', 'state', 'cache', 'appdata', 'localappdata']
|
|
294
|
+
await Promise.all(['project', 'project-alpha', 'home', 'config/opencode', 'data', 'state', 'cache', 'appdata', 'localappdata']
|
|
134
295
|
.map(directory => mkdir(join(temp, directory), { recursive: true })));
|
|
135
296
|
provider.listen(0, '127.0.0.1');
|
|
136
297
|
await once(provider, 'listening');
|
|
137
298
|
await writeFile(join(config, 'opencode.json'), JSON.stringify({
|
|
138
|
-
|
|
299
|
+
plugins: pluginEntries,
|
|
300
|
+
update: 'disable', model: 'ctg-fixture/fixture',
|
|
301
|
+
default_agent: 'build',
|
|
139
302
|
providers: { 'ctg-fixture': {
|
|
140
303
|
package: '@opencode/ai/providers/openai-compatible',
|
|
141
304
|
settings: { baseURL: `http://127.0.0.1:${provider.address().port}/v1`, apiKey: 'offline-fixture' },
|
|
@@ -144,11 +307,13 @@ export async function nativeFixture({ plugin = join(root, 'plugins/carthagent'),
|
|
|
144
307
|
}));
|
|
145
308
|
await start();
|
|
146
309
|
} catch (error) {
|
|
147
|
-
await close();
|
|
310
|
+
await close({ preserveTemp: false });
|
|
148
311
|
throw error;
|
|
149
312
|
}
|
|
150
313
|
return {
|
|
151
|
-
temp, cwd, requests, providerErrors, request, start, stop, close,
|
|
314
|
+
temp, cwd, baseUrl, password, requests, providerErrors, hostErrors, request, start, stop, close,
|
|
315
|
+
serverPid: () => child?.pid ?? null,
|
|
316
|
+
pluginObservation, pluginInventory, pluginHealth, provenance, runtimeIdentity,
|
|
152
317
|
model: { providerID: 'ctg-fixture', id: 'fixture' },
|
|
153
318
|
location: { directory: cwd },
|
|
154
319
|
get client() { return client; },
|
|
@@ -156,6 +321,7 @@ export async function nativeFixture({ plugin = join(root, 'plugins/carthagent'),
|
|
|
156
321
|
async outcome(sessionID) {
|
|
157
322
|
await client.session.wait({ sessionID }, request());
|
|
158
323
|
const deadline = Date.now() + 5000;
|
|
324
|
+
// wait resolves before the durable session projection always catches up.
|
|
159
325
|
while (true) {
|
|
160
326
|
const session = await client.session.get({ sessionID }, request());
|
|
161
327
|
if (session.outcome) return session.outcome;
|
|
@@ -163,18 +329,16 @@ export async function nativeFixture({ plugin = join(root, 'plugins/carthagent'),
|
|
|
163
329
|
await new Promise(done => setTimeout(done, 20));
|
|
164
330
|
}
|
|
165
331
|
},
|
|
166
|
-
async active(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
await client.session.create({ location, model:{providerID:'ctg-fixture',id:'fixture'}, title:`${pluginID} activation probe` }, request());
|
|
332
|
+
async active(pluginOrLocation = { directory: cwd }, at = { directory: cwd }) {
|
|
333
|
+
const pluginID = typeof pluginOrLocation === 'string' ? pluginOrLocation : 'carthagent';
|
|
334
|
+
const location = typeof pluginOrLocation === 'string' ? at : pluginOrLocation;
|
|
335
|
+
if (pluginID !== 'carthagent') await client.session.create({ location, model:{providerID:'ctg-fixture',id:'fixture'}, title:`${pluginID} activation probe` }, request());
|
|
172
336
|
const deadline = Date.now() + 15_000;
|
|
173
337
|
while (true) {
|
|
174
338
|
const inventory = await client.plugin.list({ location }, request());
|
|
175
339
|
const plugin = inventory.data.find(item => item.id === pluginID);
|
|
176
340
|
if (plugin?.state.status === 'active') return;
|
|
177
|
-
if (plugin?.state.status === '
|
|
341
|
+
if (plugin?.state.status === 'error') throw Error(`Native fixture plugin failed: ${JSON.stringify(plugin)}\n${output}`);
|
|
178
342
|
if (Date.now() >= deadline) throw Error(`Native plugin ${pluginID} activation timed out: ${JSON.stringify(plugin)}\n${output}`);
|
|
179
343
|
await new Promise(done => setTimeout(done, 50));
|
|
180
344
|
}
|
|
@@ -182,13 +346,8 @@ export async function nativeFixture({ plugin = join(root, 'plugins/carthagent'),
|
|
|
182
346
|
};
|
|
183
347
|
}
|
|
184
348
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
choices: [{ index: 0, delta, finish_reason }],
|
|
190
|
-
...(finish_reason ? { usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 } } : {}),
|
|
191
|
-
})}\n\n`;
|
|
192
|
-
response.write(chunk({ role: 'assistant', content: text }));
|
|
193
|
-
response.end(`${chunk({}, 'stop')}data: [DONE]\n\n`);
|
|
349
|
+
function inferOwnedRoot(binary) {
|
|
350
|
+
const resolved = resolve(binary);
|
|
351
|
+
const match = resolved.match(/^(.*)[\\/]cli-[a-z]+-[a-z0-9]+[\\/]bin[\\/]opencode(?:\.exe)?$/i);
|
|
352
|
+
return match?.[1];
|
|
194
353
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Portable glyph PTY screen and assertion helpers; no OS PTY dependencies."""
|
|
2
|
+
import codecs
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import unicodedata
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
USER_MARKER = 'CTG_GLYPH_PAYLOAD'
|
|
10
|
+
USER_TEXT = USER_MARKER + ' 漢字 café → • − …'
|
|
11
|
+
CHROME_PAIRS = [('▄▀▀▀ ▄▀▀▄', 'C A R T H')]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GlyphScreen:
|
|
15
|
+
"""Small VT screen used by glyph acceptance, with Unicode cell widths."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, rows=32, columns=100):
|
|
18
|
+
self.rows, self.columns = rows, columns
|
|
19
|
+
self.cells = [[' '] * columns for _ in range(rows)]
|
|
20
|
+
self.x = self.y = 0
|
|
21
|
+
self.mode = 'text'
|
|
22
|
+
self.csi = ''
|
|
23
|
+
self.osc = ''
|
|
24
|
+
self.osc_escape = False
|
|
25
|
+
self.title_updates = []
|
|
26
|
+
self.decoder = codecs.getincrementaldecoder('utf8')('replace')
|
|
27
|
+
self.wrapCount = 0
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def char_width(char):
|
|
31
|
+
if unicodedata.combining(char):
|
|
32
|
+
return 0
|
|
33
|
+
return 2 if unicodedata.east_asian_width(char) in ('W', 'F') else 1
|
|
34
|
+
|
|
35
|
+
def feed(self, data):
|
|
36
|
+
for char in self.decoder.decode(data):
|
|
37
|
+
if self.mode == 'osc':
|
|
38
|
+
if char == '\x07' or (self.osc_escape and char == '\\'):
|
|
39
|
+
command, _, title = self.osc.partition(';')
|
|
40
|
+
if command in ('0', '2'):
|
|
41
|
+
self.title_updates.append(title)
|
|
42
|
+
self.osc = ''
|
|
43
|
+
self.mode = 'text'
|
|
44
|
+
elif char != '\x1b':
|
|
45
|
+
self.osc += char
|
|
46
|
+
self.osc_escape = char == '\x1b'
|
|
47
|
+
continue
|
|
48
|
+
if self.mode == 'csi':
|
|
49
|
+
if '@' <= char <= '~':
|
|
50
|
+
self._csi(char, self.csi)
|
|
51
|
+
self.mode, self.csi = 'text', ''
|
|
52
|
+
else:
|
|
53
|
+
self.csi += char
|
|
54
|
+
continue
|
|
55
|
+
if self.mode == 'esc':
|
|
56
|
+
if char == '[':
|
|
57
|
+
self.mode, self.csi = 'csi', ''
|
|
58
|
+
elif char == ']':
|
|
59
|
+
self.mode = 'osc'
|
|
60
|
+
else:
|
|
61
|
+
self.mode = 'text'
|
|
62
|
+
continue
|
|
63
|
+
if char == '\x1b':
|
|
64
|
+
self.mode = 'esc'
|
|
65
|
+
elif char == '\r':
|
|
66
|
+
self.x = 0
|
|
67
|
+
elif char == '\n':
|
|
68
|
+
self.y = min(self.y + 1, self.rows - 1)
|
|
69
|
+
elif char == '\b':
|
|
70
|
+
self.x = max(0, self.x - 1)
|
|
71
|
+
elif char >= ' ':
|
|
72
|
+
width = self.char_width(char)
|
|
73
|
+
if width == 0:
|
|
74
|
+
if self.x > 0:
|
|
75
|
+
previous = min(self.columns - 1, self.x - 1)
|
|
76
|
+
while previous > 0 and self.cells[self.y][previous] == '':
|
|
77
|
+
previous -= 1
|
|
78
|
+
self.cells[self.y][previous] += char
|
|
79
|
+
continue
|
|
80
|
+
if self.x + width > self.columns:
|
|
81
|
+
self.wrapCount += 1
|
|
82
|
+
self.x, self.y = 0, min(self.y + 1, self.rows - 1)
|
|
83
|
+
self.cells[self.y][self.x] = char
|
|
84
|
+
if width == 2 and self.x + 1 < self.columns:
|
|
85
|
+
self.cells[self.y][self.x + 1] = ''
|
|
86
|
+
self.x += width
|
|
87
|
+
|
|
88
|
+
def _csi(self, final, params):
|
|
89
|
+
values = [int(value) if value.isdigit() else 0 for value in params.lstrip('?').split(';')]
|
|
90
|
+
n = values[0] if values and values[0] else 1
|
|
91
|
+
if final in ('H', 'f'):
|
|
92
|
+
self.y = max(0, min(self.rows - 1, (values[0] if values and values[0] else 1) - 1))
|
|
93
|
+
self.x = max(0, min(self.columns - 1, (values[1] if len(values) > 1 and values[1] else 1) - 1))
|
|
94
|
+
elif final == 'A': self.y = max(0, self.y - n)
|
|
95
|
+
elif final == 'B': self.y = min(self.rows - 1, self.y + n)
|
|
96
|
+
elif final == 'C': self.x = min(self.columns - 1, self.x + n)
|
|
97
|
+
elif final == 'D': self.x = max(0, self.x - n)
|
|
98
|
+
elif final == 'K': self.cells[self.y][self.x:] = [' '] * (self.columns - self.x)
|
|
99
|
+
elif final == 'J' and values and values[0] == 2:
|
|
100
|
+
self.cells = [[' '] * self.columns for _ in range(self.rows)]
|
|
101
|
+
self.x = self.y = 0
|
|
102
|
+
|
|
103
|
+
def visible(self):
|
|
104
|
+
return '\n'.join(''.join(row).rstrip() for row in self.cells)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def assert_composer_payload(screen_text, expected=USER_TEXT):
|
|
108
|
+
"""Require the complete ordered payload on a rendered prompt row."""
|
|
109
|
+
pattern = re.compile(r'^\s*[┃|]\s*' + re.escape(expected) + r'\s*$')
|
|
110
|
+
rows = [row for row in screen_text.splitlines() if pattern.fullmatch(row)]
|
|
111
|
+
if not rows:
|
|
112
|
+
raise AssertionError('exact staged user payload is not preserved in the rendered composer row')
|
|
113
|
+
return rows[0]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def child_environment(root, config, mode, source=None):
|
|
117
|
+
"""Build a portable allowlisted child environment, excluding release secrets."""
|
|
118
|
+
source = source if source is not None else os.environ
|
|
119
|
+
root, config = Path(root), Path(config)
|
|
120
|
+
env = {key: source[key] for key in ('PATH', 'LANG', 'LC_ALL') if key in source}
|
|
121
|
+
env.update({'HOME': str(root / 'home'), 'TERM': 'xterm-256color', 'COLORTERM': 'truecolor',
|
|
122
|
+
'XDG_CONFIG_HOME': str(root / 'config'), 'XDG_DATA_HOME': str(root / 'data'),
|
|
123
|
+
'XDG_STATE_HOME': str(root / 'state'), 'XDG_CACHE_HOME': str(root / 'cache'),
|
|
124
|
+
'OPENCODE_DB': str(root / 'db.sqlite'), 'OPENCODE_CONFIG_DIR': str(config),
|
|
125
|
+
'OPENCODE_CONFIG': str(config / 'opencode.json'),
|
|
126
|
+
'OPENCODE_CONFIG_PROJECT_DISABLE': 'true', 'CTG_TUI_GLYPHS': mode})
|
|
127
|
+
return env
|