mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(custom-model): guard, document and test the session-supplied dsh home
Second review round on PR #755: skip non-absolute DSH_HOME candidates so a relative home can never mkdir under the server cwd; probe a session-supplied home with probePathKind in both async routes before the synchronous apply; test the session env-override lookup (seventh arg) incl. the self-link fall-through and the relative-home skip; reject '.' and '..' as realHomeFallback and enforce linkFromRealHomeDirs subset in the schema; fix the stale apply JSDoc and the two doc lines lagging the verified dsh run; state the shared/isolated split plainly.
This commit is contained in:
1 parent
1464332e91
commit
fa2b81484b
9 files changed
+189
-29
No files matched your search
@@ -66,8 +66,10 @@ function expectRejectedOnDeepSeek(mutate: (recipe: Record<string, unknown>) => v
|
||||
|
||||
describe('configDir real-home linking (linkFromRealHome / realHomeFallback)', () => {
|
||||
it('rejects a realHomeFallback that is not a single relative path segment', () => {
|
||||
// The consumer joins it under the user's home, so any of these escapes that home.
|
||||
for (const bad of ['../x', '.dsh/evil', '/.dsh', '..']) {
|
||||
// The consumer joins it under the user's home, so any of these escapes that home —
|
||||
// and bare `.` IS that home: linking through it would expose the whole tree
|
||||
// unshielded instead of just the declared segments.
|
||||
for (const bad of ['../x', '.dsh/evil', '/.dsh', '..', '.']) {
|
||||
expectRejectedOnDeepSeek((r) => {
|
||||
r.realHomeFallback = bad;
|
||||
}, `realHomeFallback ${JSON.stringify(bad)}`);
|
||||
@@ -94,6 +96,22 @@ describe('configDir real-home linking (linkFromRealHome / realHomeFallback)', ()
|
||||
r.linkFromRealHome = ['cordis.patch.yml', 'profiles'];
|
||||
}, 'linkFromRealHome containing fileName');
|
||||
});
|
||||
|
||||
it('requires linkFromRealHomeDirs to be a subset of linkFromRealHome', () => {
|
||||
// types.ts documents the rule; a name absent from the linked list names nothing,
|
||||
// so the create-missing-directory behaviour would silently never apply to it.
|
||||
expectRejectedOnDeepSeek((r) => {
|
||||
r.linkFromRealHome = ['profiles', 'settings.yaml'];
|
||||
r.linkFromRealHomeDirs = ['profiles', 'sessions'];
|
||||
}, 'linkFromRealHomeDirs naming an unlinked entry');
|
||||
// The shipped recipe is a valid subset (deepseek: profiles and sessions are dirs
|
||||
// of the three linked entries) and keeps loading.
|
||||
const entry = baseEntry('deepseek');
|
||||
const recipe = (entry.capabilities as Record<string, unknown>).customModelInjection as Record<string, unknown>;
|
||||
expect(recipe.linkFromRealHome).toEqual(['profiles', 'sessions', 'settings.yaml']);
|
||||
expect(recipe.linkFromRealHomeDirs).toEqual(['profiles', 'sessions']);
|
||||
expect(CliEntrySchema.safeParse(entry).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the shipped catalog', () => {
|
||||
|
||||
@@ -106,6 +106,95 @@ describe('DeepSeek custom endpoint isolation', () => {
|
||||
rmSync(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('links from the SESSION-supplied DSH_HOME, not the server environment one', () => {
|
||||
// Both web routes pass the session's own env-override lookup as the seventh
|
||||
// argument; a user who relocated the dsh home via an allowlisted DSH_HOME
|
||||
// override must get their state linked from THAT directory, not the server's.
|
||||
const sandbox = mkdtempSync(join(tmpdir(), 'dsh-session-home-'));
|
||||
const serverHome = join(sandbox, 'server-home');
|
||||
const sessionHome = join(sandbox, 'session-home');
|
||||
mkdirSync(join(sessionHome, 'sessions'), { recursive: true });
|
||||
mkdirSync(join(sessionHome, 'profiles'), { recursive: true });
|
||||
writeFileSync(join(sessionHome, 'settings.yaml'), 'theme: light');
|
||||
vi.stubEnv('CODEMAN_DATA_DIR', join(sandbox, 'data'));
|
||||
vi.stubEnv('DSH_HOME', serverHome);
|
||||
try {
|
||||
const entry = getCli('deepseek');
|
||||
if (!entry) throw new Error('Missing DeepSeek entry');
|
||||
const endpoint = { id: 'local', label: 'Local', baseUrl: 'http://box:8080', apiKey: 'private-probe-key' };
|
||||
const applied = applyCustomModelInjection(
|
||||
entry,
|
||||
endpoint,
|
||||
'chosen-model',
|
||||
'deepseek-probe',
|
||||
131072,
|
||||
undefined,
|
||||
(k) => (k === 'DSH_HOME' ? sessionHome : undefined)
|
||||
);
|
||||
if (!applied?.configDir) throw new Error('Missing isolated profile');
|
||||
expect(realpathSync(join(applied.configDir, 'sessions'))).toBe(join(sessionHome, 'sessions'));
|
||||
expect(realpathSync(join(applied.configDir, 'profiles'))).toBe(join(sessionHome, 'profiles'));
|
||||
expect(realpathSync(join(applied.configDir, 'settings.yaml'))).toBe(join(sessionHome, 'settings.yaml'));
|
||||
// The server-environment home was never consulted, let alone created.
|
||||
expect(existsSync(serverHome)).toBe(false);
|
||||
} finally {
|
||||
rmSync(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('never self-links when the session override IS its own config dir; falls through to the server home', () => {
|
||||
// Boot recovery re-applies onto a session already holding a previous injection's
|
||||
// DSH_HOME value; linking that value would point the isolated directory at itself.
|
||||
const sandbox = mkdtempSync(join(tmpdir(), 'dsh-self-home-'));
|
||||
const serverHome = join(sandbox, 'server-home');
|
||||
mkdirSync(join(serverHome, 'sessions'), { recursive: true });
|
||||
vi.stubEnv('CODEMAN_DATA_DIR', join(sandbox, 'data'));
|
||||
vi.stubEnv('DSH_HOME', serverHome);
|
||||
try {
|
||||
const entry = getCli('deepseek');
|
||||
if (!entry) throw new Error('Missing DeepSeek entry');
|
||||
const endpoint = { id: 'local', label: 'Local', baseUrl: 'http://box:8080', apiKey: 'private-probe-key' };
|
||||
const applied = applyCustomModelInjection(
|
||||
entry,
|
||||
endpoint,
|
||||
'chosen-model',
|
||||
'deepseek-probe',
|
||||
131072,
|
||||
undefined,
|
||||
(k) => (k === 'DSH_HOME' ? customModelConfigDir('deepseek-probe') : undefined)
|
||||
);
|
||||
if (!applied?.configDir) throw new Error('Missing isolated profile');
|
||||
expect(realpathSync(join(applied.configDir, 'sessions'))).toBe(join(serverHome, 'sessions'));
|
||||
} finally {
|
||||
rmSync(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('skips a relative DSH_HOME rather than creating it under the server cwd', () => {
|
||||
// A session may supply DSH_HOME; `work` would otherwise mkdir `work/sessions` in
|
||||
// the SERVER's working directory and leave relative links that dangle (a relative
|
||||
// home resolves against the CLI's cwd, which the server cannot know). Skipping it
|
||||
// falls through to the recipe's `realHomeFallback`, and nothing is created under cwd.
|
||||
const sandbox = mkdtempSync(join(tmpdir(), 'dsh-relative-home-'));
|
||||
const fallbackHome = join(sandbox, '.dsh');
|
||||
mkdirSync(fallbackHome, { recursive: true });
|
||||
const relativeHome = `dsh-relative-probe-${process.pid}-${Date.now()}`;
|
||||
vi.stubEnv('HOME', sandbox); // the fallback resolves under homedir()
|
||||
vi.stubEnv('CODEMAN_DATA_DIR', join(sandbox, 'data'));
|
||||
vi.stubEnv('DSH_HOME', relativeHome);
|
||||
try {
|
||||
const entry = getCli('deepseek');
|
||||
if (!entry) throw new Error('Missing DeepSeek entry');
|
||||
const endpoint = { id: 'local', label: 'Local', baseUrl: 'http://box:8080', apiKey: 'private-probe-key' };
|
||||
const applied = applyCustomModelInjection(entry, endpoint, 'chosen-model', 'deepseek-probe', 131072);
|
||||
if (!applied?.configDir) throw new Error('Missing isolated profile');
|
||||
expect(existsSync(join(process.cwd(), relativeHome))).toBe(false);
|
||||
expect(realpathSync(join(applied.configDir, 'sessions'))).toBe(join(fallbackHome, 'sessions'));
|
||||
} finally {
|
||||
rmSync(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** readlinkSync without the throw an absent link causes (ENOENT -> undefined). */
|
||||
|
||||
Reference in new issue
Block a user