carthagent 1.0.7 → 1.1.0-preview.13
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/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 +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 +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-combined-v5.patch +13965 -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.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 +11 -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 +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 +211 -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 +223 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +496 -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 +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 +142 -0
- package/plugins/carthagent/src/ui/voyages.tsx +129 -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 +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 +11 -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 +451 -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-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 +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-delivery.test.mjs +16 -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/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
package/lib/opencode/runtime.mjs
CHANGED
|
@@ -1,13 +1,47 @@
|
|
|
1
|
-
import { chmod, mkdir, open, readFile, rename, rm, stat } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { chmod, lstat, mkdir, open, opendir, readFile, realpath, rename, rm, stat, unlink } from 'node:fs/promises';
|
|
2
|
+
import { constants, readFileSync } from 'node:fs';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { arch, homedir, platform } from 'node:os';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import path from 'node:path';
|
|
6
8
|
import { spawn } from 'node:child_process';
|
|
9
|
+
import { acquireOwnedRuntime } from './runtime-artifact.mjs';
|
|
10
|
+
import { createRuntimeGithubFetch } from './runtime-github-transport.mjs';
|
|
7
11
|
|
|
8
12
|
export const OPENCODE_VERSION = '2.0.32';
|
|
13
|
+
export const CTG_RUNTIME_VERSION = '0.0.0-ctg.2.0.32';
|
|
14
|
+
export const CTG_RUNTIME_SOURCE = '843a6c514e606b436a7fe713422ace0a93ad3d61';
|
|
15
|
+
export const CTG_RUNTIME_LOCK_SHA256 = '9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d';
|
|
16
|
+
export const CTG_RUNTIME_PATCH_SHA256 = 'ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975';
|
|
17
|
+
// Update only this development pin after reviewing a new checked-in patch and
|
|
18
|
+
// script/ctg/prepare-development-runtime.sh. The published release pin above,
|
|
19
|
+
// its build recipe, and the release manifest stay immutable until a separately
|
|
20
|
+
// verified runtime release is promoted.
|
|
21
|
+
export const CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256 = 'ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975';
|
|
9
22
|
const DEFAULT_ROOT = path.join(homedir(), '.carthagent', 'opencode');
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
10
24
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
25
|
+
const RUNTIME_RELEASE_MANIFEST = path.join(PACKAGE_ROOT, 'lib', 'opencode', 'runtime-release.json');
|
|
26
|
+
// Tracks the verified cache selected by the packaged release path without
|
|
27
|
+
// turning it into the separate, explicitly trusted staging selector.
|
|
28
|
+
const RESOLVED_OWNED_RUNTIME = Symbol('resolvedOwnedRuntime');
|
|
29
|
+
|
|
30
|
+
export function resolvePathCommand(command, env = process.env, hostPlatform = platform()) {
|
|
31
|
+
const entries = (env.PATH ?? env.Path ?? '').split(path.delimiter).filter(Boolean);
|
|
32
|
+
const extensions = hostPlatform === 'win32' && !path.extname(command)
|
|
33
|
+
? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';')
|
|
34
|
+
: [''];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
for (const extension of extensions) {
|
|
37
|
+
const candidate = path.resolve(entry, `${command}${extension}`);
|
|
38
|
+
try {
|
|
39
|
+
const details = require('node:fs').statSync(candidate);
|
|
40
|
+
if (details.isFile() && (hostPlatform === 'win32' || (details.mode & 0o111) !== 0)) return candidate;
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
11
45
|
|
|
12
46
|
function abs(value) { return path.resolve(value); }
|
|
13
47
|
|
|
@@ -34,31 +68,202 @@ export function runtimePaths(options = {}) {
|
|
|
34
68
|
|
|
35
69
|
/** Find a CTG/OpenCode executable without downloading or replacing user binaries. */
|
|
36
70
|
export async function resolveOpenCode(options = {}) {
|
|
71
|
+
const owned = options.ownedRuntime ?? process.env.CTG_OWNED_OPENCODE_RUNTIME;
|
|
37
72
|
const explicit = options.binary ?? process.env.CTG_OPENCODE_BIN;
|
|
38
|
-
|
|
73
|
+
if (!explicit && owned) return resolveOwnedOpenCode(owned, options);
|
|
74
|
+
if (!explicit) {
|
|
75
|
+
const release = await readRuntimeReleaseManifest(options.runtimeReleaseManifestPath ?? RUNTIME_RELEASE_MANIFEST);
|
|
76
|
+
if (release) {
|
|
77
|
+
const key = `${platform() === 'win32' ? 'windows' : platform()}-${arch()}`;
|
|
78
|
+
const asset = release.assets[key];
|
|
79
|
+
if (!asset) throw new Error(`This CTG release does not provide its owned Carthagent runtime for ${key}; no upstream or system fallback was attempted.`);
|
|
80
|
+
const root = await (options.acquireOwnedRuntime ?? acquireOwnedRuntime)({
|
|
81
|
+
releaseVersion: release.releaseVersion,
|
|
82
|
+
platform: platform() === 'win32' ? 'windows' : platform(),
|
|
83
|
+
arch: arch(),
|
|
84
|
+
assetName: asset.name,
|
|
85
|
+
expectedArchiveSha256: asset.archiveSha256,
|
|
86
|
+
expectedExecutableSha256: asset.executableSha256,
|
|
87
|
+
expectedSourceRevision: release.sourceRevision,
|
|
88
|
+
expectedPatchSha256: release.patchSha256,
|
|
89
|
+
expectedLockSha256: release.lockSha256,
|
|
90
|
+
expectedCompatibilityVersion: release.compatibilityVersion,
|
|
91
|
+
expectedRuntimeVersion: release.runtimeVersion,
|
|
92
|
+
expectedChannel: `ctg.${release.compatibilityVersion}`,
|
|
93
|
+
baseUrl: options.runtimeReleaseBaseUrl ?? `https://github.com/B-icy/Carthagent/releases/download/v${release.releaseVersion}`,
|
|
94
|
+
cacheRoot: options.runtimeCacheRoot,
|
|
95
|
+
fetchImpl: process.env.CTG_RUNTIME_GITHUB_TOKEN
|
|
96
|
+
? createRuntimeGithubFetch({ token: process.env.CTG_RUNTIME_GITHUB_TOKEN, fetchImpl: options.fetchImpl ?? globalThis.fetch })
|
|
97
|
+
: options.fetchImpl,
|
|
98
|
+
signal: options.signal,
|
|
99
|
+
});
|
|
100
|
+
// Automatic acquisition never opts into development provenance, even
|
|
101
|
+
// when a caller supplies an acquisition implementation or cached root.
|
|
102
|
+
const binary = await resolvePinnedOwnedOpenCode(root, options, false, asset.executableSha256);
|
|
103
|
+
// Preserve the selection for runtimeIdentity(), but do not put it in
|
|
104
|
+
// ownedRuntime: that is an explicit developer/staging trust selector.
|
|
105
|
+
// Repeated resolution must pass through acquisition again so its pinned
|
|
106
|
+
// executable digest is rechecked even if the cache and SHA256SUMS changed.
|
|
107
|
+
Object.defineProperty(options, RESOLVED_OWNED_RUNTIME, {
|
|
108
|
+
configurable: true,
|
|
109
|
+
value: root,
|
|
110
|
+
});
|
|
111
|
+
return binary;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
let packaged;
|
|
115
|
+
if (!explicit) {
|
|
116
|
+
try {
|
|
117
|
+
const packageJson = require.resolve('@opencode/cli/package.json');
|
|
118
|
+
const metadata = JSON.parse(await readFile(packageJson, 'utf8'));
|
|
119
|
+
const executable = typeof metadata.bin === 'string'
|
|
120
|
+
? metadata.bin
|
|
121
|
+
: metadata.bin?.opencode;
|
|
122
|
+
if (typeof executable !== 'string') throw new Error('package has no opencode binary entry');
|
|
123
|
+
packaged = path.resolve(path.dirname(packageJson), executable);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error(`Packaged Carthagent runtime ${OPENCODE_VERSION} dependency could not be resolved from CTG: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
39
128
|
const candidates = explicit ? [explicit] : [packaged];
|
|
40
129
|
for (const candidate of candidates) {
|
|
41
130
|
if (candidate.includes(path.sep) || candidate.includes('/')) {
|
|
42
131
|
try {
|
|
43
|
-
const result =
|
|
132
|
+
const result = options.spawnImpl
|
|
133
|
+
? await runCapture(candidate, ['--version'], { timeout: 5000, spawnImpl: options.spawnImpl })
|
|
134
|
+
: await runCapture(candidate, ['--version'], { timeout: 5000 });
|
|
44
135
|
const version = result.stdout.trim().match(/(?:opencode\s+)?v?(\d+\.\d+\.\d+)/i)?.[1];
|
|
45
|
-
if (version !== OPENCODE_VERSION) throw new Error(`
|
|
136
|
+
if (version !== OPENCODE_VERSION) throw new Error(`Carthagent runtime ${OPENCODE_VERSION} is required; found ${version ?? 'unknown'} at ${candidate}`);
|
|
46
137
|
return abs(candidate);
|
|
47
138
|
} catch (error) {
|
|
48
139
|
if (explicit || /is required/.test(error.message)) throw error;
|
|
49
140
|
}
|
|
50
|
-
|
|
141
|
+
} else {
|
|
51
142
|
try {
|
|
52
|
-
const result = await runCapture(candidate, ['--version'], { timeout: 5000, spawnImpl: options.spawnImpl });
|
|
143
|
+
const result = await runCapture(candidate, ['--version'], { timeout: 5000, spawnImpl: options.spawnImpl, env: options.env });
|
|
53
144
|
const version = result.stdout.trim().match(/(?:opencode\s+)?v?(\d+\.\d+\.\d+)/i)?.[1];
|
|
54
|
-
if (version !== OPENCODE_VERSION) throw new Error(`
|
|
55
|
-
|
|
145
|
+
if (version !== OPENCODE_VERSION) throw new Error(`Carthagent runtime ${OPENCODE_VERSION} is required; found ${version ?? 'unknown'} on PATH`);
|
|
146
|
+
const resolved = resolvePathCommand(candidate, options.env ?? process.env);
|
|
147
|
+
if (!resolved) throw new Error(`${candidate} was not found on PATH`);
|
|
148
|
+
return await realpath(resolved);
|
|
56
149
|
} catch (error) {
|
|
57
150
|
if (explicit || /is required/.test(error.message)) throw error;
|
|
58
151
|
}
|
|
59
152
|
}
|
|
60
153
|
}
|
|
61
|
-
throw new Error(`Packaged
|
|
154
|
+
throw new Error(`Packaged Carthagent runtime ${OPENCODE_VERSION} was not found at ${packaged}. Reinstall CTG or set CTG_OPENCODE_BIN explicitly; CTG will not use or replace another Carthagent runtime installation.`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Read and strictly validate the package-pinned release trust root. */
|
|
158
|
+
export async function readRuntimeReleaseManifest(file = RUNTIME_RELEASE_MANIFEST) {
|
|
159
|
+
let manifest;
|
|
160
|
+
try { manifest = JSON.parse(await readFile(file, 'utf8')); }
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error.code === 'ENOENT') return undefined;
|
|
163
|
+
throw new Error(`Invalid CTG Carthagent runtime release manifest at ${file}: ${error.message}`);
|
|
164
|
+
}
|
|
165
|
+
const invalid = () => { throw new Error(`Invalid or incompatible CTG Carthagent runtime release manifest at ${file}`); };
|
|
166
|
+
let packageMetadata;
|
|
167
|
+
try { packageMetadata = JSON.parse(await readFile(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')); }
|
|
168
|
+
catch (error) { throw new Error(`Could not verify CTG package version for runtime release manifest: ${error.message}`); }
|
|
169
|
+
if (!manifest || manifest.version !== 1
|
|
170
|
+
|| !isReleaseVersion(manifest.releaseVersion)
|
|
171
|
+
|| manifest.releaseVersion !== packageMetadata.version
|
|
172
|
+
|| manifest.compatibilityVersion !== OPENCODE_VERSION
|
|
173
|
+
|| manifest.runtimeVersion !== CTG_RUNTIME_VERSION
|
|
174
|
+
|| manifest.sourceRevision !== CTG_RUNTIME_SOURCE
|
|
175
|
+
|| manifest.patchSha256 !== CTG_RUNTIME_PATCH_SHA256
|
|
176
|
+
|| manifest.lockSha256 !== CTG_RUNTIME_LOCK_SHA256
|
|
177
|
+
|| !manifest.assets || typeof manifest.assets !== 'object' || Array.isArray(manifest.assets)) invalid();
|
|
178
|
+
const allowed = new Set(['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64']);
|
|
179
|
+
for (const [key, asset] of Object.entries(manifest.assets)) {
|
|
180
|
+
if (!allowed.has(key) || !asset || typeof asset !== 'object'
|
|
181
|
+
|| asset.name !== `opencode-${key}.tar.gz`
|
|
182
|
+
|| !isSha256(asset.archiveSha256) || !isSha256(asset.executableSha256)) invalid();
|
|
183
|
+
}
|
|
184
|
+
for (const key of allowed) if (!manifest.assets[key]) invalid();
|
|
185
|
+
if (Object.keys(manifest.assets).length !== allowed.size) invalid();
|
|
186
|
+
return manifest;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isReleaseVersion(value) {
|
|
190
|
+
return typeof value === 'string' && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
|
|
191
|
+
}
|
|
192
|
+
function isSha256(value) { return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value); }
|
|
193
|
+
|
|
194
|
+
/** Explicit developer/staging selector: accept release or checked-in development provenance. */
|
|
195
|
+
export async function resolveOwnedOpenCode(directory, options = {}) {
|
|
196
|
+
return resolvePinnedOwnedOpenCode(directory, options, true);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The trust choice is internal, not an option or adjacent-manifest override.
|
|
200
|
+
async function resolvePinnedOwnedOpenCode(directory, options, allowDevelopment, expectedExecutableSha256) {
|
|
201
|
+
const root = abs(directory);
|
|
202
|
+
const manifestPath = path.join(root, 'ctg-runtime.json');
|
|
203
|
+
let manifest;
|
|
204
|
+
try { manifest = JSON.parse(await readFile(manifestPath, 'utf8')); }
|
|
205
|
+
catch (error) { throw new Error(`Invalid CTG-owned Carthagent runtime manifest at ${manifestPath}: ${error.message}`); }
|
|
206
|
+
if (!manifest || manifest.owner !== 'Carthagent'
|
|
207
|
+
|| manifest.sourceRepository !== 'https://github.com/anomalyco/opencode'
|
|
208
|
+
|| manifest.sourceRevision !== CTG_RUNTIME_SOURCE
|
|
209
|
+
|| manifest.lockSha256 !== CTG_RUNTIME_LOCK_SHA256
|
|
210
|
+
|| (manifest.patchSha256 !== CTG_RUNTIME_PATCH_SHA256
|
|
211
|
+
&& !(allowDevelopment && manifest.patchSha256 === CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256))
|
|
212
|
+
|| manifest.compatibilityVersion !== OPENCODE_VERSION
|
|
213
|
+
|| manifest.runtimeVersion !== CTG_RUNTIME_VERSION
|
|
214
|
+
|| manifest.channel !== 'ctg.2.0.32'
|
|
215
|
+
|| manifest.webUI !== false) {
|
|
216
|
+
throw new Error(`CTG-owned Carthagent runtime manifest is not compatible with the pinned CTG ${OPENCODE_VERSION} runtime at ${manifestPath}`);
|
|
217
|
+
}
|
|
218
|
+
const platformName = platform() === 'win32' ? 'windows' : platform();
|
|
219
|
+
const supportedPlatform = `${platformName}-${arch()}`;
|
|
220
|
+
if (manifest.platform !== supportedPlatform) {
|
|
221
|
+
throw new Error(`CTG-owned Carthagent runtime platform ${manifest.platform} does not match this host ${supportedPlatform}`);
|
|
222
|
+
}
|
|
223
|
+
const artifactPlatform = manifest.platform;
|
|
224
|
+
const executable = path.join(root, `cli-${artifactPlatform}`, 'bin', platform() === 'win32' ? 'opencode.exe' : 'opencode');
|
|
225
|
+
const checksumFile = path.join(root, 'SHA256SUMS');
|
|
226
|
+
const relativeExecutable = path.relative(root, executable).split(path.sep).join('/');
|
|
227
|
+
let sums;
|
|
228
|
+
try { sums = await readFile(checksumFile, 'utf8'); }
|
|
229
|
+
catch (error) { throw new Error(`CTG-owned Carthagent runtime checksums are missing at ${checksumFile}: ${error.message}`); }
|
|
230
|
+
const expected = sums.split(/\r?\n/).map(line => line.trim().split(/\s+/)).find(([hash, name]) => /^[a-f0-9]{64}$/i.test(hash || '') && name?.replace(/^\*?/, '') === relativeExecutable)?.[0];
|
|
231
|
+
if (!expected) throw new Error(`CTG-owned Carthagent runtime checksum does not list ${relativeExecutable}`);
|
|
232
|
+
let bytes;
|
|
233
|
+
try { bytes = await readFile(executable); }
|
|
234
|
+
catch (error) { throw new Error(`CTG-owned Carthagent runtime executable is missing at ${executable}: ${error.message}`); }
|
|
235
|
+
const actual = createHash('sha256').update(bytes).digest('hex');
|
|
236
|
+
if (actual !== expected.toLowerCase()) throw new Error(`CTG-owned Carthagent runtime executable SHA-256 mismatch at ${executable}`);
|
|
237
|
+
if (expectedExecutableSha256 !== undefined && actual !== expectedExecutableSha256) {
|
|
238
|
+
throw new Error(`CTG-owned Carthagent runtime pinned executable SHA-256 mismatch at ${executable}`);
|
|
239
|
+
}
|
|
240
|
+
const result = await runCapture(executable, ['--version'], { timeout: 5000, spawnImpl: options.spawnImpl });
|
|
241
|
+
if (result.stdout.trim() !== `opencode v${CTG_RUNTIME_VERSION}`) {
|
|
242
|
+
throw new Error(`CTG-owned Carthagent runtime identity mismatch at ${executable}: ${result.stdout.trim() || 'unknown version'}`);
|
|
243
|
+
}
|
|
244
|
+
return executable;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Describe the executable identity that a profile's persistent server must match. */
|
|
248
|
+
export async function runtimeIdentity(binary, options = {}) {
|
|
249
|
+
const executable = await realpath(binary.includes(path.sep) || binary.includes('/') ? binary : resolvePathCommand(binary, options.env ?? process.env) ?? binary);
|
|
250
|
+
const digest = createHash('sha256').update(await readFile(executable)).digest('hex');
|
|
251
|
+
const ownedDirectory = options.ownedRuntime ?? process.env.CTG_OWNED_OPENCODE_RUNTIME ?? options[RESOLVED_OWNED_RUNTIME];
|
|
252
|
+
if (!ownedDirectory || options.binary || process.env.CTG_OPENCODE_BIN) {
|
|
253
|
+
return Object.freeze({ kind: 'upstream-compatible', binaryPath: executable, binarySha256: digest, version: OPENCODE_VERSION });
|
|
254
|
+
}
|
|
255
|
+
const manifest = JSON.parse(await readFile(path.join(abs(ownedDirectory), 'ctg-runtime.json'), 'utf8'));
|
|
256
|
+
const artifactPlatform = manifest.platform;
|
|
257
|
+
const ownedPath = await realpath(path.join(abs(ownedDirectory), `cli-${artifactPlatform}`, 'bin', platform() === 'win32' ? 'opencode.exe' : 'opencode'));
|
|
258
|
+
if (ownedPath !== executable) {
|
|
259
|
+
throw new Error(`CTG-owned runtime selector resolved ${executable}, but the selected artifact manifest binds ${ownedPath}`);
|
|
260
|
+
}
|
|
261
|
+
return Object.freeze({
|
|
262
|
+
kind: 'ctg-owned', binaryPath: executable, binarySha256: digest,
|
|
263
|
+
version: manifest.runtimeVersion, sourceRevision: manifest.sourceRevision,
|
|
264
|
+
lockSha256: manifest.lockSha256, patchSha256: manifest.patchSha256,
|
|
265
|
+
platform: manifest.platform, capabilities: { shellOrigin: true },
|
|
266
|
+
});
|
|
62
267
|
}
|
|
63
268
|
|
|
64
269
|
/** Construct the environment for CTG-owned OpenCode children. */
|
|
@@ -68,7 +273,10 @@ export function runtimeEnvironment(options = {}) {
|
|
|
68
273
|
// overrides. Workspace child environments are restored through the native
|
|
69
274
|
// session.environment API, not through this host process environment.
|
|
70
275
|
const inherited = { ...process.env, ...(options.env ?? {}) };
|
|
71
|
-
const
|
|
276
|
+
for (const key of Object.keys(inherited)) {
|
|
277
|
+
if (key.toUpperCase() === 'CTG_RUNTIME_GITHUB_TOKEN') delete inherited[key];
|
|
278
|
+
}
|
|
279
|
+
const env = Object.fromEntries(Object.entries(inherited).filter(([key]) => !key.startsWith('OPENCODE_') && key !== 'CTG_NATIVE_GLYPHS' && key !== 'CTG_TUI_GLYPHS'));
|
|
72
280
|
Object.assign(env, {
|
|
73
281
|
XDG_CONFIG_HOME: paths.config,
|
|
74
282
|
XDG_DATA_HOME: paths.data,
|
|
@@ -82,10 +290,9 @@ export function runtimeEnvironment(options = {}) {
|
|
|
82
290
|
OPENCODE_DB: path.join(paths.data, 'opencode.db'),
|
|
83
291
|
OPENCODE_CONFIG: paths.configFile,
|
|
84
292
|
OPENCODE_CONFIG_DIR: paths.configDir,
|
|
85
|
-
OPENCODE_CONFIG_PROJECT_DISABLE: 'true',
|
|
293
|
+
...(options.isolate ? { OPENCODE_CONFIG_PROJECT_DISABLE: 'true' } : {}),
|
|
86
294
|
OPENCODE_DISABLE_MODELS_FETCH: 'true',
|
|
87
|
-
OPENCODE_DISABLE_CLAUDE_CODE: 'true',
|
|
88
|
-
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: 'true',
|
|
295
|
+
...(options.isolate ? { OPENCODE_DISABLE_CLAUDE_CODE: 'true', OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: 'true' } : {}),
|
|
89
296
|
OPENCODE_DISABLE_AUTOUPDATE: 'true',
|
|
90
297
|
CTG_OPENCODE_HOME: paths.root,
|
|
91
298
|
});
|
|
@@ -105,18 +312,74 @@ export async function provisionRuntime(options = {}) {
|
|
|
105
312
|
current = JSON.parse(await readFile(paths.configFile, 'utf8'));
|
|
106
313
|
if (!current || typeof current !== 'object' || Array.isArray(current)) throw new Error('expected a JSON object');
|
|
107
314
|
} catch (error) {
|
|
108
|
-
if (error.code !== 'ENOENT') throw new Error(`Cannot preserve CTG
|
|
315
|
+
if (error.code !== 'ENOENT') throw new Error(`Cannot preserve CTG Carthagent runtime profile at ${paths.configFile}: ${error.message}`);
|
|
109
316
|
}
|
|
110
317
|
const config = { ...current, ...(options.profile ?? {}), $schema: current.$schema ?? 'https://opencode.ai/config.json' };
|
|
111
318
|
if (options.pluginPath) {
|
|
112
319
|
const plugin = abs(options.pluginPath);
|
|
113
|
-
config.plugins =
|
|
320
|
+
config.plugins = replaceManagedPlugin(config.plugins, plugin);
|
|
114
321
|
}
|
|
115
322
|
await atomicWrite(paths.configFile, `${JSON.stringify(config, null, 2)}\n`, 0o600);
|
|
116
323
|
});
|
|
117
324
|
return paths;
|
|
118
325
|
}
|
|
119
326
|
|
|
327
|
+
/** Old checkout/install references share one plugin ID, even when paths differ.
|
|
328
|
+
* Recognize the managed layout without requiring obsolete paths to still exist.
|
|
329
|
+
* Leave unrelated plugins (including their options and ordering) untouched. */
|
|
330
|
+
function replaceManagedPlugin(plugins, plugin) {
|
|
331
|
+
const entries = Array.isArray(plugins) ? plugins : [];
|
|
332
|
+
const managed = entry => {
|
|
333
|
+
let reference = typeof entry === 'string' ? entry : entry?.package;
|
|
334
|
+
if (typeof reference !== 'string') return false;
|
|
335
|
+
if (reference.startsWith('file:')) {
|
|
336
|
+
try { reference = fileURLToPath(reference); } catch { return false; }
|
|
337
|
+
}
|
|
338
|
+
if (abs(reference) === plugin) return true;
|
|
339
|
+
return /(?:^|\/)plugins\/carthagent(?:\/(?:src\/)?index\.ts)?$/.test(reference.replaceAll('\\', '/').replace(/\/+$/, ''));
|
|
340
|
+
};
|
|
341
|
+
const previous = entries.filter(managed);
|
|
342
|
+
// Prefer the selected installation's options, otherwise retain the latest
|
|
343
|
+
// managed entry's options when moving between installations.
|
|
344
|
+
const selected = previous.find(entry => typeof entry === 'object' && entry?.package === plugin)
|
|
345
|
+
?? previous.findLast(entry => entry && typeof entry === 'object')
|
|
346
|
+
?? previous.at(-1);
|
|
347
|
+
const replacement = selected && typeof selected === 'object' ? { ...selected, package: plugin } : plugin;
|
|
348
|
+
const result = [];
|
|
349
|
+
let replaced = false;
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
if (!managed(entry)) result.push(entry);
|
|
352
|
+
else if (!replaced) { result.push(replacement); replaced = true; }
|
|
353
|
+
}
|
|
354
|
+
if (!replaced) result.push(replacement);
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Remove only this file's atomicWrite staging entries without reading payloads.
|
|
359
|
+
* The caller must hold the same exclusive lock as every writer to this file;
|
|
360
|
+
* otherwise a staging entry could belong to a live write.
|
|
361
|
+
*/
|
|
362
|
+
export async function cleanupAtomicWriteStaging(file) {
|
|
363
|
+
const directoryPath = path.dirname(file), prefix = `${path.basename(file)}.`;
|
|
364
|
+
const suffix = /^[1-9][0-9]*\.[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\.tmp$/i;
|
|
365
|
+
// Windows cannot open directories as file handles; lstat still rejects a
|
|
366
|
+
// final-component symlink there. POSIX also persists the directory unlink.
|
|
367
|
+
const directory = process.platform === 'win32' ? undefined
|
|
368
|
+
: await open(directoryPath, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
|
|
369
|
+
try {
|
|
370
|
+
if (!(directory ? await directory.stat() : await lstat(directoryPath)).isDirectory()) throw new Error('Invalid atomicWrite staging directory');
|
|
371
|
+
// Stream entries rather than loading an unbounded directory listing.
|
|
372
|
+
let removed = false;
|
|
373
|
+
for await (const entry of await opendir(directoryPath)) {
|
|
374
|
+
if (!entry.name.startsWith(prefix) || !suffix.test(entry.name.slice(prefix.length))
|
|
375
|
+
|| (!entry.isFile() && !entry.isSymbolicLink())) continue;
|
|
376
|
+
// unlink does not follow symlinks and never recursively deletes paths.
|
|
377
|
+
await unlink(path.join(directoryPath, entry.name)); removed = true;
|
|
378
|
+
}
|
|
379
|
+
if (removed) await directory?.sync();
|
|
380
|
+
} finally { await directory?.close(); }
|
|
381
|
+
}
|
|
382
|
+
|
|
120
383
|
export async function atomicWrite(file, contents, mode = 0o600) {
|
|
121
384
|
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
122
385
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -210,15 +473,27 @@ function lockTimeoutError(file, owner) {
|
|
|
210
473
|
}
|
|
211
474
|
|
|
212
475
|
export function processAlive(pid) {
|
|
213
|
-
try { process.kill(pid, 0);
|
|
476
|
+
try { process.kill(pid, 0); }
|
|
214
477
|
catch (error) { return error.code === 'EPERM'; }
|
|
478
|
+
if (process.platform === 'linux') {
|
|
479
|
+
try {
|
|
480
|
+
// kill(pid, 0) also succeeds for exited children awaiting reaping by PID 1.
|
|
481
|
+
const status = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
482
|
+
return !['Z', 'X', 'x'].includes(status.slice(status.lastIndexOf(')') + 2).split(' ')[0]);
|
|
483
|
+
} catch (error) { if (error.code === 'ENOENT') return false; }
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
215
486
|
}
|
|
216
487
|
|
|
217
488
|
/** Run a child process with bounded startup and captured diagnostic output. */
|
|
218
489
|
export function runCapture(command, args, options = {}) {
|
|
219
490
|
return new Promise((resolve, reject) => {
|
|
220
491
|
const spawnImpl = options.spawnImpl ?? spawn;
|
|
221
|
-
const
|
|
492
|
+
const env = { ...(options.env ?? process.env) };
|
|
493
|
+
for (const key of Object.keys(env)) {
|
|
494
|
+
if (key.toUpperCase() === 'CTG_RUNTIME_GITHUB_TOKEN') delete env[key];
|
|
495
|
+
}
|
|
496
|
+
const child = spawnImpl(command, args, { cwd: options.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
222
497
|
let stdout = '';
|
|
223
498
|
let stderr = '';
|
|
224
499
|
const cap = options.maxOutput ?? 256 * 1024;
|
|
@@ -236,7 +511,11 @@ export function runCapture(command, args, options = {}) {
|
|
|
236
511
|
|
|
237
512
|
/** Preserve workspace authentication/environment: do not apply CTG host XDG overrides. */
|
|
238
513
|
export function workspaceEnvironment(workspace, env = process.env) {
|
|
239
|
-
|
|
514
|
+
const child = { ...env, CTG_WORKSPACE: abs(workspace) };
|
|
515
|
+
for (const key of Object.keys(child)) {
|
|
516
|
+
if (key.toUpperCase() === 'CTG_RUNTIME_GITHUB_TOKEN') delete child[key];
|
|
517
|
+
}
|
|
518
|
+
return child;
|
|
240
519
|
}
|
|
241
520
|
|
|
242
521
|
export async function readRuntimeCredentials(options = {}) {
|
package/lib/opencode/service.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { readFile, rm } from 'node:fs/promises';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { randomBytes } from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { processAlive, provisionRuntime, resolveOpenCode, runtimeEnvironment, runtimeIdentity, runtimePaths, withFileLock, atomicWrite, workspaceEnvironment } from './runtime.mjs';
|
|
5
|
+
import { withoutRuntimeGithubToken } from '../environment.mjs';
|
|
6
|
+
import { packageVersion } from '../update.mjs';
|
|
5
7
|
|
|
6
8
|
function authHeaders(info) {
|
|
7
9
|
if (!info.password) return {};
|
|
@@ -10,7 +12,7 @@ function authHeaders(info) {
|
|
|
10
12
|
|
|
11
13
|
async function registration(file) {
|
|
12
14
|
try { return JSON.parse(await readFile(file, 'utf8')); }
|
|
13
|
-
catch (error) { if (error.code === 'ENOENT') return undefined; throw new Error(`Cannot read isolated
|
|
15
|
+
catch (error) { if (error.code === 'ENOENT') return undefined; throw new Error(`Cannot read isolated Carthagent runtime service registration: ${error.message}`); }
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
async function inspect(info, timeout = 1000) {
|
|
@@ -19,7 +21,9 @@ async function inspect(info, timeout = 1000) {
|
|
|
19
21
|
if (!response.ok) return undefined;
|
|
20
22
|
const payload = await response.json();
|
|
21
23
|
const body = payload.data ?? payload;
|
|
22
|
-
if (body.version !==
|
|
24
|
+
if (body.version !== (info.runtimeIdentity?.version ?? info.version) || (info.pid && body.pid !== info.pid)) return undefined;
|
|
25
|
+
if (info.runtimeIdentity?.capabilities?.shellOrigin === true
|
|
26
|
+
&& body.capabilities?.shellOrigin !== info.runtimeIdentity.capabilities.shellOrigin) return undefined;
|
|
23
27
|
return { version: body.version, pid: body.pid, server: body, url: info.url };
|
|
24
28
|
} catch { return undefined; }
|
|
25
29
|
}
|
|
@@ -28,22 +32,46 @@ async function inspect(info, timeout = 1000) {
|
|
|
28
32
|
export async function ensureService(options = {}) {
|
|
29
33
|
const paths = await provisionRuntime(options);
|
|
30
34
|
const binary = await resolveOpenCode(options);
|
|
35
|
+
const identity = await runtimeIdentity(binary, options);
|
|
31
36
|
const env = runtimeEnvironment(options);
|
|
32
37
|
return withFileLock(paths.lifecycleLock, async () => {
|
|
38
|
+
// Serialize profile provisioning with lifecycle startup so concurrent
|
|
39
|
+
// launchers cannot race while installing the same theme files.
|
|
40
|
+
const { provisionDefaultNativeTheme } = await import('../../plugins/carthagent/src/ui/theme-provision.mjs');
|
|
41
|
+
await provisionDefaultNativeTheme(paths.configDir);
|
|
33
42
|
const info = await registration(paths.serviceFile);
|
|
34
43
|
if (info) {
|
|
44
|
+
const sameIdentity = JSON.stringify(info.runtimeIdentity) === JSON.stringify(identity);
|
|
35
45
|
const healthy = await inspect(info, options.healthTimeout ?? 1500);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
const sameLauncher = info.launcherVersion === packageVersion();
|
|
47
|
+
const ownedUpgrade = healthy && info.runtimeIdentity?.kind === 'ctg-owned' && identity.kind === 'ctg-owned';
|
|
48
|
+
if (healthy && sameIdentity && (sameLauncher || !ownedUpgrade)) return Object.freeze({ ...healthy, info, headers: authHeaders(info), paths, binary, runtimeIdentity: identity });
|
|
49
|
+
if (ownedUpgrade) {
|
|
50
|
+
// The lifecycle lock stays held through shutdown and replacement.
|
|
51
|
+
await stopRegisteredService(paths, info, options);
|
|
52
|
+
} else if (processAlive(info.pid)) {
|
|
53
|
+
if (!sameIdentity) {
|
|
54
|
+
throw serviceIdentityMismatch(info, identity, paths);
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`CTG Carthagent runtime service process ${info.pid} is alive but unhealthy at ${info.url}; registration was retained to avoid orphaning or duplicating it.`);
|
|
39
57
|
}
|
|
40
58
|
const current = await registration(paths.serviceFile);
|
|
41
59
|
if (current?.id === info.id && current?.pid === info.pid) await rm(paths.serviceFile, { force: true });
|
|
42
60
|
}
|
|
43
|
-
return startService({ paths, binary, env, options });
|
|
61
|
+
return startService({ paths, binary, env, options, identity });
|
|
44
62
|
}, { timeout: options.lockTimeout ?? 20000 });
|
|
45
63
|
}
|
|
46
64
|
|
|
65
|
+
function serviceIdentityMismatch(info, expected, paths) {
|
|
66
|
+
return new Error(
|
|
67
|
+
`CTG Carthagent runtime service profile is already running runtime ${info.runtimeIdentity?.version ?? 'with unknown identity'} `
|
|
68
|
+
+ `(${info.runtimeIdentity?.kind ?? 'legacy'}). This invocation selected ${expected.version} (${expected.kind}). `
|
|
69
|
+
+ `The active service was left running; no process was stopped or replaced. `
|
|
70
|
+
+ `After finishing active sessions, run ctg native service stop with CTG_OPENCODE_HOME=${JSON.stringify(paths.root)}, `
|
|
71
|
+
+ `then relaunch. Sessions and credentials are preserved. Alternatively, choose a separate CTG_OPENCODE_HOME profile.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
/** Stop only the process registered by this CTG profile; never signal a shared service. */
|
|
48
76
|
export async function stopService(options = {}) {
|
|
49
77
|
const paths = runtimePaths(options);
|
|
@@ -57,25 +85,31 @@ export async function stopService(options = {}) {
|
|
|
57
85
|
if (current?.id === info.id && current?.pid === info.pid) await rm(paths.serviceFile, { force: true });
|
|
58
86
|
return false;
|
|
59
87
|
}
|
|
60
|
-
throw new Error(`Refusing to stop CTG
|
|
61
|
-
}
|
|
62
|
-
const current = await registration(paths.serviceFile);
|
|
63
|
-
if (current?.id !== info.id || current?.pid !== info.pid || current?.url !== info.url) return false;
|
|
64
|
-
try { process.kill(info.pid, 'SIGTERM'); } catch (error) { if (error.code !== 'ESRCH') throw error; }
|
|
65
|
-
const deadline = Date.now() + (options.stopTimeout ?? 5000);
|
|
66
|
-
while (Date.now() < deadline) {
|
|
67
|
-
if (!processAlive(info.pid)) {
|
|
68
|
-
const latest = await registration(paths.serviceFile);
|
|
69
|
-
if (latest?.id === info.id && latest?.pid === info.pid) await rm(paths.serviceFile, { force: true });
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
88
|
+
throw new Error(`Refusing to stop CTG Carthagent runtime PID ${info.pid}: the registered process is alive but its service identity cannot be verified. Registration was retained.`);
|
|
73
89
|
}
|
|
74
|
-
|
|
90
|
+
return stopRegisteredService(paths, info, options);
|
|
75
91
|
}, { timeout: options.lockTimeout ?? 20000 });
|
|
76
92
|
}
|
|
77
93
|
|
|
78
|
-
async function
|
|
94
|
+
async function stopRegisteredService(paths, info, options) {
|
|
95
|
+
const current = await registration(paths.serviceFile);
|
|
96
|
+
if (current?.id !== info.id || current?.pid !== info.pid || current?.url !== info.url) {
|
|
97
|
+
throw new Error('CTG runtime service registration changed before shutdown; no process was stopped.');
|
|
98
|
+
}
|
|
99
|
+
try { process.kill(info.pid, 'SIGTERM'); } catch (error) { if (error.code !== 'ESRCH') throw error; }
|
|
100
|
+
const deadline = Date.now() + (options.stopTimeout ?? 5000);
|
|
101
|
+
while (Date.now() < deadline) {
|
|
102
|
+
if (!processAlive(info.pid)) {
|
|
103
|
+
const latest = await registration(paths.serviceFile);
|
|
104
|
+
if (latest?.id === info.id && latest?.pid === info.pid) await rm(paths.serviceFile, { force: true });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`CTG Carthagent runtime service process ${info.pid} did not stop within ${options.stopTimeout ?? 5000}ms`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function startService({ paths, binary, env: baseEnv, options, identity }) {
|
|
79
113
|
const deadline = Date.now() + (options.startupTimeout ?? 20000);
|
|
80
114
|
const password = randomBytes(32).toString('base64url');
|
|
81
115
|
const env = { ...baseEnv, OPENCODE_SERVER_PASSWORD: password };
|
|
@@ -99,10 +133,10 @@ async function startService({ paths, binary, env: baseEnv, options }) {
|
|
|
99
133
|
child.stderr.on('data', collect);
|
|
100
134
|
child.once('error', (error) => { startupFailure = error; if (!settled) { settled = true; clearTimeout(startupTimer); reject(error); } });
|
|
101
135
|
child.once('exit', (code, signal) => {
|
|
102
|
-
if (!settled) { settled = true; clearTimeout(startupTimer); reject(new Error(`
|
|
136
|
+
if (!settled) { settled = true; clearTimeout(startupTimer); reject(new Error(`Carthagent runtime exited ${code ?? signal} before listening: ${output.replaceAll(password, '[redacted]')}`)); }
|
|
103
137
|
});
|
|
104
138
|
startupTimer = setTimeout(() => {
|
|
105
|
-
if (!settled) { settled = true; reject(new Error(`
|
|
139
|
+
if (!settled) { settled = true; reject(new Error(`Carthagent runtime startup timed out after ${options.startupTimeout ?? 20000}ms`)); }
|
|
106
140
|
}, Math.max(1, deadline - Date.now()));
|
|
107
141
|
child.unref();
|
|
108
142
|
});
|
|
@@ -112,17 +146,18 @@ async function startService({ paths, binary, env: baseEnv, options }) {
|
|
|
112
146
|
id: `ctg-${child.pid}-${randomBytes(8).toString('hex')}`,
|
|
113
147
|
pid: child.pid,
|
|
114
148
|
state: 'starting',
|
|
115
|
-
version:
|
|
149
|
+
version: identity.version,
|
|
150
|
+
runtimeIdentity: identity,
|
|
116
151
|
})}\n`);
|
|
117
152
|
}
|
|
118
153
|
const url = await started;
|
|
119
154
|
child.stdout.unref?.();
|
|
120
155
|
child.stderr.unref?.();
|
|
121
|
-
const info = { id: `ctg-${child.pid}-${randomBytes(8).toString('hex')}`, pid: child.pid, url, password, version:
|
|
156
|
+
const info = { id: `ctg-${child.pid}-${randomBytes(8).toString('hex')}`, pid: child.pid, url, password, version: identity.version, runtimeIdentity: identity, launcherVersion: packageVersion() };
|
|
122
157
|
await atomicWrite(paths.serviceFile, `${JSON.stringify(info)}\n`);
|
|
123
158
|
const healthy = await inspect(info, Math.max(250, deadline - Date.now()));
|
|
124
|
-
if (!healthy) throw new Error('
|
|
125
|
-
return Object.freeze({ ...healthy, info, headers: authHeaders(info), paths, binary });
|
|
159
|
+
if (!healthy) throw new Error('Carthagent runtime listener started but authenticated service identity did not validate');
|
|
160
|
+
return Object.freeze({ ...healthy, info, headers: authHeaders(info), paths, binary, runtimeIdentity: identity });
|
|
126
161
|
} catch (error) {
|
|
127
162
|
let stopped = !child?.pid || !processAlive(child.pid);
|
|
128
163
|
if (child?.pid && processAlive(child.pid)) {
|
|
@@ -137,9 +172,9 @@ async function startService({ paths, binary, env: baseEnv, options }) {
|
|
|
137
172
|
const registered = await registration(paths.serviceFile).catch(() => undefined);
|
|
138
173
|
if (stopped && registered?.pid === child?.pid) await rm(paths.serviceFile, { force: true });
|
|
139
174
|
if (!stopped) {
|
|
140
|
-
throw new Error(`CTG
|
|
175
|
+
throw new Error(`CTG Carthagent runtime startup failed and process ${child.pid} could not be stopped; ownership registration was retained at ${paths.serviceFile}.`, { cause: error });
|
|
141
176
|
}
|
|
142
|
-
throw new Error(`Unable to start CTG
|
|
177
|
+
throw new Error(`Unable to start CTG Carthagent runtime service: ${startupFailure?.message ?? error.message}${output ? `\n${output.replaceAll(password, '[redacted]')}` : ''}`, { cause: error });
|
|
143
178
|
}
|
|
144
179
|
}
|
|
145
180
|
|
|
@@ -165,7 +200,7 @@ export async function serviceRequest(service, method, endpoint, options = {}) {
|
|
|
165
200
|
let data;
|
|
166
201
|
try { data = text ? JSON.parse(text) : undefined; } catch { data = text; }
|
|
167
202
|
if (!response.ok) {
|
|
168
|
-
const error = new Error(`
|
|
203
|
+
const error = new Error(`Carthagent runtime API ${method} ${url.pathname} failed (${response.status}): ${typeof data === 'string' ? data : JSON.stringify(data)}`);
|
|
169
204
|
error.status = response.status;
|
|
170
205
|
error.body = data;
|
|
171
206
|
throw error;
|
|
@@ -226,7 +261,7 @@ export async function setSessionEnvironment(service, sessionID, env, options = {
|
|
|
226
261
|
|
|
227
262
|
function normalizeDirectory(location) {
|
|
228
263
|
const directory = typeof location === 'string' ? location : location?.directory;
|
|
229
|
-
if (typeof directory !== 'string' || !directory) throw new TypeError('
|
|
264
|
+
if (typeof directory !== 'string' || !directory) throw new TypeError('Carthagent runtime location must include a directory');
|
|
230
265
|
return directory;
|
|
231
266
|
}
|
|
232
267
|
|
|
@@ -234,13 +269,20 @@ export function launchTui(service, workspace, options = {}) {
|
|
|
234
269
|
const args = ['--server', service.url, workspace];
|
|
235
270
|
if (options.sessionID) args.push('--session', options.sessionID);
|
|
236
271
|
if (options.continue) args.push('--continue');
|
|
272
|
+
const env = { ...runtimeEnvironment({ root: service.paths.root, env: options.env }) };
|
|
273
|
+
if (typeof options.theme === 'string' && options.theme.trim()) env.OPENCODE_CLI_CONFIG_CONTENT = JSON.stringify({ theme: { name: options.theme.trim(), mode: 'system' } });
|
|
274
|
+
const glyphs = options.glyphs ?? options.env?.CTG_TUI_GLYPHS;
|
|
275
|
+
if (glyphs === 'ascii' || glyphs === 'unicode') env.CTG_TUI_GLYPHS = glyphs;
|
|
276
|
+
for (const key of Object.keys(env)) {
|
|
277
|
+
if (key.toUpperCase() === 'CTG_RUNTIME_GITHUB_TOKEN') delete env[key];
|
|
278
|
+
}
|
|
237
279
|
const child = (options.spawnImpl ?? spawn)(service.binary, args, {
|
|
238
280
|
cwd: workspace,
|
|
239
|
-
env: {
|
|
240
|
-
...
|
|
281
|
+
env: withoutRuntimeGithubToken({
|
|
282
|
+
...env,
|
|
241
283
|
OPENCODE_PASSWORD: service.info?.password ?? service.password,
|
|
242
284
|
CTG_WORKSPACE: workspaceEnvironment(workspace, options.env ?? process.env).CTG_WORKSPACE,
|
|
243
|
-
},
|
|
285
|
+
}),
|
|
244
286
|
stdio: 'inherit',
|
|
245
287
|
});
|
|
246
288
|
return child;
|
|
@@ -2,8 +2,8 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { validateBudgetOptions } from '../budget.mjs';
|
|
3
3
|
|
|
4
4
|
export const SESSION_OPTIONS_KEY = 'carthagent.options';
|
|
5
|
-
const booleans = ['delivery', 'guide', 'strict', 'isolate', 'protectExisting'];
|
|
6
|
-
const integers = ['maxTools', 'maxSeconds', 'maxRepairs', 'codegraphMaxFiles', 'rewriteCap', 'toolOutputCap'];
|
|
5
|
+
const booleans = ['delivery', 'guide', 'strict', 'isolate', 'protectExisting', 'jev'];
|
|
6
|
+
const integers = ['maxTools', 'maxSeconds', 'maxRepairs', 'codegraphMaxFiles', 'rewriteCap', 'toolOutputCap', 'turnDelayMs'];
|
|
7
7
|
const paths = ['validators', 'context'];
|
|
8
8
|
|
|
9
9
|
/** Only delivery runtime settings belong here; credentials and UI choices do not. */
|
|
@@ -23,6 +23,10 @@ export function normalizeSessionOptions(input = {}, cwd = process.cwd()) {
|
|
|
23
23
|
}
|
|
24
24
|
result[key] = Number(input[key]);
|
|
25
25
|
}
|
|
26
|
+
if (input.jevProvider !== undefined) {
|
|
27
|
+
if (!['ship', 'openrouter', 'typesafe'].includes(input.jevProvider)) throw new TypeError('jevProvider must be ship, openrouter or typesafe');
|
|
28
|
+
result.jevProvider = input.jevProvider;
|
|
29
|
+
}
|
|
26
30
|
if (input.bashCap !== undefined) {
|
|
27
31
|
const cap = Number(input.bashCap);
|
|
28
32
|
if (!Number.isFinite(cap) || cap <= 0 || !/^\d+(?:\.\d+)?$/.test(String(input.bashCap))) throw new TypeError('bashCap must be a positive number');
|