super-dsh 0.1.10 → 0.1.11
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 +18 -2
- package/agent-agy/dist/agent.js +19 -1
- package/agent-claude/dist/adapter.js +106 -31
- package/agent-claude/dist/agent.js +177 -22
- package/agent-claude/dist/claude-client.js +84 -43
- package/agent-claude/dist/claude-launch.js +101 -0
- package/agent-claude/dist/claude-native-config.js +169 -0
- package/agent-claude/dist/claude-provider-gateway.js +340 -0
- package/agent-claude/dist/claude-providers.js +391 -0
- package/agent-claude/dist/claude-route.js +152 -0
- package/agent-claude/dist/index.js +214 -37
- package/agent-claude/dist/models.js +14 -0
- package/agent-claude/dist/sdk-runtime.js +320 -0
- package/agent-claude/package.json +6 -4
- package/agent-codex/README.md +18 -6
- package/agent-codex/dist/adapter.js +3 -2
- package/agent-codex/dist/agent.js +41 -7
- package/agent-codex/dist/app-server-client.js +7 -1
- package/agent-codex/dist/app-server-wire.js +10 -22
- package/agent-codex/dist/codex-launch.js +10 -32
- package/agent-codex/dist/codex-native-config.js +9 -3
- package/agent-codex/dist/codex-oauth.js +153 -0
- package/agent-codex/dist/codex-provider-gateway.js +36 -38
- package/agent-codex/dist/codex-providers.js +20 -7
- package/agent-codex/dist/codex-runtime.js +120 -0
- package/agent-codex/dist/index.js +18 -11
- package/agent-codex/package.json +1 -1
- package/agent-codex-sdk/dist/adapter.js +3 -2
- package/agent-codex-sdk/dist/agent.js +35 -1
- package/agent-codex-sdk/dist/codex-native-config.js +9 -3
- package/agent-codex-sdk/dist/codex-oauth.js +153 -0
- package/agent-codex-sdk/dist/codex-provider-gateway.js +36 -38
- package/agent-codex-sdk/dist/codex-providers.js +20 -7
- package/agent-codex-sdk/dist/codex-sdk-launch.js +5 -38
- package/agent-codex-sdk/dist/index.js +18 -11
- package/agent-codex-sdk/dist/sdk-runtime.js +2 -2
- package/agent-hermes/dist/agent.js +46 -2
- package/agent-hermes/dist/hermes-client.js +49 -1
- package/agent-omp/dist/agent.js +17 -4
- package/agent-omp-sdk/dist/agent.d.ts +11 -0
- package/agent-omp-sdk/dist/agent.js +17 -4
- package/agent-pi/dist/agent.js +33 -1
- package/client/index.js +150 -58
- package/package.json +2 -3
- package/superd-hub/dist/client/agent-settings.d.ts +66 -12
- package/superd-hub/dist/client/agent-settings.js +85 -14
- package/superd-hub/dist/client/runtime-settings-api.js +5 -5
- package/superd-hub/dist/world-mount.d.ts +6 -5
- package/superd-hub/dist/world-mount.js +6 -6
- package/superd-hub/lib/client/index.js +150 -58
|
@@ -95,9 +95,10 @@ function slugOf(entry) {
|
|
|
95
95
|
* that we do not already hold, then upserts `models` by slug. Each upserted
|
|
96
96
|
* entry is a CLONE of `template` — an existing entry for the same slug if there
|
|
97
97
|
* is one, else the caller's template, else the first entry either side supplied
|
|
98
|
-
* — with
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
* — with identity and declared context window overridden. The other metadata
|
|
99
|
+
* is copied through except `comp_hash`: this optional opaque compatibility
|
|
100
|
+
* identifier belongs to the template's model, not an unrelated provider model.
|
|
101
|
+
* See the plan's D5 for why we do not synthesize the remaining fields.
|
|
101
102
|
*/
|
|
102
103
|
export function mergeModelCatalog(existing, absorbed, models, template) {
|
|
103
104
|
// Both sides are a TEMPLATE POOL only — never output. What gets emitted is
|
|
@@ -132,6 +133,11 @@ export function mergeModelCatalog(existing, absorbed, models, template) {
|
|
|
132
133
|
slug: model.id,
|
|
133
134
|
display_name: model.displayName ?? model.id,
|
|
134
135
|
};
|
|
136
|
+
// A copied OpenAI hash falsely triggers previous-model compaction on
|
|
137
|
+
// provider switches in Codex 0.160. Our provider model declarations do not
|
|
138
|
+
// carry verified hashes; remove stale generated values too. Context-limit
|
|
139
|
+
// compaction and its window/threshold metadata remain unchanged.
|
|
140
|
+
delete entry["comp_hash"];
|
|
135
141
|
if (model.contextWindow !== undefined) {
|
|
136
142
|
// Codex reads context_window and caps against max_context_window; a stale
|
|
137
143
|
// template value must not silently cap a larger real window.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Native OAuth discovery. Tokens stay in Codex's own home; no network/spawn. */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readNativeSelection } from './codex-native-config.js';
|
|
5
|
+
import { readProviderStore } from './codex-providers.js';
|
|
6
|
+
export const OPENAI_OAUTH_ID = 'openai-oauth';
|
|
7
|
+
export function hasOpenAiOAuth(codexHome) {
|
|
8
|
+
try {
|
|
9
|
+
const value = JSON.parse(readFileSync(join(codexHome, 'auth.json'), 'utf8'));
|
|
10
|
+
const tokens = value?.tokens;
|
|
11
|
+
return value?.auth_mode === 'chatgpt' && tokens !== null && typeof tokens === 'object'
|
|
12
|
+
&& [tokens.access_token, tokens.refresh_token].some(token => typeof token === 'string' && token.trim() !== '');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function nonEmpty(value) {
|
|
22
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
23
|
+
}
|
|
24
|
+
function oneOf(value, choices) {
|
|
25
|
+
return typeof value === 'string' && choices.includes(value);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The non-defaulted ModelInfo fields consumed by Codex, including its required
|
|
29
|
+
* nested metadata. Keep unknown native fields untouched; never repair or
|
|
30
|
+
* synthesize a catalog. Source: codex-rs/protocol/src/openai_models.rs (0.154 / 0.160).
|
|
31
|
+
*/
|
|
32
|
+
function usableModel(value) {
|
|
33
|
+
if (!isRecord(value) || !nonEmpty(value['slug']) || typeof value['display_name'] !== 'string')
|
|
34
|
+
return false;
|
|
35
|
+
const reasoning = value['supported_reasoning_levels'];
|
|
36
|
+
const truncation = value['truncation_policy'];
|
|
37
|
+
const messages = value['model_messages'];
|
|
38
|
+
if (typeof value['base_instructions'] !== 'string' && !(isRecord(messages) && typeof messages['instructions_template'] === 'string'))
|
|
39
|
+
return false;
|
|
40
|
+
return Array.isArray(reasoning) && reasoning.every(level => isRecord(level) && nonEmpty(level['effort']) && typeof level['description'] === 'string')
|
|
41
|
+
&& oneOf(value['shell_type'], ['unified_exec', 'default', 'local', 'shell_command', 'disabled'])
|
|
42
|
+
&& oneOf(value['visibility'], ['list', 'hide', 'none'])
|
|
43
|
+
&& typeof value['supported_in_api'] === 'boolean'
|
|
44
|
+
&& Number.isSafeInteger(value['priority']) && Number(value['priority']) >= -2147483648 && Number(value['priority']) <= 2147483647
|
|
45
|
+
&& typeof value['support_verbosity'] === 'boolean'
|
|
46
|
+
&& isRecord(truncation) && oneOf(truncation['mode'], ['tokens', 'bytes']) && Number.isSafeInteger(truncation['limit'])
|
|
47
|
+
&& Array.isArray(value['experimental_supported_tools']) && value['experimental_supported_tools'].every(tool => typeof tool === 'string');
|
|
48
|
+
}
|
|
49
|
+
/** One bad entry invalidates the file: Codex deserializes the whole catalog. */
|
|
50
|
+
function officialCatalogModels(codexHome) {
|
|
51
|
+
try {
|
|
52
|
+
const cache = JSON.parse(readFileSync(join(codexHome, 'models_cache.json'), 'utf8'));
|
|
53
|
+
if (!isRecord(cache))
|
|
54
|
+
return undefined;
|
|
55
|
+
const models = cache['models'];
|
|
56
|
+
if (Array.isArray(models) && models.length > 0 && models.every(usableModel))
|
|
57
|
+
return models;
|
|
58
|
+
}
|
|
59
|
+
catch { /* Missing/stale cache must not become a forced catalog override. */ }
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Conservatively detect configuration that would leak into an official child
|
|
64
|
+
* without a cache override. Inspect all tables: a profile can supply a catalog
|
|
65
|
+
* even when the root has none. Accept indentation, quoted and dotted keys. We
|
|
66
|
+
* only need to know whether a conflicting override exists, not parse its value.
|
|
67
|
+
*/
|
|
68
|
+
function nativeAssignments(codexHome) {
|
|
69
|
+
let text;
|
|
70
|
+
try {
|
|
71
|
+
text = readFileSync(join(codexHome, 'config.toml'), 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const assignments = [];
|
|
77
|
+
for (const line of text.split('\n')) {
|
|
78
|
+
const assignment = /^\s*((?:[\w-]+|"[^"\n]+"|'[^'\n]+')(?:\s*\.\s*(?:[\w-]+|"[^"\n]+"|'[^'\n]+'))*)\s*=\s*(.*)$/.exec(line);
|
|
79
|
+
if (assignment === null)
|
|
80
|
+
continue;
|
|
81
|
+
const key = assignment[1].split(/\s*\.\s*/).at(-1)?.replace(/^["']|["']$/g, '');
|
|
82
|
+
assignments.push({ key, value: assignment[2] });
|
|
83
|
+
}
|
|
84
|
+
return assignments;
|
|
85
|
+
}
|
|
86
|
+
function nativeCatalogConflict(codexHome) {
|
|
87
|
+
return nativeAssignments(codexHome).some(({ key, value }) => key === 'model_catalog_json' || key === 'profile'
|
|
88
|
+
|| (key === 'model_provider' && !/^(?:"openai"|'openai')\s*(?:#.*)?$/.test(value)));
|
|
89
|
+
}
|
|
90
|
+
/** Official runtime cache only: never consume a third-party custom catalog. */
|
|
91
|
+
export function readOpenAiModels(codexHome) {
|
|
92
|
+
const models = [];
|
|
93
|
+
for (const raw of officialCatalogModels(codexHome) ?? []) {
|
|
94
|
+
if (raw['visibility'] !== 'list')
|
|
95
|
+
continue;
|
|
96
|
+
const slug = raw['slug'];
|
|
97
|
+
if (models.some(model => model.id === slug))
|
|
98
|
+
continue;
|
|
99
|
+
const displayName = raw['display_name'];
|
|
100
|
+
const contextWindow = raw['context_window'];
|
|
101
|
+
models.push({ id: slug,
|
|
102
|
+
...(nonEmpty(displayName) ? { displayName } : {}),
|
|
103
|
+
...(Number.isSafeInteger(contextWindow) && Number(contextWindow) > 0 ? { contextWindow: Number(contextWindow) } : {}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// A different binary can leave a valid, older catalog in the shared home.
|
|
107
|
+
// Preserve the user's official configured choice as an identity, without
|
|
108
|
+
// inventing metadata or claiming that this cache/binary supports that model.
|
|
109
|
+
if (!nativeCatalogConflict(codexHome)) {
|
|
110
|
+
const native = readNativeSelection(codexHome);
|
|
111
|
+
if ((native.providerId === undefined || native.providerId === 'openai') && nonEmpty(native.model)
|
|
112
|
+
&& !models.some(model => model.id === native.model))
|
|
113
|
+
models.push({ id: native.model });
|
|
114
|
+
}
|
|
115
|
+
return models;
|
|
116
|
+
}
|
|
117
|
+
/** Internal catalog shape; disk serialization stores just its native identity. */
|
|
118
|
+
export function openAiOAuthEntry(codexHome) {
|
|
119
|
+
return { id: OPENAI_OAUTH_ID, label: 'OpenAI OAuth', kind: 'openai-oauth', origin: 'native',
|
|
120
|
+
baseUrl: '', protocol: 'responses', auth: { kind: 'apiKey', value: '' }, models: readOpenAiModels(codexHome) };
|
|
121
|
+
}
|
|
122
|
+
export function readEffectiveProviderStore(worldHome, codexHome) {
|
|
123
|
+
const store = readProviderStore(worldHome);
|
|
124
|
+
const aliasEntries = store.providers.filter(entry => entry.id === OPENAI_OAUTH_ID);
|
|
125
|
+
const ambiguous = aliasEntries.length > 1;
|
|
126
|
+
const blocked = store.blockedNativeIds?.includes(OPENAI_OAUTH_ID) === true;
|
|
127
|
+
return { ...store, providers: store.providers.flatMap(entry => entry.id === OPENAI_OAUTH_ID && ambiguous ? [] : entry.kind === 'openai-oauth'
|
|
128
|
+
? !blocked && hasOpenAiOAuth(codexHome) ? [openAiOAuthEntry(codexHome)] : [] : [entry]) };
|
|
129
|
+
}
|
|
130
|
+
/** A usable runtime-owned catalog path, excluding malformed/empty cache files. */
|
|
131
|
+
export function openAiCatalogPath(codexHome) {
|
|
132
|
+
return officialCatalogModels(codexHome) === undefined ? undefined : join(codexHome, 'models_cache.json');
|
|
133
|
+
}
|
|
134
|
+
/** Do not let an official child inherit a third-party model_catalog_json. */
|
|
135
|
+
export function officialCatalogForLaunch(codexHome) {
|
|
136
|
+
if (!hasOpenAiOAuth(codexHome))
|
|
137
|
+
throw new Error('OpenAI OAuth authentication is unavailable; sign in using Codex');
|
|
138
|
+
if (nativeAssignments(codexHome).some(({ key, value }) => key === 'forced_login_method' && !/^(?:"chatgpt"|'chatgpt')\s*(?:#.*)?$/.test(value))) {
|
|
139
|
+
throw new Error('OpenAI OAuth cannot use a conflicting forced_login_method; choose ChatGPT login in your Codex configuration');
|
|
140
|
+
}
|
|
141
|
+
// A forced catalog is static in Codex and disables version/identity/TTL
|
|
142
|
+
// refresh. Let the selected binary own discovery when native config is safe;
|
|
143
|
+
// only override a conflicting native catalog with the validated official one.
|
|
144
|
+
if (!nativeCatalogConflict(codexHome))
|
|
145
|
+
return undefined;
|
|
146
|
+
const catalog = openAiCatalogPath(codexHome);
|
|
147
|
+
if (catalog !== undefined)
|
|
148
|
+
return catalog;
|
|
149
|
+
const native = readNativeSelection(codexHome);
|
|
150
|
+
if (nativeCatalogConflict(codexHome) || (native.providerId !== undefined && native.providerId !== 'openai'))
|
|
151
|
+
throw new Error('OpenAI OAuth model cache unavailable; open Codex with the official provider to refresh its models');
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
@@ -1,34 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* codex-provider-gateway.ts — the adapter-owned Remote behind the Hub's Codex
|
|
3
|
-
* settings panel (2026-10-03 user ruling; reworked 2026-10-04).
|
|
4
|
-
*
|
|
5
|
-
* Why a Remote and not a hub route: the provider list is ADAPTER config
|
|
6
|
-
* (dev-rules §12), so the adapter owns it and the hub only forwards.
|
|
7
|
-
* `TypertRemoteService` + `@Remote` is the whole mechanism — standard (TC39)
|
|
8
|
-
* method decorators recorded on the class prototype, read back at dispatch time
|
|
9
|
-
* by the gateway's source-claim scan. No codegen, no compiler-injected metadata,
|
|
10
|
-
* which is why a plain `tsc` adapter build can ship one.
|
|
11
|
-
*
|
|
12
|
-
* Wire contract (`superd-hub/src/client/runtime-settings-api.ts` builds each
|
|
13
|
-
* envelope; the hub carrier forwards `envelope.method` verbatim):
|
|
14
|
-
*
|
|
15
|
-
* request POST /hub/api?agent=codex
|
|
16
|
-
* { type:'client-request', rpcId, method:'codexProviders/<op>',
|
|
17
|
-
* payload: { args: { request?: … } } }
|
|
18
|
-
* response { type:'server-response', rpcId, result:{ ok:true, value:<view> } }
|
|
19
|
-
*
|
|
20
|
-
* There is no `select`: which provider a SESSION runs on is DSH's own
|
|
21
|
-
* per-session `model/selection`, chosen from the harness's model selector. This
|
|
22
|
-
* panel is a list editor — add, edit, remove — plus the reconcile with the
|
|
23
|
-
* user's `config.toml` that {@link syncFromNative} performs.
|
|
24
|
-
*
|
|
25
|
-
* The class is a delegation shell on purpose: every operation is a plain
|
|
26
|
-
* function over (worldHome, codexHome), so the entire contract — the key
|
|
27
|
-
* privacy rule, the validation, the sync, the view projection — is pinnable
|
|
28
|
-
* under `node --test` with no Cordis Context and no real home.
|
|
29
|
-
*
|
|
30
|
-
* @module agent-codex/codex-provider-gateway
|
|
31
|
-
*/
|
|
32
1
|
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
33
2
|
var useValue = arguments.length > 2;
|
|
34
3
|
for (var i = 0; i < initializers.length; i++) {
|
|
@@ -63,6 +32,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
63
32
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
64
33
|
done = true;
|
|
65
34
|
};
|
|
35
|
+
import { OPENAI_OAUTH_ID, hasOpenAiOAuth, openAiOAuthEntry } from "./codex-oauth.js";
|
|
66
36
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
67
37
|
import { isProviderId, isWireProtocol, providerStorePath, readProviderStore, removeProvider, syncNativeProviders, upsertProvider, writeProviderStore, } from "./codex-providers.js";
|
|
68
38
|
import { providerCatalogDir, readNativeProviders, readNativeSelection, } from "./codex-native-config.js";
|
|
@@ -134,6 +104,8 @@ function keyAvailable(codexHome, entry) {
|
|
|
134
104
|
return nonEmpty(process.env[entry.auth.name]) !== undefined;
|
|
135
105
|
}
|
|
136
106
|
function project(codexHome, entry) {
|
|
107
|
+
if (entry.kind === "openai-oauth")
|
|
108
|
+
return { id: entry.id, label: entry.label, protocol: "responses", models: openAiOAuthEntry(codexHome).models, authKind: "oauth", hasKey: hasOpenAiOAuth(codexHome) };
|
|
137
109
|
return {
|
|
138
110
|
id: entry.id,
|
|
139
111
|
label: entry.label,
|
|
@@ -172,10 +144,22 @@ export function syncFromNative(worldHome, codexHome) {
|
|
|
172
144
|
const store = readProviderStore(worldHome);
|
|
173
145
|
const imported = readNativeProviders(codexHome).map(toImport);
|
|
174
146
|
const result = syncNativeProviders(store, imported);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
147
|
+
const legacyAlias = result.store.providers.some(entry => entry.id === OPENAI_OAUTH_ID && entry.kind !== "openai-oauth");
|
|
148
|
+
const blockedNativeIds = [...new Set([...(result.store.blockedNativeIds ?? []), ...(legacyAlias ? [OPENAI_OAUTH_ID] : [])])];
|
|
149
|
+
const oauth = hasOpenAiOAuth(codexHome) && !blockedNativeIds.includes(OPENAI_OAUTH_ID);
|
|
150
|
+
const existed = store.providers.some(entry => entry.kind === "openai-oauth");
|
|
151
|
+
// Keep collided identities on disk so ambiguous old routes fail closed. They
|
|
152
|
+
// are quarantined by readEffectiveProviderStore, not silently repurposed.
|
|
153
|
+
const providers = result.store.providers.flatMap(entry => entry.kind === "openai-oauth"
|
|
154
|
+
? legacyAlias || oauth ? [openAiOAuthEntry(codexHome)] : [] : [entry]);
|
|
155
|
+
if (oauth && !existed)
|
|
156
|
+
providers.push(openAiOAuthEntry(codexHome));
|
|
157
|
+
const effective = { ...result.store, providers, ...(blockedNativeIds.length === 0 ? {} : { blockedNativeIds }) };
|
|
158
|
+
const blockedChanged = JSON.stringify(blockedNativeIds) !== JSON.stringify(store.blockedNativeIds ?? []);
|
|
159
|
+
const identityChanged = !legacyAlias && oauth !== existed;
|
|
160
|
+
if (result.added.length > 0 || result.updated.length > 0 || identityChanged || blockedChanged)
|
|
161
|
+
writeProviderStore(worldHome, effective);
|
|
162
|
+
return { ...result, store: effective };
|
|
179
163
|
}
|
|
180
164
|
/** The view for one (already loaded) store. */
|
|
181
165
|
function viewOf(worldHome, codexHome, store, syncedAt, note) {
|
|
@@ -189,13 +173,19 @@ function viewOf(worldHome, codexHome, store, syncedAt, note) {
|
|
|
189
173
|
// Only a SEEDED keyed entry defers to auth.json, so only it can be shadowed
|
|
190
174
|
// by a ChatGPT login. An entry holding its own key bypasses the auth manager
|
|
191
175
|
// entirely through the launch's synthetic `env_key`.
|
|
192
|
-
const deferred = store.providers.some((entry) => entry.auth.kind === "apiKey" && entry.auth.value === "");
|
|
176
|
+
const deferred = store.providers.some((entry) => entry.kind !== "openai-oauth" && entry.auth.kind === "apiKey" && entry.auth.value === "");
|
|
193
177
|
const shadowed = deferred && chatgptAuthMode(codexHome)
|
|
194
178
|
? 'Codex is signed in with a ChatGPT account, which takes precedence over a saved API key — a provider that relies on auth.json will not run until you sign out (`codex logout`) or give this provider its own key.'
|
|
195
179
|
: undefined;
|
|
196
|
-
const
|
|
180
|
+
const nativeAlias = readNativeProviders(codexHome).some(entry => entry.id === OPENAI_OAUTH_ID);
|
|
181
|
+
const blocked = store.blockedNativeIds?.includes(OPENAI_OAUTH_ID) === true;
|
|
182
|
+
const duplicate = store.providers.filter(entry => entry.id === OPENAI_OAUTH_ID).length > 1;
|
|
183
|
+
const collision = blocked || duplicate || (nativeAlias && store.providers.some(entry => entry.kind === "openai-oauth"))
|
|
184
|
+
? 'OpenAI OAuth discovery is blocked by an openai-oauth provider ID conflict. Existing third-party routes and keys are preserved. Rename the conflicting native provider table; historical session routes need explicit repair before this ID can be reused.'
|
|
185
|
+
: undefined;
|
|
186
|
+
const combined = [note, shadowed, collision].filter((part) => part !== undefined).join(" ");
|
|
197
187
|
return {
|
|
198
|
-
providers: store.providers.map((entry) => project(codexHome, entry)),
|
|
188
|
+
providers: store.providers.filter(entry => !(duplicate && entry.id === OPENAI_OAUTH_ID)).map((entry) => project(codexHome, entry)),
|
|
199
189
|
native,
|
|
200
190
|
protocols: PROTOCOL_OPTIONS,
|
|
201
191
|
paths: {
|
|
@@ -243,6 +233,8 @@ export function normalizeSaveRequest(request, existing) {
|
|
|
243
233
|
}
|
|
244
234
|
const raw = request["provider"];
|
|
245
235
|
const id = raw["id"];
|
|
236
|
+
if (id === OPENAI_OAUTH_ID && (existing === undefined || existing.kind === "openai-oauth"))
|
|
237
|
+
throw new TypeError("OpenAI OAuth is a reserved native provider; configure authentication in Codex");
|
|
246
238
|
if (!isProviderId(id)) {
|
|
247
239
|
throw new TypeError(`codexProviders/save: id must match /^[a-z0-9][a-z0-9-]*$/ (got ${JSON.stringify(id)})`);
|
|
248
240
|
}
|
|
@@ -328,6 +320,8 @@ export function normalizeSaveRequest(request, existing) {
|
|
|
328
320
|
export function saveProvider(worldHome, codexHome, request, syncedAt) {
|
|
329
321
|
const store = readProviderStore(worldHome);
|
|
330
322
|
const id = isRecord(request) && isRecord(request["provider"]) ? request["provider"]["id"] : undefined;
|
|
323
|
+
if (store.providers.filter(entry => entry.id === id).length > 1)
|
|
324
|
+
throw new TypeError("codexProviders/save: duplicate provider ID is ambiguous; explicitly repair the conflicting identities before editing");
|
|
331
325
|
const existing = store.providers.find((entry) => entry.id === id);
|
|
332
326
|
const entry = normalizeSaveRequest(request, existing);
|
|
333
327
|
const next = upsertProvider(store, entry);
|
|
@@ -340,6 +334,10 @@ export function removeProviderEntry(worldHome, codexHome, request, syncedAt) {
|
|
|
340
334
|
if (id === undefined)
|
|
341
335
|
throw new TypeError("codexProviders/remove: id must be a non-empty string");
|
|
342
336
|
const store = readProviderStore(worldHome);
|
|
337
|
+
if (id === OPENAI_OAUTH_ID && store.providers.some(entry => entry.id === id && entry.kind === "openai-oauth"))
|
|
338
|
+
throw new TypeError("OpenAI OAuth is a read-only native provider");
|
|
339
|
+
if (store.providers.filter(entry => entry.id === id).length > 1)
|
|
340
|
+
throw new TypeError("codexProviders/remove: duplicate provider ID is ambiguous; explicitly repair the conflicting identities before removing");
|
|
343
341
|
const next = removeProvider(store, id);
|
|
344
342
|
writeProviderStore(worldHome, next);
|
|
345
343
|
return viewOf(worldHome, codexHome, next, syncedAt);
|
|
@@ -99,6 +99,9 @@ export function providerEntry(value) {
|
|
|
99
99
|
if (value === null || typeof value !== "object")
|
|
100
100
|
return undefined;
|
|
101
101
|
const record = value;
|
|
102
|
+
if (record["kind"] === "openai-oauth" && record["id"] === "openai-oauth") {
|
|
103
|
+
return { id: "openai-oauth", label: "OpenAI OAuth", kind: "openai-oauth", origin: "native", baseUrl: "", protocol: "responses", auth: { kind: "apiKey", value: "" }, models: [] };
|
|
104
|
+
}
|
|
102
105
|
const id = record["id"];
|
|
103
106
|
const label = str(record["label"]);
|
|
104
107
|
const baseUrl = str(record["baseUrl"]);
|
|
@@ -144,7 +147,9 @@ export function readProviderStore(worldHome) {
|
|
|
144
147
|
const rawRemoved = record["removed"];
|
|
145
148
|
const removed = (Array.isArray(rawRemoved) ? rawRemoved : [])
|
|
146
149
|
.filter((value) => typeof value === "string" && value.trim() !== "");
|
|
147
|
-
|
|
150
|
+
const blockedNativeIds = Array.isArray(record["blockedNativeIds"])
|
|
151
|
+
? [...new Set(record["blockedNativeIds"].filter((id) => id === "openai-oauth"))] : [];
|
|
152
|
+
return { version: 1, providers, ...(removed.length === 0 ? {} : { removed }), ...(blockedNativeIds.length === 0 ? {} : { blockedNativeIds }) };
|
|
148
153
|
}
|
|
149
154
|
/**
|
|
150
155
|
* Persist the store atomically (tmp + rename) and 0600 — the file holds API
|
|
@@ -155,7 +160,7 @@ export function writeProviderStore(worldHome, store) {
|
|
|
155
160
|
const path = providerStorePath(worldHome);
|
|
156
161
|
mkdirSync(dirname(path), { recursive: true });
|
|
157
162
|
const temp = `${path}.tmp-${process.pid}`;
|
|
158
|
-
writeFileSync(temp, `${JSON.stringify(store, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
163
|
+
writeFileSync(temp, `${JSON.stringify({ ...store, providers: store.providers.map(entry => entry.kind === "openai-oauth" ? { id: entry.id, label: entry.label, kind: entry.kind, origin: "native" } : entry) }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
159
164
|
renameSync(temp, path);
|
|
160
165
|
}
|
|
161
166
|
/**
|
|
@@ -176,7 +181,8 @@ export function upsertProvider(store, entry) {
|
|
|
176
181
|
// carry the tombstone forward, and the next sync would keep skipping the
|
|
177
182
|
// endpoint the user just re-added.
|
|
178
183
|
const { removed: _stale, ...rest } = store;
|
|
179
|
-
|
|
184
|
+
const blockedNativeIds = [...new Set([...(store.blockedNativeIds ?? []), ...(entry.id === "openai-oauth" && entry.kind !== "openai-oauth" ? [entry.id] : [])])];
|
|
185
|
+
return { ...rest, providers, ...(removed.length === 0 ? {} : { removed }), ...(blockedNativeIds.length === 0 ? {} : { blockedNativeIds }) };
|
|
180
186
|
}
|
|
181
187
|
/**
|
|
182
188
|
* Drop one provider, and TOMBSTONE its endpoint so the reconcile cannot bring
|
|
@@ -193,7 +199,8 @@ export function removeProvider(store, id) {
|
|
|
193
199
|
return { ...store, providers };
|
|
194
200
|
const key = endpointKey(entry.baseUrl);
|
|
195
201
|
const removed = store.removed?.includes(key) === true ? store.removed : [...store.removed ?? [], key];
|
|
196
|
-
|
|
202
|
+
const blockedNativeIds = [...new Set([...(store.blockedNativeIds ?? []), ...(entry.id === "openai-oauth" && entry.kind !== "openai-oauth" ? [entry.id] : [])])];
|
|
203
|
+
return { ...store, providers, removed, ...(blockedNativeIds.length === 0 ? {} : { blockedNativeIds }) };
|
|
197
204
|
}
|
|
198
205
|
/**
|
|
199
206
|
* The comparison key for "the same endpoint". Codex itself treats two provider
|
|
@@ -330,7 +337,13 @@ export function syncNativeProviders(store, imported) {
|
|
|
330
337
|
if (store.removed?.includes(key) === true)
|
|
331
338
|
continue;
|
|
332
339
|
const wantedId = incoming.nativeId !== undefined && isProviderId(incoming.nativeId) ? incoming.nativeId : undefined;
|
|
333
|
-
|
|
340
|
+
// A native identity is not an endpoint table. Never refresh it, rename a
|
|
341
|
+
// conflicting native table behind the user's back, or select a duplicate id.
|
|
342
|
+
if (wantedId === "openai-oauth" && (providers.some(entry => entry.id === wantedId && entry.kind === "openai-oauth")
|
|
343
|
+
|| providers.filter(entry => entry.id === wantedId).length > 1))
|
|
344
|
+
continue;
|
|
345
|
+
const at = providers.findIndex((entry) => entry.kind !== "openai-oauth" &&
|
|
346
|
+
((wantedId !== undefined && entry.id === wantedId) || endpointKey(entry.baseUrl) === key));
|
|
334
347
|
if (at === -1) {
|
|
335
348
|
const id = uniqueProviderId(wantedId ?? providerIdFromEndpoint(baseUrl), providers.map((entry) => entry.id));
|
|
336
349
|
providers.push({
|
|
@@ -370,7 +383,7 @@ export function syncNativeProviders(store, imported) {
|
|
|
370
383
|
}
|
|
371
384
|
const removed = store.removed ?? [];
|
|
372
385
|
const next = removed.length === 0
|
|
373
|
-
? { version: 1, providers }
|
|
374
|
-
: { version: 1, providers, removed };
|
|
386
|
+
? { ...store, version: 1, providers }
|
|
387
|
+
: { ...store, version: 1, providers, removed };
|
|
375
388
|
return { store: next, added, updated };
|
|
376
389
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Resolve an installed Codex runtime at first use, without modifying its home. */
|
|
2
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { posix, win32 } from "node:path";
|
|
6
|
+
function readableFile(path) {
|
|
7
|
+
try {
|
|
8
|
+
accessSync(path, constants.R_OK);
|
|
9
|
+
return statSync(path).isFile();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function executable(path) {
|
|
16
|
+
try {
|
|
17
|
+
accessSync(path, constants.X_OK);
|
|
18
|
+
return statSync(path).isFile();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function desktopPaths(discovery) {
|
|
25
|
+
if (discovery.desktopCandidates !== undefined)
|
|
26
|
+
return discovery.desktopCandidates;
|
|
27
|
+
const explicit = discovery.env.CODEX_DESKTOP_BIN?.trim();
|
|
28
|
+
const paths = explicit ? [explicit] : [];
|
|
29
|
+
if (discovery.platform === "linux") {
|
|
30
|
+
paths.push("/usr/lib/chatgpt/resources/codex", "/usr/lib/codex/resources/codex", "/opt/Codex/resources/codex", "/opt/codex/resources/codex");
|
|
31
|
+
}
|
|
32
|
+
else if (discovery.platform === "darwin") {
|
|
33
|
+
for (const root of ["/Applications", posix.join(discovery.homeDir, "Applications")]) {
|
|
34
|
+
for (const app of ["Codex.app", "ChatGPT.app"])
|
|
35
|
+
paths.push(posix.join(root, app, "Contents/Resources/codex"));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (discovery.platform === "win32") {
|
|
39
|
+
const local = discovery.env.LOCALAPPDATA;
|
|
40
|
+
if (local) {
|
|
41
|
+
for (const app of ["Codex", "ChatGPT"])
|
|
42
|
+
paths.push(win32.join(local, "Programs", app, "resources", "codex.exe"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return paths;
|
|
46
|
+
}
|
|
47
|
+
function isNodeScript(command, discovery) {
|
|
48
|
+
let target = command;
|
|
49
|
+
try {
|
|
50
|
+
target = discovery.realpath(command);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Explicit nonexistent paths are rejected by the file check below.
|
|
54
|
+
}
|
|
55
|
+
return /\.(?:[cm]?js)$/i.test(target);
|
|
56
|
+
}
|
|
57
|
+
export function resolveCodexRuntime(options = {}) {
|
|
58
|
+
const platform = options.platform ?? process.platform;
|
|
59
|
+
const env = options.env ?? process.env;
|
|
60
|
+
const isFile = options.isFile ?? readableFile;
|
|
61
|
+
const isExecutable = options.isExecutable ?? executable;
|
|
62
|
+
const path = platform === "win32" ? win32 : posix;
|
|
63
|
+
const shellWrapper = (command) => platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
|
|
64
|
+
const discovery = {
|
|
65
|
+
platform,
|
|
66
|
+
env,
|
|
67
|
+
homeDir: options.homeDir ?? homedir(),
|
|
68
|
+
...(options.desktopCandidates === undefined ? {} : { desktopCandidates: options.desktopCandidates }),
|
|
69
|
+
isFile,
|
|
70
|
+
isExecutable,
|
|
71
|
+
realpath: options.realpath ?? realpathSync,
|
|
72
|
+
findCommand: options.findCommand ?? ((name) => {
|
|
73
|
+
const suffixes = platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
|
|
74
|
+
for (const directory of (env.PATH ?? "").split(platform === "win32" ? ";" : ":")) {
|
|
75
|
+
if (directory === "")
|
|
76
|
+
continue;
|
|
77
|
+
for (const suffix of suffixes) {
|
|
78
|
+
const candidate = path.join(directory, `${name}${suffix}`);
|
|
79
|
+
if (shellWrapper(candidate))
|
|
80
|
+
continue;
|
|
81
|
+
if (isFile(candidate) && isExecutable(candidate))
|
|
82
|
+
return candidate;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}),
|
|
87
|
+
resolvePackageEntry: options.resolvePackageEntry ?? (() => createRequire(import.meta.url).resolve("@openai/codex/bin/codex.js")),
|
|
88
|
+
};
|
|
89
|
+
const usable = (command) => !shellWrapper(command) && isFile(command) && (isNodeScript(command, discovery) || isExecutable(command));
|
|
90
|
+
const selected = (command, source) => ({ command, nodeScript: isNodeScript(command, discovery), source });
|
|
91
|
+
const override = env.CODEX_BIN?.trim();
|
|
92
|
+
if (override) {
|
|
93
|
+
const command = /[\\/]/.test(override) ? override : discovery.findCommand(override) ?? override;
|
|
94
|
+
if (shellWrapper(command))
|
|
95
|
+
throw new Error(`CODEX_BIN ${override} is a Windows .cmd/.bat shell wrapper. Set CODEX_BIN to the native codex.exe executable or the @openai/codex JavaScript entry instead.`);
|
|
96
|
+
if (!usable(command))
|
|
97
|
+
throw new Error(`CODEX_BIN ${override} is not a readable JavaScript entry or executable Codex runtime. Fix CODEX_BIN or remove the override.`);
|
|
98
|
+
return selected(command, "override");
|
|
99
|
+
}
|
|
100
|
+
for (const candidate of desktopPaths(discovery)) {
|
|
101
|
+
if (!shellWrapper(candidate) && isFile(candidate) && isExecutable(candidate))
|
|
102
|
+
return selected(candidate, "desktop");
|
|
103
|
+
}
|
|
104
|
+
const cli = discovery.findCommand("codex");
|
|
105
|
+
if (cli !== undefined && usable(cli))
|
|
106
|
+
return selected(cli, "cli");
|
|
107
|
+
try {
|
|
108
|
+
const entry = discovery.resolvePackageEntry();
|
|
109
|
+
if (isFile(entry))
|
|
110
|
+
return { command: entry, nodeScript: true, source: "package" };
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Installation fallback only; authentication/generation never reaches here.
|
|
114
|
+
}
|
|
115
|
+
throw new Error("cannot locate a Codex runtime: install Codex desktop or a user CLI on PATH, restore the exact-pin @openai/codex package, or set CODEX_BIN to its executable or JavaScript entry.");
|
|
116
|
+
}
|
|
117
|
+
/** Compatibility interface for callers that need only the selected path. */
|
|
118
|
+
export function resolveCodexCommand(options = {}) {
|
|
119
|
+
return resolveCodexRuntime(options).command;
|
|
120
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OPENAI_OAUTH_ID, readEffectiveProviderStore } from "./codex-oauth.js";
|
|
1
2
|
/**
|
|
2
3
|
* agent-codex provider — Codex provider plugin for DeepSeek Harness
|
|
3
4
|
* (Super D, app-server line).
|
|
@@ -29,7 +30,6 @@ import { CodexLlmAdapter, routeIds } from "./adapter.js";
|
|
|
29
30
|
import { registerCodexPreset } from "./agent-preset-codex.js";
|
|
30
31
|
import { readRolloutHead, resolveCodexHome } from "./codex-store.js";
|
|
31
32
|
import { CodexProvidersGateway, syncFromNative } from "./codex-provider-gateway.js";
|
|
32
|
-
import { readProviderStore } from "./codex-providers.js";
|
|
33
33
|
import { readNativeSelection } from "./codex-native-config.js";
|
|
34
34
|
import { seedRoute } from "./codex-route.js";
|
|
35
35
|
import { providerLaunch } from "./codex-launch.js";
|
|
@@ -159,12 +159,12 @@ export class CodexProvider extends Service {
|
|
|
159
159
|
*/
|
|
160
160
|
syncNow() {
|
|
161
161
|
try {
|
|
162
|
-
const {
|
|
162
|
+
const { added, updated } = syncFromNative(this.home, resolveCodexHome());
|
|
163
163
|
if (added.length > 0 || updated.length > 0) {
|
|
164
164
|
this.runtime.ctx.logger.info(`codex-provider: config.toml sync — added ${added.join(", ") || "none"}, refreshed ${updated.join(", ") || "none"}`);
|
|
165
165
|
}
|
|
166
166
|
this.syncedAt = Date.now();
|
|
167
|
-
this.#applyRoutes(
|
|
167
|
+
this.#applyRoutes(readEffectiveProviderStore(this.home, resolveCodexHome()).providers);
|
|
168
168
|
}
|
|
169
169
|
catch (error) {
|
|
170
170
|
this.runtime.ctx.logger.warn(`codex-provider: provider sync failed: ${String(error)}`);
|
|
@@ -173,7 +173,7 @@ export class CodexProvider extends Service {
|
|
|
173
173
|
/** Re-register the `ctx.llm` routes from the current store (panel mutation path). */
|
|
174
174
|
refreshRoutes() {
|
|
175
175
|
try {
|
|
176
|
-
this.#applyRoutes(
|
|
176
|
+
this.#applyRoutes(readEffectiveProviderStore(this.home, resolveCodexHome()).providers);
|
|
177
177
|
}
|
|
178
178
|
catch (error) {
|
|
179
179
|
this.runtime.ctx.logger.warn(`codex-provider: route refresh failed: ${String(error)}`);
|
|
@@ -188,14 +188,14 @@ export class CodexProvider extends Service {
|
|
|
188
188
|
nativeProviderId() {
|
|
189
189
|
const native = readNativeSelection(resolveCodexHome());
|
|
190
190
|
const wanted = native.providerId;
|
|
191
|
-
if (wanted === undefined)
|
|
192
|
-
return undefined;
|
|
193
|
-
const providers =
|
|
194
|
-
return providers.find((entry) => entry.id === wanted)?.id ?? undefined;
|
|
191
|
+
if (wanted === undefined || wanted === "openai")
|
|
192
|
+
return readEffectiveProviderStore(this.home, resolveCodexHome()).providers.some(entry => entry.kind === "openai-oauth") ? OPENAI_OAUTH_ID : undefined;
|
|
193
|
+
const providers = readEffectiveProviderStore(this.home, resolveCodexHome()).providers;
|
|
194
|
+
return providers.find((entry) => entry.kind !== "openai-oauth" && entry.id === wanted)?.id ?? undefined;
|
|
195
195
|
}
|
|
196
196
|
/** The store entry a DSH route id names. */
|
|
197
197
|
providerEntry(providerId) {
|
|
198
|
-
return
|
|
198
|
+
return readEffectiveProviderStore(this.home, resolveCodexHome()).providers.find((entry) => entry.id === providerId);
|
|
199
199
|
}
|
|
200
200
|
/**
|
|
201
201
|
* The factory's session-level route fallback — always ONE coherent pair.
|
|
@@ -217,8 +217,13 @@ export class CodexProvider extends Service {
|
|
|
217
217
|
sessionAgentOptions(requested) {
|
|
218
218
|
const native = readNativeSelection(resolveCodexHome());
|
|
219
219
|
const wanted = requested?.provider !== undefined && typeof requested.model === "string" ? requested : undefined;
|
|
220
|
+
const requestedEntry = wanted === undefined ? undefined : this.providerEntry(wanted.provider);
|
|
221
|
+
// An explicitly selected native identity must survive unavailable auth or
|
|
222
|
+
// metadata. Its launch fails honestly instead of seeding a third-party route.
|
|
223
|
+
if (wanted?.provider === OPENAI_OAUTH_ID && (requestedEntry === undefined || requestedEntry.kind === "openai-oauth"))
|
|
224
|
+
return { ...wanted };
|
|
220
225
|
const serves = wanted !== undefined
|
|
221
|
-
&&
|
|
226
|
+
&& requestedEntry?.models.some((model) => model.id === wanted.model) === true;
|
|
222
227
|
if (wanted !== undefined && serves)
|
|
223
228
|
return { ...wanted, provider: wanted.provider, model: wanted.model };
|
|
224
229
|
const provider = this.nativeProviderId();
|
|
@@ -274,6 +279,8 @@ export class CodexProvider extends Service {
|
|
|
274
279
|
// the user's home (the per-provider model catalog is ours).
|
|
275
280
|
launchFor: (providerId) => {
|
|
276
281
|
const entry = this.providerEntry(providerId);
|
|
282
|
+
if (entry === undefined && providerId === OPENAI_OAUTH_ID)
|
|
283
|
+
throw new Error("OpenAI OAuth authentication is unavailable; sign in using Codex");
|
|
277
284
|
return entry === undefined ? undefined : providerLaunch(this.home, codexHome, entry);
|
|
278
285
|
},
|
|
279
286
|
contextWindowFor: (providerId, model) => {
|
|
@@ -379,7 +386,7 @@ export class CodexProvider extends Service {
|
|
|
379
386
|
* picker would be noise.
|
|
380
387
|
*/
|
|
381
388
|
#registerModelCatalog() {
|
|
382
|
-
this.#applyRoutes(
|
|
389
|
+
this.#applyRoutes(readEffectiveProviderStore(this.home, resolveCodexHome()).providers);
|
|
383
390
|
}
|
|
384
391
|
/** Register or atomically re-point the codex routes at the given providers. */
|
|
385
392
|
#applyRoutes(providers) {
|
package/agent-codex/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readEffectiveProviderStore } from "./codex-oauth.js";
|
|
2
|
+
import { resolveCodexHome } from "./codex-store.js";
|
|
1
3
|
/**
|
|
2
4
|
* Codex-backed `LlmAdapter` registered on `ctx.llm` by the Codex provider.
|
|
3
5
|
*
|
|
@@ -23,7 +25,6 @@
|
|
|
23
25
|
* @module agent-codex/adapter
|
|
24
26
|
*/
|
|
25
27
|
import { LlmAdapter, LlmError, } from "@deepseek-ai/dsh-llm";
|
|
26
|
-
import { readProviderStore } from "./codex-providers.js";
|
|
27
28
|
/**
|
|
28
29
|
* The single route every Codex model was served under before the multi-provider
|
|
29
30
|
* rework. No longer registered (a session pinned to it falls back to whatever
|
|
@@ -41,7 +42,7 @@ export function routeIds(store) {
|
|
|
41
42
|
}
|
|
42
43
|
/** The provider entry a route id names, or `undefined`. */
|
|
43
44
|
export function providerFor(worldHome, providerId) {
|
|
44
|
-
return
|
|
45
|
+
return readEffectiveProviderStore(worldHome, resolveCodexHome()).providers.find((entry) => entry.id === providerId);
|
|
45
46
|
}
|
|
46
47
|
export class CodexLlmAdapter extends LlmAdapter {
|
|
47
48
|
worldHome;
|