feat(server): agent-definition loader (Phase 1)#363
Conversation
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 8 issue(s) (1 critical) (4 warning).
server/agent-loader.ts
The agent-loader module is well-structured with good test coverage, but has a critical bug: it imports from the yaml package which is not installed (the project uses js-yaml). The local YAML fallback path will fail at runtime, hidden by test mocks. Secondary concerns: const → let without mutation, hardcoded fallback identity name, and cache key missing the server URL.
- 🔴 bugs (L14): Imports
parsefrom'yaml'but the project depends on'js-yaml'(see root package.json andserver/session-index.tswhich imports from'js-yaml'). Theyamlpackage is not installed —loadFromLocal()will fail at runtime with a module-not-found error. The test mocks theyamlmodule so this is invisible in tests. Should beimport { load as parseYaml } from 'js-yaml'(note:js-yamlexportsload, notparse).[fixable] - 🟡 unsafe_assumptions (L208): Cache key is
${agentName}:${cwd}but does not includecontexginUrl. IfloadAgentDefis ever called with differentcontexginUrlvalues for the same agent+cwd pair, the cache will return the result from the first server. Currently safe becausecontexginUrlis always the default, but fragile if the API surface expands.[fixable] - 🟡 bugs (L239): The bundled fallback always returns
DEFAULT_AGENT_DEFINITIONwhich hardcodesidentity.name: 'mitzo-conversational', regardless of theagentNameparameter. If a different agent name is requested and all sources fail, the returned definition has a mismatched name. Consider spreading the constant and overridingidentity.namewith the requestedagentName.[fixable] - 🟡 unsafe_assumptions (L127): ContexGin response fields (
identity,provider,governance,memory) are cast directly to typed interfaces viaaswithout runtime validation. A malformed ContexGin response (e.g.,identity.nameis a number) would pass the null-check but produce a silently incorrectAgentDefinition. Theidentity?.nameandidentity?.descriptiontruthy checks catch null/undefined but not wrong types.[fixable]
server/chat.ts
The agent-loader module is well-structured with good test coverage, but has a critical bug: it imports from the yaml package which is not installed (the project uses js-yaml). The local YAML fallback path will fail at runtime, hidden by test mocks. Secondary concerns: const → let without mutation, hardcoded fallback identity name, and cache key missing the server URL.
- 🔵 style (L888):
systemPromptAppendwas changed fromconsttoletbut is never reassigned anywhere in the function. Should remainconst.[fixable] - 🟡 regressions (L920): The refactored
fetchBootContextno longer passesrepoRootwhen called from_startChatInner— it uses the defaultBASE_REPO. The old code usedcwd(which may be a worktree path). If the local Python fallback script (build_boot_context.py) needs to run from the worktree rather than BASE_REPO, this changes behavior. Verify the script works correctly when run from BASE_REPO even when the session is in a worktree.[fixable]
server/__tests__/agent-loader.test.ts
The agent-loader module is well-structured with good test coverage, but has a critical bug: it imports from the yaml package which is not installed (the project uses js-yaml). The local YAML fallback path will fail at runtime, hidden by test mocks. Secondary concerns: const → let without mutation, hardcoded fallback identity name, and cache key missing the server URL.
- 🔵 missing_tests: No test for cache TTL expiration — a cached entry past its 5-minute TTL should trigger a re-fetch. The existing cache tests only verify basic hit/miss and
clearCache(), not time-based expiry.[fixable]
packages/harness/src/session-registry.ts
The agent-loader module is well-structured with good test coverage, but has a critical bug: it imports from the yaml package which is not installed (the project uses js-yaml). The local YAML fallback path will fail at runtime, hidden by test mocks. Secondary concerns: const → let without mutation, hardcoded fallback identity name, and cache key missing the server URL.
- 🔵 style (L34):
agentDefinitionis typed asRecord<string, unknown>in ManagedSession but the loader produces a well-typedAgentDefinition. Consider importing and usingAgentDefinitionfrom agent-loader to preserve type safety end-to-end, instead of the double cast throughunknownat chat.ts:903.[fixable]
|
|
||
| import { readFileSync } from 'fs'; | ||
| import { join } from 'path'; | ||
| import { parse as parseYaml } from 'yaml'; |
There was a problem hiding this comment.
🔴 bugs: Imports parse from 'yaml' but the project depends on 'js-yaml' (see root package.json and server/session-index.ts which imports from 'js-yaml'). The yaml package is not installed — loadFromLocal() will fail at runtime with a module-not-found error. The test mocks the yaml module so this is invisible in tests. Should be import { load as parseYaml } from 'js-yaml' (note: js-yaml exports load, not parse). [fixable]
There was a problem hiding this comment.
Fixed — changed to import { load as parseYaml } from 'js-yaml' matching the rest of the codebase.
| contexginUrl: string = process.env.CONTEXGIN_URL || 'http://localhost:8321', | ||
| ): Promise<LoadedAgentDefinition> { | ||
| // Check cache | ||
| const cacheKey = `${agentName}:${cwd}`; |
There was a problem hiding this comment.
🟡 unsafe_assumptions: Cache key is ${agentName}:${cwd} but does not include contexginUrl. If loadAgentDef is ever called with different contexginUrl values for the same agent+cwd pair, the cache will return the result from the first server. Currently safe because contexginUrl is always the default, but fragile if the API surface expands. [fixable]
There was a problem hiding this comment.
Fixed — cache key now includes contexginUrl: ${agentName}:${cwd}:${contexginUrl}.
| // 3. Bundled fallback | ||
| log.info('using bundled agent definition fallback', { agent: agentName }); | ||
| const fallback: LoadedAgentDefinition = { | ||
| definition: DEFAULT_AGENT_DEFINITION, |
There was a problem hiding this comment.
🟡 bugs: The bundled fallback always returns DEFAULT_AGENT_DEFINITION which hardcodes identity.name: 'mitzo-conversational', regardless of the agentName parameter. If a different agent name is requested and all sources fail, the returned definition has a mismatched name. Consider spreading the constant and overriding identity.name with the requested agentName. [fixable]
There was a problem hiding this comment.
Fixed — fallback now spreads DEFAULT_AGENT_DEFINITION and overrides identity.name with the requested agentName.
|
|
||
| if (!res.ok) return null; | ||
|
|
||
| const data = (await res.json()) as Record<string, unknown>; |
There was a problem hiding this comment.
🟡 unsafe_assumptions: ContexGin response fields (identity, provider, governance, memory) are cast directly to typed interfaces via as without runtime validation. A malformed ContexGin response (e.g., identity.name is a number) would pass the null-check but produce a silently incorrect AgentDefinition. The identity?.name and identity?.description truthy checks catch null/undefined but not wrong types. [fixable]
There was a problem hiding this comment.
Acknowledged — runtime validation of ContexGin response fields is a valid concern. The current truthy checks catch null/undefined which covers the most common failure modes. Full schema validation could be a Phase 2 addition.
|
|
||
| // Build the system prompt append string (used by both query and comparison) | ||
| const systemPromptAppend = | ||
| let systemPromptAppend = |
There was a problem hiding this comment.
🔵 style: systemPromptAppend was changed from const to let but is never reassigned anywhere in the function. Should remain const. [fixable]
There was a problem hiding this comment.
Fixed — systemPromptAppend restored to const (resolved during rebase).
| }); | ||
|
|
||
| // Fire-and-forget: fetch boot context from running ContexGin server | ||
| fetchBootContext(agentName) |
There was a problem hiding this comment.
🟡 regressions: The refactored fetchBootContext no longer passes repoRoot when called from _startChatInner — it uses the default BASE_REPO. The old code used cwd (which may be a worktree path). If the local Python fallback script (build_boot_context.py) needs to run from the worktree rather than BASE_REPO, this changes behavior. Verify the script works correctly when run from BASE_REPO even when the session is in a worktree. [fixable]
There was a problem hiding this comment.
This is the same observation as PR #361 (now merged). The fallback running against BASE_REPO is correct since the script reads canonical source files, not worktree-specific content.
| /** All worktrees created for this session, keyed by repo name. */ | ||
| worktreePaths: Map<string, { path: string; wtId: string }>; | ||
| /** Parsed agent definition (recipe). Null if loading failed. */ | ||
| agentDefinition: Record<string, unknown> | null; |
There was a problem hiding this comment.
🔵 style: agentDefinition is typed as Record<string, unknown> in ManagedSession but the loader produces a well-typed AgentDefinition. Consider importing and using AgentDefinition from agent-loader to preserve type safety end-to-end, instead of the double cast through unknown at chat.ts:903. [fixable]
There was a problem hiding this comment.
Partially addressed — agentDefinitionSource now uses the concrete union type 'contexgin' | 'local' | 'fallback'. For agentDefinition, keeping Record<string, unknown> to avoid cross-package type duplication (the AgentDefinition interface lives in server/, harness is a separate package). Full type sharing is a Phase 2 concern.
8066d4d to
cbca1e5
Compare
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 6 issue(s) (2 warning).
server/agent-loader.ts
Solid Phase 1 with good test coverage and graceful degradation; main concerns are type erasure through Record<string, unknown> on the session interface (will compound in Phase 2) and an inherited path-traversal pattern on the user-controlled agentName.
- 🟡 unsafe_assumptions (L164): Path traversal via user-controlled
agentName:join(cwd, '.agents',${agentName}.yaml)does not validate that the resolved path stays undercwd/.agents/. An authenticated WebSocket client sendingagentName: '../../../etc/something'could read arbitrary.yamlfiles on disk. The same pattern already exists atchat.ts:825, so this is pre-existing, but worth fixing as part of this new module — e.g.resolve()the path and check it starts with the expected prefix.[fixable] - 🔵 bugs (L239): The bundled fallback always returns
DEFAULT_AGENT_DEFINITIONwithidentity.name: 'mitzo-conversational'regardless of whichagentNamewas requested. If a session starts with a non-default agent and all sources fail, the stored definition's name won't match the session'sagentName. Consider overridingidentity.namewith the requestedagentNamein the fallback path.[fixable] - 🔵 style (L165):
readFileSyncblocks the event loop inside what is otherwise a fully async resolution chain. SinceloadAgentDefis already async, consider usingreadFilefromfs/promisesand makingloadFromLocalasync to match.[fixable]
server/chat.ts
Solid Phase 1 with good test coverage and graceful degradation; main concerns are type erasure through Record<string, unknown> on the session interface (will compound in Phase 2) and an inherited path-traversal pattern on the user-controlled agentName.
- 🟡 style (L777):
loaded.definition as unknown as Record<string, unknown>double-casts away the typedAgentDefinitioninto an untyped record. This happens becauseManagedSession.agentDefinitionis declared asRecord<string, unknown> | nullinstead of importing and usingAgentDefinition. Phase 2 consumers will have to cast it back. Consider typing the session field asAgentDefinition | null(import fromagent-loader.js) to preserve type safety end-to-end.[fixable]
packages/harness/src/session-registry.ts
Solid Phase 1 with good test coverage and graceful degradation; main concerns are type erasure through Record<string, unknown> on the session interface (will compound in Phase 2) and an inherited path-traversal pattern on the user-controlled agentName.
- 🔵 style (L56):
agentDefinitionSourceis typed asstring?but the actual values are the union'contexgin' | 'local' | 'fallback'(AgentDefinitionSourcein agent-loader.ts). Using the concrete union type would prevent invalid values and give downstream consumers better autocomplete/exhaustiveness checking.[fixable]
server/__tests__/agent-loader.test.ts
Solid Phase 1 with good test coverage and graceful degradation; main concerns are type erasure through Record<string, unknown> on the session interface (will compound in Phase 2) and an inherited path-traversal pattern on the user-controlled agentName.
- 🔵 missing_tests: Cache TTL expiry is untested — the tests verify cache hits and
clearCache(), but don't verify that an expired entry is re-fetched (e.g. by advancingDate.now()withvi.useFakeTimers). Also, theContexGin 200 with missing providerpath (exercising theprovider ?? { default: 'claude-opus-4' }fallback at line 146) has no test.[fixable]
| */ | ||
| function loadFromLocal(agentName: string, cwd: string): LoadedAgentDefinition | null { | ||
| try { | ||
| const filePath = join(cwd, '.agents', `${agentName}.yaml`); |
There was a problem hiding this comment.
🟡 unsafe_assumptions: Path traversal via user-controlled agentName: join(cwd, '.agents', ${agentName}.yaml) does not validate that the resolved path stays under cwd/.agents/. An authenticated WebSocket client sending agentName: '../../../etc/something' could read arbitrary .yaml files on disk. The same pattern already exists at chat.ts:825, so this is pre-existing, but worth fixing as part of this new module — e.g. resolve() the path and check it starts with the expected prefix. [fixable]
There was a problem hiding this comment.
Fixed — added path traversal guard: resolve() the path and verify it starts with resolve(cwd, '.agents') + '/'. Returns null if traversal detected.
| // 3. Bundled fallback | ||
| log.info('using bundled agent definition fallback', { agent: agentName }); | ||
| const fallback: LoadedAgentDefinition = { | ||
| definition: DEFAULT_AGENT_DEFINITION, |
There was a problem hiding this comment.
🔵 bugs: The bundled fallback always returns DEFAULT_AGENT_DEFINITION with identity.name: 'mitzo-conversational' regardless of which agentName was requested. If a session starts with a non-default agent and all sources fail, the stored definition's name won't match the session's agentName. Consider overriding identity.name with the requested agentName in the fallback path. [fixable]
There was a problem hiding this comment.
Fixed — same as above, fallback now overrides identity.name with the requested agentName.
| function loadFromLocal(agentName: string, cwd: string): LoadedAgentDefinition | null { | ||
| try { | ||
| const filePath = join(cwd, '.agents', `${agentName}.yaml`); | ||
| const raw = readFileSync(filePath, 'utf-8'); |
There was a problem hiding this comment.
🔵 style: readFileSync blocks the event loop inside what is otherwise a fully async resolution chain. Since loadAgentDef is already async, consider using readFile from fs/promises and making loadFromLocal async to match. [fixable]
There was a problem hiding this comment.
Fixed — readFileSync replaced with readFile from fs/promises, loadFromLocal is now fully async.
| .then((loaded) => { | ||
| const s = registry.get(clientId); | ||
| if (s) { | ||
| s.agentDefinition = loaded.definition as unknown as Record<string, unknown>; |
There was a problem hiding this comment.
🟡 style: loaded.definition as unknown as Record<string, unknown> double-casts away the typed AgentDefinition into an untyped record. This happens because ManagedSession.agentDefinition is declared as Record<string, unknown> | null instead of importing and using AgentDefinition. Phase 2 consumers will have to cast it back. Consider typing the session field as AgentDefinition | null (import from agent-loader.js) to preserve type safety end-to-end. [fixable]
There was a problem hiding this comment.
Keeping the cast for now — AgentDefinition lives in server/ (agent-loader) and the harness is a separate package. Removing the cast requires either moving the type to harness or a shared types package. Phase 2 scope.
| /** Parsed agent definition (recipe). Null if loading failed. */ | ||
| agentDefinition: Record<string, unknown> | null; | ||
| /** Source of the agent definition: 'contexgin' | 'local' | 'fallback'. */ | ||
| agentDefinitionSource?: string; |
There was a problem hiding this comment.
🔵 style: agentDefinitionSource is typed as string? but the actual values are the union 'contexgin' | 'local' | 'fallback' (AgentDefinitionSource in agent-loader.ts). Using the concrete union type would prevent invalid values and give downstream consumers better autocomplete/exhaustiveness checking. [fixable]
There was a problem hiding this comment.
Fixed — agentDefinitionSource now typed as 'contexgin' | 'local' | 'fallback' instead of string.
Load full agent definitions from ContexGin API, local .agents/ overrides, or bundled fallback at session start. Store parsed recipe in session registry for downstream consumption (governance, provider tiering, memory scope in future phases). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix YAML import: yaml → js-yaml (load) - Override fallback identity.name with requested agentName - Add path traversal guard in loadFromLocal - readFileSync → async readFile - Cache key includes contexginUrl - Type agentDefinitionSource as union in session registry - Update test mocks for async readFile and js-yaml Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cbca1e5 to
3437039
Compare
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 7 issue(s) (3 warning).
server/agent-loader.ts
Solid Phase 1 implementation with correct fallback semantics and good test coverage. Main issues: duplicate AgentDefinitionSource type across packages, untyped ManagedSession.agentDefinition requiring unsafe casts, and missing tests for cache TTL expiry and path traversal guard.
- 🟡 style (L89): AgentDefinitionSource is defined identically in both server/agent-loader.ts:89 and packages/harness/src/session-registry.ts:12. The server module should import the type from @mitzo/harness (via session-registry.ts) instead of declaring its own copy — two definitions will drift independently.
[fixable] - 🟡 bugs (L215): Dead null-coalesce:
contexginUrl ?? 'default'in the cache key can never trigger because the parameter has a default value (process.env.CONTEXGIN_URL || 'http://localhost:8321'). The??guard is unreachable — if the intent is to handle an explicitundefinedargument, the default parameter already covers it. Harmless but misleading.[fixable] - 🟡 unsafe_assumptions (L170): The path traversal guard (
!filePath.startsWith(agentsDir + '/')) is correct for the../case but fragile — it relies onresolve()normalizing away traversal segments. A safer pattern isconst rel = path.relative(agentsDir, filePath); if (rel.startsWith('..') || path.isAbsolute(rel)) return null;which is the idiomatic Node.js approach. The current guard also doesn't protect against symlink-based escapes if a symlink exists inside.agents/. Low risk sinceagentNamecomes from server-controlled config, but worth hardening.[fixable] - 🔵 unsafe_assumptions (L130): The ContexGin response fields (identity, provider, governance, memory) are cast with
aswithout validation — if ContexGin returns e.g.identity: "string"instead of an object, the code proceeds with a malformed definition stored in the session. The local YAML path has the same issue. Since this is Phase 1 and the data is from a trusted internal service, this is low-risk, but a lightweight Zod schema would prevent silent corruption.[fixable]
server/chat.ts
Solid Phase 1 implementation with correct fallback semantics and good test coverage. Main issues: duplicate AgentDefinitionSource type across packages, untyped ManagedSession.agentDefinition requiring unsafe casts, and missing tests for cache TTL expiry and path traversal guard.
- 🔵 unsafe_assumptions (L919): The double cast
loaded.definition as unknown as Record<string, unknown>is needed because ManagedSession storesagentDefinitionasRecord<string, unknown> | nullwhile the loader returns a typedAgentDefinition. Consider typing ManagedSession.agentDefinition asAgentDefinition | null(importing the type from agent-loader) to eliminate the cast and get type safety on consumers. The untyped Record makes downstream reads unsafe.[fixable]
server/__tests__/agent-loader.test.ts
Solid Phase 1 implementation with correct fallback semantics and good test coverage. Main issues: duplicate AgentDefinitionSource type across packages, untyped ManagedSession.agentDefinition requiring unsafe casts, and missing tests for cache TTL expiry and path traversal guard.
- 🔵 missing_tests: No test covers cache TTL expiry — the cache has a 5-minute TTL but no test advances time (via vi.useFakeTimers / vi.advanceTimersByTime) past expiry to verify a refetch occurs. The existing 'refetches after cache clear' test only covers manual invalidation.
[fixable] - 🔵 missing_tests: No test covers the path traversal guard in loadFromLocal — e.g., calling loadAgentDef with agentName='../../etc/passwd' should fall through to fallback without reading the file. This is the security-critical code path.
[fixable]
| output?: AgentOutput; | ||
| } | ||
|
|
||
| export type AgentDefinitionSource = 'contexgin' | 'local' | 'fallback'; |
There was a problem hiding this comment.
🟡 style: AgentDefinitionSource is defined identically in both server/agent-loader.ts:89 and packages/harness/src/session-registry.ts:12. The server module should import the type from @mitzo/harness (via session-registry.ts) instead of declaring its own copy — two definitions will drift independently. [fixable]
| contexginUrl: string = process.env.CONTEXGIN_URL || 'http://localhost:8321', | ||
| ): Promise<LoadedAgentDefinition> { | ||
| // Check cache | ||
| const cacheKey = `${agentName}:${cwd}:${contexginUrl ?? 'default'}`; |
There was a problem hiding this comment.
🟡 bugs: Dead null-coalesce: contexginUrl ?? 'default' in the cache key can never trigger because the parameter has a default value (process.env.CONTEXGIN_URL || 'http://localhost:8321'). The ?? guard is unreachable — if the intent is to handle an explicit undefined argument, the default parameter already covers it. Harmless but misleading. [fixable]
| // Path traversal guard: ensure resolved path stays under .agents/ | ||
| const agentsDir = resolve(cwd, '.agents'); | ||
| const filePath = resolve(agentsDir, `${agentName}.yaml`); | ||
| if (!filePath.startsWith(agentsDir + '/')) return null; |
There was a problem hiding this comment.
🟡 unsafe_assumptions: The path traversal guard (!filePath.startsWith(agentsDir + '/')) is correct for the ../ case but fragile — it relies on resolve() normalizing away traversal segments. A safer pattern is const rel = path.relative(agentsDir, filePath); if (rel.startsWith('..') || path.isAbsolute(rel)) return null; which is the idiomatic Node.js approach. The current guard also doesn't protect against symlink-based escapes if a symlink exists inside .agents/. Low risk since agentName comes from server-controlled config, but worth hardening. [fixable]
| const data = (await res.json()) as Record<string, unknown>; | ||
|
|
||
| // ContexGin returns identity, provider, governance, memory at the top level | ||
| const identity = data.identity as AgentIdentity | undefined; |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The ContexGin response fields (identity, provider, governance, memory) are cast with as without validation — if ContexGin returns e.g. identity: "string" instead of an object, the code proceeds with a malformed definition stored in the session. The local YAML path has the same issue. Since this is Phase 1 and the data is from a trusted internal service, this is low-risk, but a lightweight Zod schema would prevent silent corruption. [fixable]
| .then((loaded) => { | ||
| const s = registry.get(clientId); | ||
| if (s) { | ||
| s.agentDefinition = loaded.definition as unknown as Record<string, unknown>; |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The double cast loaded.definition as unknown as Record<string, unknown> is needed because ManagedSession stores agentDefinition as Record<string, unknown> | null while the loader returns a typed AgentDefinition. Consider typing ManagedSession.agentDefinition as AgentDefinition | null (importing the type from agent-loader) to eliminate the cast and get type safety on consumers. The untyped Record makes downstream reads unsafe. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 8 issue(s) (1 critical) (4 warning).
server/agent-loader.ts
Missing js-yaml dependency will crash at runtime. Beyond that, solid implementation with good error handling and test coverage, but the duplicate type definition and Record<string, unknown> typing in ManagedSession should be cleaned up before merge.
- 🔴 bugs (L14):
js-yamlis imported but is not declared as a dependency in anypackage.json(root, server, or any workspace package), andnode_modules/js-yamldoes not exist. This will crash at runtime withERR_MODULE_NOT_FOUND. Either addjs-yaml(and@types/js-yaml) to the root or server dependencies, or switch to theyamlpackage if already present.[fixable] - 🟡 style (L89): Duplicate type definition:
AgentDefinitionSourceis defined identically in bothserver/agent-loader.ts:89andpackages/harness/src/session-registry.ts:12. The session-registry version is authoritative (it's used in theManagedSessioninterface).agent-loader.tsshould import it from@mitzo/harnessinstead of redeclaring it, to avoid drift.[fixable] - 🔵 unsafe_assumptions (L169): The path traversal guard
!filePath.startsWith(agentsDir + '/')is correct for Unix but would fail on Windows wherepath.sepis\. Not a practical issue for this project (macOS/Linux deployment), but worth noting if portability is ever a goal. TheagentNameis also used verbatim as a filename — names containing/,\, or null bytes could produce unexpected paths even after resolve. Consider validatingagentNameagainst a safe pattern (e.g.,/^[a-z0-9-]+$/).[fixable] - 🔵 style (L215): Cache key includes
contexginUrl ?? 'default'butcontexginUrlis already defaulted to a non-nullish string on line 212 (process.env.CONTEXGIN_URL || 'http://localhost:8321'), so the nullish coalescing is dead code.[fixable]
packages/harness/src/session-registry.ts
Missing js-yaml dependency will crash at runtime. Beyond that, solid implementation with good error handling and test coverage, but the duplicate type definition and Record<string, unknown> typing in ManagedSession should be cleaned up before merge.
- 🟡 regressions (L56):
agentDefinitionis declared as a required (non-optional) field onManagedSession, but it's loaded asynchronously via fire-and-forget inchat.ts. Any code that accessessession.agentDefinitionimmediately afterregister()will seenulleven on successful loads. Consider making the field optional (agentDefinition?: ...) to signal that it may not be populated yet, or document the async contract clearly.[fixable]
server/chat.ts
Missing js-yaml dependency will crash at runtime. Beyond that, solid implementation with good error handling and test coverage, but the duplicate type definition and Record<string, unknown> typing in ManagedSession should be cleaned up before merge.
- 🟡 unsafe_assumptions (L919): Double type cast
loaded.definition as unknown as Record<string, unknown>is a code smell indicating theManagedSession.agentDefinitionfield should use theAgentDefinitiontype fromagent-loader.tsinstead ofRecord<string, unknown>. This erases all type safety — consumers will need their own casts to use the definition. Consider typingManagedSession.agentDefinitionasAgentDefinition | nulland re-exporting the type from@mitzo/harness.[fixable] - 🟡 unsafe_assumptions (L917): When
registry.get(clientId)returnsundefined(session already torn down during the async load), the definition is silently discarded with no log. Thelog.infoat line 922 still runs and reports success. Move the log inside theif (s)block or add awarnin the else branch so operators can see when the race is lost.[fixable]
server/__tests__/agent-loader.test.ts
Missing js-yaml dependency will crash at runtime. Beyond that, solid implementation with good error handling and test coverage, but the duplicate type definition and Record<string, unknown> typing in ManagedSession should be cleaned up before merge.
- 🔵 missing_tests: The test suite covers the loader itself well, but there are no tests for the integration in
chat.ts— specifically the fire-and-forget attachment to the session registry, the race where registry.get returns undefined, or the double type cast. A unit test for_startChatInner(or an integration test) verifyingsession.agentDefinitionis populated after the promise settles would catch regressions in the wiring.
|
|
||
| import { readFile } from 'fs/promises'; | ||
| import { resolve } from 'path'; | ||
| import { load as parseYaml } from 'js-yaml'; |
There was a problem hiding this comment.
🔴 bugs: js-yaml is imported but is not declared as a dependency in any package.json (root, server, or any workspace package), and node_modules/js-yaml does not exist. This will crash at runtime with ERR_MODULE_NOT_FOUND. Either add js-yaml (and @types/js-yaml) to the root or server dependencies, or switch to the yaml package if already present. [fixable]
| output?: AgentOutput; | ||
| } | ||
|
|
||
| export type AgentDefinitionSource = 'contexgin' | 'local' | 'fallback'; |
There was a problem hiding this comment.
🟡 style: Duplicate type definition: AgentDefinitionSource is defined identically in both server/agent-loader.ts:89 and packages/harness/src/session-registry.ts:12. The session-registry version is authoritative (it's used in the ManagedSession interface). agent-loader.ts should import it from @mitzo/harness instead of redeclaring it, to avoid drift. [fixable]
| try { | ||
| // Path traversal guard: ensure resolved path stays under .agents/ | ||
| const agentsDir = resolve(cwd, '.agents'); | ||
| const filePath = resolve(agentsDir, `${agentName}.yaml`); |
There was a problem hiding this comment.
🔵 unsafe_assumptions: The path traversal guard !filePath.startsWith(agentsDir + '/') is correct for Unix but would fail on Windows where path.sep is \. Not a practical issue for this project (macOS/Linux deployment), but worth noting if portability is ever a goal. The agentName is also used verbatim as a filename — names containing /, \, or null bytes could produce unexpected paths even after resolve. Consider validating agentName against a safe pattern (e.g., /^[a-z0-9-]+$/). [fixable]
| contexginUrl: string = process.env.CONTEXGIN_URL || 'http://localhost:8321', | ||
| ): Promise<LoadedAgentDefinition> { | ||
| // Check cache | ||
| const cacheKey = `${agentName}:${cwd}:${contexginUrl ?? 'default'}`; |
There was a problem hiding this comment.
🔵 style: Cache key includes contexginUrl ?? 'default' but contexginUrl is already defaulted to a non-nullish string on line 212 (process.env.CONTEXGIN_URL || 'http://localhost:8321'), so the nullish coalescing is dead code. [fixable]
| /** Cached boot_context payload for replay on reconnect/switch. */ | ||
| bootContext?: Record<string, unknown>; | ||
| /** Parsed agent definition (recipe). Null if loading failed. */ | ||
| agentDefinition: Record<string, unknown> | null; |
There was a problem hiding this comment.
🟡 regressions: agentDefinition is declared as a required (non-optional) field on ManagedSession, but it's loaded asynchronously via fire-and-forget in chat.ts. Any code that accesses session.agentDefinition immediately after register() will see null even on successful loads. Consider making the field optional (agentDefinition?: ...) to signal that it may not be populated yet, or document the async contract clearly. [fixable]
| .then((loaded) => { | ||
| const s = registry.get(clientId); | ||
| if (s) { | ||
| s.agentDefinition = loaded.definition as unknown as Record<string, unknown>; |
There was a problem hiding this comment.
🟡 unsafe_assumptions: Double type cast loaded.definition as unknown as Record<string, unknown> is a code smell indicating the ManagedSession.agentDefinition field should use the AgentDefinition type from agent-loader.ts instead of Record<string, unknown>. This erases all type safety — consumers will need their own casts to use the definition. Consider typing ManagedSession.agentDefinition as AgentDefinition | null and re-exporting the type from @mitzo/harness. [fixable]
| // Fire-and-forget: load agent definition and store in session registry. | ||
| loadAgentDef(agentName, cwd) | ||
| .then((loaded) => { | ||
| const s = registry.get(clientId); |
There was a problem hiding this comment.
🟡 unsafe_assumptions: When registry.get(clientId) returns undefined (session already torn down during the async load), the definition is silently discarded with no log. The log.info at line 922 still runs and reports success. Move the log inside the if (s) block or add a warn in the else branch so operators can see when the race is lost. [fixable]
Summary
server/agent-loader.ts— three-tier recipe discovery (ContexGin API → local.agents/override → bundled fallback) with 5-minute TTL cacheagentDefinition+agentDefinitionSourceinManagedSession(session-registry)loadAgentDef()fire-and-forget into session start inchat.tsDEFAULT_AGENT_DEFINITIONbundled fallback toconstants.tsThis is Phase 1 of the agent-definition-loading spec. Future phases will consume the stored recipe for governance enforcement, provider tiering, memory scoping, and output conventions.
Test plan
agent-loader.test.ts— all sources, caching, error handling🤖 Generated with Claude Code