11 changed files with 135 additions and 20 deletions
@@ -2,13 +2,23 @@ import assert from 'node:assert/strict'
import { test } from 'vitest' import { test } from 'vitest'
import { classifyActiveRuntime, hasValidBootstrapMarker } from './active-runtime-state' import { classifyActiveRuntime, hasValidBootstrapMarker, needsPackagedRuntimeUpgrade } from './active-runtime-state'
const VALID_MARKER = { const VALID_MARKER = {
pinnedCommit: '1234567890abcdef1234567890abcdef12345678', pinnedCommit: '1234567890abcdef1234567890abcdef12345678',
schemaVersion: 1 schemaVersion: 1
} }
test('a package upgrade refreshes only a proven managed runtime, once per package', () => {
const stamp = {commit: 'b'.repeat(40), source: 'git'}
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, VALID_MARKER), true)
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, {...VALID_MARKER, pinnedCommit: stamp.commit}), false)
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, {...VALID_MARKER, pinnedCommit: 'c'.repeat(40), packageCommit: stamp.commit}), false)
assert.equal(needsPackagedRuntimeUpgrade(false, stamp, VALID_MARKER), false)
assert.equal(needsPackagedRuntimeUpgrade(true, stamp, null), false)
assert.equal(needsPackagedRuntimeUpgrade(true, {commit: '0'.repeat(40), source: 'fallback'}, VALID_MARKER), false)
})
test('hasValidBootstrapMarker accepts the current schema with a real-looking commit', () => { test('hasValidBootstrapMarker accepts the current schema with a real-looking commit', () => {
assert.equal(hasValidBootstrapMarker(VALID_MARKER, 1), true) assert.equal(hasValidBootstrapMarker(VALID_MARKER, 1), true)
}) })
@@ -1,8 +1,21 @@
export interface BootstrapMarkerLike { export interface BootstrapMarkerLike {
pinnedCommit?: unknown pinnedCommit?: unknown
packageCommit?: unknown
schemaVersion?: unknown schemaVersion?: unknown
} }
// An AITURK package upgrade must bring its managed agent along with the UI.
// Developer checkouts and installs without proven Desktop ownership keep their
// existing launch behavior. The installer preserves edits and refuses rollback.
export function needsPackagedRuntimeUpgrade(
packaged: boolean, stamp: { commit?: string; source?: string } | null,
marker: BootstrapMarkerLike | null
): boolean {
return Boolean(packaged && stamp?.source !== 'fallback' &&
/^[0-9a-f]{40}$/i.test(stamp?.commit || '') && !/^0+$/.test(stamp?.commit || '') &&
hasValidBootstrapMarker(marker, 1) && (marker?.packageCommit || marker?.pinnedCommit) !== stamp?.commit)
}
export interface ActiveRuntimeState { export interface ActiveRuntimeState {
hasValidMarker: boolean hasValidMarker: boolean
shouldUseActiveRuntime: boolean shouldUseActiveRuntime: boolean
@@ -143,8 +143,8 @@ test('resolveMarkerPinnedCommit prefers real HEAD over fallback stamp zeros', ()
resolveMarkerPinnedCommit({ commit: 'd'.repeat(40), branch: 'main' }, '/tmp/checkout', { resolveMarkerPinnedCommit({ commit: 'd'.repeat(40), branch: 'main' }, '/tmp/checkout', {
resolveHead: () => realHead resolveHead: () => realHead
}), }),
'd'.repeat(40), realHead,
'packaged real pin wins over checkout HEAD' 'marker reports the actual installed checkout, including a preserved newer runtime'
) )
assert.equal( assert.equal(
resolveMarkerPinnedCommit({ commit: ZERO_COMMIT, branch: 'main' }, '/tmp/missing', { resolveMarkerPinnedCommit({ commit: ZERO_COMMIT, branch: 'main' }, '/tmp/missing', {
+7 -6
View File
@@ -111,16 +111,16 @@ function resolveMarkerPinnedCommit(
): string | null { ): string | null {
const resolveHead = opts.resolveHead || resolveCheckoutHead const resolveHead = opts.resolveHead || resolveCheckoutHead
if (installStamp && isPinnedCommit(installStamp.commit)) {
return installStamp.commit
}
const head = resolveHead(activeRoot) const head = resolveHead(activeRoot)
if (head) { if (head) {
return head return head
} }
if (installStamp && isPinnedCommit(installStamp.commit)) {
return installStamp.commit
}
return readExistingPinnedCommit(activeRoot) return readExistingPinnedCommit(activeRoot)
} }
@@ -918,14 +918,14 @@ async function runBootstrap(opts) {
try { try {
const existingCheckout = hasExistingGitCheckout(activeRoot) const existingCheckout = hasExistingGitCheckout(activeRoot)
const pinCommit = !existingCheckout const pinCommit = Boolean(installStamp && isPinnedCommit(installStamp.commit)) || !existingCheckout
if (existingCheckout && installStamp && installStamp.commit) { if (existingCheckout && installStamp && installStamp.commit) {
emit({ emit({
type: 'log', type: 'log',
line: line:
`[bootstrap] existing checkout detected at ${activeRoot}; ` + `[bootstrap] existing checkout detected at ${activeRoot}; ` +
`not pinning to packaged install stamp ${installStamp.commit.slice(0, 12)}` `applying packaged install stamp ${installStamp.commit.slice(0, 12)} with installer rollback protection`
}) })
} }
@@ -1002,6 +1002,7 @@ async function runBootstrap(opts) {
const markerPayload = { const markerPayload = {
pinnedCommit, pinnedCommit,
packageCommit: installStamp && isPinnedCommit(installStamp.commit) ? installStamp.commit : null,
pinnedBranch: installStamp ? installStamp.branch : null pinnedBranch: installStamp ? installStamp.branch : null
} }
+7 -1
View File
@@ -30,7 +30,7 @@ import {
systemPreferences systemPreferences
} from 'electron' } from 'electron'
import { classifyActiveRuntime } from './active-runtime-state' import { classifyActiveRuntime, needsPackagedRuntimeUpgrade } from './active-runtime-state'
import { destroyKeepaliveAgents, downloadAgentFor, jsonAgentFor, withRetry } from './api-transport' import { destroyKeepaliveAgents, downloadAgentFor, jsonAgentFor, withRetry } from './api-transport'
import { appIconCandidates, resolveAppIcon } from './app-icon' import { appIconCandidates, resolveAppIcon } from './app-icon'
import { stopBackendChild as stopBackendChildImpl, stopBackendTreesForUpdate } from './backend-child' import { stopBackendChild as stopBackendChildImpl, stopBackendTreesForUpdate } from './backend-child'
@@ -4597,6 +4597,7 @@ function writeBootstrapMarker(payload) {
const merged = { const merged = {
schemaVersion: BOOTSTRAP_MARKER_SCHEMA_VERSION, schemaVersion: BOOTSTRAP_MARKER_SCHEMA_VERSION,
pinnedCommit: payload.pinnedCommit || null, pinnedCommit: payload.pinnedCommit || null,
packageCommit: payload.packageCommit || null,
pinnedBranch: payload.pinnedBranch || null, pinnedBranch: payload.pinnedBranch || null,
completedAt: new Date().toISOString(), completedAt: new Date().toISOString(),
desktopVersion: app.getVersion() desktopVersion: app.getVersion()
@@ -4905,6 +4906,11 @@ function resolveHermesBackend(backendArgs) {
// bootstrap when the runtime itself is unusable. // bootstrap when the runtime itself is unusable.
const activeRuntime = activeRuntimeState() const activeRuntime = activeRuntimeState()
if (needsPackagedRuntimeUpgrade(IS_PACKAGED, INSTALL_STAMP, readBootstrapMarker())) {
rememberLog('[bootstrap] AITURK package changed; updating its managed agent before launch.')
return createBootstrapBackend(backendArgs)
}
if (activeRuntime.shouldUseActiveRuntime && !bootstrapRepairRequested) { if (activeRuntime.shouldUseActiveRuntime && !bootstrapRepairRequested) {
if (!activeRuntime.hasValidMarker) { if (!activeRuntime.hasValidMarker) {
rememberLog( rememberLog(
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide", "name": "aiturk-ide",
"productName": "AITURK IDE", "productName": "AITURK IDE",
"private": true, "private": true,
"version": "1.0.0-beta.1", "version": "1.0.0-beta.3",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.", "description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis", "author": "AITURK / TurkServis",
"repository": { "repository": {
+2 -2
View File
@@ -25,7 +25,7 @@ export function aiturkInstallStage(name: string): string | undefined {
const en = { const en = {
intro: 'Bring your code, question, or idea. Build with AITURK.', intro: 'Bring your code, question, or idea. Build with AITURK.',
pitch: 'Connect your models with your TurkServis account.', pitch: 'Connect your models and available image and audio tools with your TurkServis account.',
key: 'TurkServis API key', key: 'TurkServis API key',
getKey: 'My account and API keys', getKey: 'My account and API keys',
noModels: 'No models are available for this key.', noModels: 'No models are available for this key.',
@@ -74,7 +74,7 @@ const en = {
const tr: typeof en = { const tr: typeof en = {
intro: 'Kodunuzu, sorunuzu veya fikrinizi paylaşın. AITURK ile birlikte geliştirin.', intro: 'Kodunuzu, sorunuzu veya fikrinizi paylaşın. AITURK ile birlikte geliştirin.',
pitch: 'TurkServis hesabınızla modellerinize bağlanın.', pitch: 'TurkServis hesabınızla modellerinize, kullanılabilir görsel ve ses araçlarına bağlanın.',
key: 'TurkServis API anahtarı', key: 'TurkServis API anahtarı',
getKey: 'Hesabım ve API anahtarlarım', getKey: 'Hesabım ve API anahtarlarım',
noModels: 'Bu anahtarla kullanılabilecek model bulunamadı.', noModels: 'Bu anahtarla kullanılabilecek model bulunamadı.',
+41
View File
@@ -0,0 +1,41 @@
"""Connect the AITURK distribution's media tools to its existing member key."""
def configure_existing_media(cfg: dict) -> bool:
"""Migrate an existing member endpoint when the upgraded backend starts."""
providers = cfg.get("providers") or {}
entries = providers.values() if isinstance(providers, dict) else []
for entry in entries:
if isinstance(entry, dict) and configure_media(cfg, entry):
return True
return False
def configure_media(cfg: dict, entry: dict) -> bool:
if entry.get("base_url", "").rstrip("/") != "https://ai.turkservis.online/v1":
return False
key_env = entry.get("key_env")
if not isinstance(key_env, str) or not key_env:
return False
servers = cfg.get("mcp_servers")
if servers is None:
servers = cfg["mcp_servers"] = {}
if not isinstance(servers, dict):
return False
name = "turkservis-media"
existing = servers.get(name, {})
if not isinstance(existing, dict):
return False
if existing and existing.get("url") != "https://ai.turkservis.online/mcp":
return False
updated = {
**existing,
"url": "https://ai.turkservis.online/mcp",
"headers": {"Authorization": "Bearer ${" + key_env + "}"},
"timeout": 300,
"tools": {"include": ["list_capabilities", "generate_media"]},
}
if updated == existing:
return False
servers[name] = updated
return True
+9
View File
@@ -405,6 +405,12 @@ def _eager_reconcile_own_session_db() -> None:
@asynccontextmanager @asynccontextmanager
async def _lifespan(app: "FastAPI"): async def _lifespan(app: "FastAPI"):
from hermes_cli.aiturk_media import configure_existing_media
from hermes_cli.config import read_raw_config, save_config
with _CONFIG_MUTATION_LOCK:
media_cfg = read_raw_config()
if configure_existing_media(media_cfg):
save_config(media_cfg)
app.state.event_channels = {} # dict[str, set] app.state.event_channels = {} # dict[str, set]
app.state.event_lock = asyncio.Lock() app.state.event_lock = asyncio.Lock()
app.state.pty_active_session_files = {} # dict[str, Path] app.state.pty_active_session_files = {} # dict[str, Path]
@@ -8823,6 +8829,9 @@ def _write_custom_endpoint(cfg: Dict[str, Any], body: CustomEndpointUpdate) -> T
providers[endpoint_id] = entry providers[endpoint_id] = entry
cfg["providers"] = providers cfg["providers"] = providers
from hermes_cli.aiturk_media import configure_media
configure_media(cfg, entry)
if body.make_default: if body.make_default:
cfg["model"] = _apply_main_model_assignment( cfg["model"] = _apply_main_model_assignment(
cfg.get("model", {}), endpoint_id, model, base_url cfg.get("model", {}), endpoint_id, model, base_url
+7 -7
View File
@@ -65,7 +65,8 @@
}, },
"apps/desktop": { "apps/desktop": {
"name": "aiturk-ide", "name": "aiturk-ide",
"version": "1.0.0-beta.1", "version": "1.0.0-beta.3",
"license": "MIT",
"dependencies": { "dependencies": {
"@assistant-ui/core": "0.2.23", "@assistant-ui/core": "0.2.23",
"@assistant-ui/react": "0.14.24", "@assistant-ui/react": "0.14.24",
@@ -175,8 +176,7 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"get-windows": "9.3.0" "get-windows": "9.3.0"
}, }
"license": "MIT"
}, },
"apps/desktop/node_modules/@babel/code-frame": { "apps/desktop/node_modules/@babel/code-frame": {
"version": "8.0.0", "version": "8.0.0",
@@ -7465,6 +7465,10 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/aiturk-ide": {
"resolved": "apps/desktop",
"link": true
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.15.0", "version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
@@ -19873,10 +19877,6 @@
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
},
"node_modules/aiturk-ide": {
"resolved": "apps/desktop",
"link": true
} }
} }
} }
+35
View File
@@ -0,0 +1,35 @@
from hermes_cli.aiturk_media import configure_media, configure_existing_media
def test_existing_member_profile_migrates_once_without_changing_provider():
entry = {"base_url": "https://ai.turkservis.online/v1", "key_env": "MEMBER_KEY"}
cfg = {"providers": {"turkservis": dict(entry)}, "model": {"default": "kept"}}
assert configure_existing_media(cfg)
assert cfg["providers"]["turkservis"] == entry
assert cfg["model"] == {"default": "kept"}
assert not configure_existing_media(cfg)
def test_member_key_is_referenced_without_copying_or_overwriting_other_servers():
cfg = {"mcp_servers": {"other": {"url": "https://example.org/mcp"}}}
entry = {"base_url": "https://ai.turkservis.online/v1", "key_env": "HERMES_CUSTOM_TURKSERVIS_API_KEY"}
assert configure_media(cfg, entry)
assert cfg["mcp_servers"]["other"] == {"url": "https://example.org/mcp"}
assert cfg["mcp_servers"]["turkservis-media"]["headers"]["Authorization"] == "Bearer ${HERMES_CUSTOM_TURKSERVIS_API_KEY}"
assert not configure_media(cfg, entry)
entry["key_env"] = "ROTATED_MEMBER_KEY"
assert configure_media(cfg, entry)
assert "ROTATED_MEMBER_KEY" in cfg["mcp_servers"]["turkservis-media"]["headers"]["Authorization"]
def test_other_provider_and_missing_key_do_not_enable_media():
for entry in ({"base_url": "https://example.com/v1", "key_env": "SECRET"}, {"base_url": "https://ai.turkservis.online/v1"}):
cfg = {}
assert not configure_media(cfg, entry)
assert cfg == {}
def test_explicitly_disabled_connection_stays_disabled():
cfg = {"mcp_servers": {"turkservis-media": {"url": "https://ai.turkservis.online/mcp", "enabled": False}}}
configure_media(cfg, {"base_url": "https://ai.turkservis.online/v1", "key_env": "KEY"})
assert cfg["mcp_servers"]["turkservis-media"]["enabled"] is False