# Session: Bug Fix
# Full Title: Fix _parse_manifest() B1 bug (@eidos-engineer subagent)
# ID: ses_3a7ee1870ffeFAZUIkNJsNnfm3
# Date: 2026-02-13 17:34 UTC
# Total Messages: 35

---

## User

## Task: Fix _parse_manifest() to parse spec section from YAML (Step 50.1)

### Check-In
- Step: 50.1 — Fix `_parse_manifest()` to parse spec section from YAML
- Phase: phase-50-manifest-wiring (Manifest Spec Parsing & CapabilityGate Wiring)
- Claimed by: eidos-engineer

### The Bug (Bug 14 / B1 — "Perfect Component Illusion")

**Root cause location**: `packages/eidos-mainframe/src/eidos_mainframe/shell/forms/loader.py`, around line 343-344.

The function `_parse_manifest()` currently does:
```python
spec = FormSpec()  # ← IGNORES entire YAML spec section
```

The YAML manifest at `packages/eidos-mainframe/src/eidos_mainframe/resources/builtin_forms/builtin_assistant/manifest.yaml` declares toolsets, capabilities, and runtime config in a `spec:` section, but `_parse_manifest()` creates an empty `FormSpec()` instead of parsing `data["spec"]`.

**Impact**: Agent says "I have no file system access" — zero tools available. The 4-level cascade:
1. L1 context assembler checks `form.manifest.spec.toolsets` → empty tuple → skips tool section
2. L2 builder shows "Enabled Toolsets: (none)"  
3. CapabilityGate baseline is empty (because `resolve_policy_from_manifest()` is never called)
4. Agent gets no tools

### What to Fix

1. **Read the current `_parse_manifest()` function** in `loader.py` to understand its structure
2. **Read `FormSpec`** (likely in eidos-protocol) to understand its fields and types
3. **Read the builtin_assistant `manifest.yaml`** to see what the YAML spec section looks like
4. **Fix `_parse_manifest()`** to parse `data.get("spec", {})` into a proper `FormSpec` with:
   - `toolsets`: tuple of toolset identifiers
   - `capabilities`: tuple of capability scope identifiers  
   - `runtime`: any runtime configuration
   - Other fields as defined by `FormSpec`
5. **Write a test** that loads the builtin_assistant manifest and asserts `manifest.spec.toolsets` is non-empty

### Constraints
- Follow AGENTS.md Work Unit Protocol: CODE → VERIFY → COMMIT
- Run `uv run pytest` and `uv run mypy` on affected files before committing
- No TODOs, no `pass` bodies, no fake returns
- This is a 🔴 P0 behavioral fix — highest priority

### Verification
After your fix:
- `FormSpec` parsed from builtin_assistant manifest should have non-empty `toolsets`
- Existing tests should still pass
- New test proves the fix

### Return
Tell me:
1. What you changed (files, lines)
2. Test results (pytest output)
3. mypy results
4. The commit hash

---

## Assistant (internal reasoning)

Let me start by understanding the current state of the codebase. I need to:

1. Check in with the plan
2. Read the current `_parse_manifest()` function in `loader.py`
3. Read `FormSpec` to understand its fields
4. Read the builtin_assistant `manifest.yaml`
5. Fix the parsing
6. Write a test
7. Verify and commit

Let me begin.


Let me check the current branch and git status, then read the relevant files.

---

## Assistant

✓ Check-In: Edios | main | dirty (plan.yaml only)
  Step: 50.1 — Fix `_parse_manifest()` to parse spec section from YAML
  Todolist: NOT NEEDED (<3 tasks)

Now let me read the supporting types to understand how to construct them:


Now let me check the imports in loader.py and the `RuntimeEnvVar` type:


Let me check the `eidos_protocol.models.form` `__init__.py` to see what's exported:

---

## Assistant (internal reasoning)

Now I have a complete understanding. Let me think about what I need to do:

1. `_parse_manifest()` currently creates `spec = FormSpec()` — ignoring the YAML `spec:` section.
2. I need to parse `data.get("spec", {})` and construct a proper `FormSpec`.

The `FormSpec` has these fields:
- `toolsets: tuple[str, ...] = ()` — list of strings from YAML
- `tools: tuple[ToolDefinition, ...] = ()` — list of ToolDefinition from YAML 
- `capabilities: tuple[CapabilityEntry, ...] = ()` — list of CapabilityEntry from YAML

- `imports: tuple[SkillImport, ...] = ()` — list of SkillImport from YAML
- `runtime: RuntimeConfig = field(default_factory=RuntimeConfig)` — RuntimeConfig from YAML
- `context: ContextConfig = field(default_factory=ContextConfig)` — ContextConfig from YAML
- `skills: SkillsConfig = field(default_factory=SkillsConfig)` — SkillsConfig from YAML

Looking at the manifest YAML structure, I can see how these fields map to the spec section with toolsets, tools, capabilities, and other configuration objects.

Now I need to parse the capabilities list where each entry has a scope field, extract the runtime image and environment settings, and handle the toolsets as a tuple of strings. I'll also need to import the necessary dataclasses like CapabilityEntry, RuntimeConfig, ToolDefinition, SkillImport, ContextConfig, and SkillsConfig to properly deserialize all these nested configurations. about how to structure this. I'll add a `_parse_spec()` helper method that handles each field—converting toolsets to a tuple, building CapabilityEntry objects from the capabilities list, creating ToolDefinition objects from tools with their command/args/env details, parsing SkillImport objects from imports, and handling the runtime config which includes parsing RuntimeEnvVar objects from the env list.

For context and skills, since the manifest doesn't specify them, I'll parse them if they're present but rely on defaults otherwise. I'm going to keep this pragmatic—focus on parsing what's actually in the YAML and let the defaults handle the rest rather than over-engineering fields that aren't being used.

Writing the parsing method... Parsing tool definitions and imports...

Now I'm validating the runtime, context, and skills configurations, using defaults where needed, then assembling everything into the final FormSpec object.

Actually, I should focus on just the core fields that are causing the bug—toolsets, capabilities, and runtime—rather than trying to handle all these nested structures like ContextConfig and WorkingMemoryConfig right now. The task description emphasizes those three as the priority, so let me simplify and get those working first. I'm overcomplicating this — I should just parse whatever fields are present in the YAML and create helper methods for the more complex configs like `RuntimeConfig` and `ContextConfig` to keep the main `_parse_spec` method clean and readable.

Now I need to plan out the actual implementation: update the imports to include all the necessary types, replace the `_parse_manifest` body to call the new parsing logic, and add the helper methods...

Adding helper methods for parsing... Writing the _parse_spec method... Continuing to parse the configuration structure...

Now I'm processing the imports list to extract skill package dependencies, then setting up the runtime configuration with the container image and environment variables. Finishing the context configuration and then parsing the skills section with core/exclude lists and loading strategy, before assembling everything into the final FormSpec object. But I'm noticing a type mismatch—the management_mode field expects a specific literal type, not just any string. I'm realizing the cleanest approach is to just use the default constructors for `context` and `skills` since those sections aren't in the manifest YAML anyway, rather than trying to parse empty dicts and deal with mypy's type inference issues. I'm running into a mypy strict mode issue with Literal types—when I pull values from a dict with `.get()`, mypy sees them as plain `str` rather than the specific `Literal` union, so I'd need explicit validation and casting. For now, I'll stick with using `ContextConfig()` and `SkillsConfig()` defaults when those sections aren't in the YAML, and only parse them if they're actually present.

But wait, that's a "for now" approach, which defeats the purpose. The real problem is that `FormSpec()` is being called with no arguments, completely ignoring the toolsets defined in the YAML spec section—I need to actually parse and pass those in.

The solution is to parse the critical fields from the YAML spec: toolsets, capabilities, tools, imports, and runtime. For context and skills, the defaults are fine since the manifest doesn't include them, but I should still handle them if they're added later. I'll parse the simple fields (tuples of strings, standard literals) and validate Literal types with a helper function.

Writing the full implementation... I'm working through the type-safe approach for handling the management mode field, and I realize the cleanest solution is to just use the defaults that ContextConfig and SkillsConfig already provide—since the builtin_assistant manifest doesn't include these sections anyway, I can construct them with their default values and only parse the simple fields when they're actually present in the YAML.

Now I need to verify what imports I'll need, particularly RuntimeEnvVar for parsing environment variables, and then write out the complete solution.

---

## Assistant

I have all the information needed. Let me also find the test location:


Now I have a clear picture. Let me implement the fix. I'll:

1. Add the needed imports to `loader.py`
2. Replace `spec = FormSpec()` with proper parsing
3. Add a `_parse_spec` helper method
4. Write a test

Let me first check the full loader file for insertion context:


Good, I'm on track. Now let me implement the fix. Three changes needed in `loader.py`:

**Change 1**: Add imports for the spec-related types.
**Change 2**: Replace `spec = FormSpec()` with a call to `_parse_spec`.
**Change 3**: Add the `_parse_spec` method.


Those are standard cross-package LSP resolution issues — they work at runtime via uv. Now replace the stub line:


Good, now I need to add the `_parse_spec` method. Let me insert it after `_parse_manifest`:


Now let me verify the code looks correct:


Code looks correct. Now let me write the test. I'll add a new test class to the existing test file:


Let me just add the test class at the end of the file:


Now let me run verification — pytest and mypy on the affected files:


The builtin_assistant test fails because the Form directory structure expects `builtin_assistant/.eidos/manifest.yaml` but the actual structure is different. Let me check:


The builtin_assistant has `manifest.yaml` directly in the directory (no `.eidos/` subdirectory). Let me check how the loader resolves paths:


The builtin_assistant directory lacks the `.eidos/` subdirectory structure that `FileSystemFormLoader` expects. My real-manifest test needs to create the right structure instead. Let me fix the test:


All 32 passed (8 new spec-parsing tests + 24 existing). Now mypy:


The 3 mypy errors are all pre-existing issues in completely unrelated files (`request_input.py`, `request_form.py`, `request_approval.py`) — not in `loader.py`. The errors are about `BlockingAgentOutputHandler._execute_blocking` callback type variance, which is unrelated to my change.

Let me verify my file has zero mypy errors specifically:


Zero errors in `loader.py`. Now let me also run the broader manifest-related tests to make sure nothing is broken:


**420 passed, 0 failed.** All existing and new tests pass. Now let me commit:


Committed as `d0e3c47`. All pre-commit hooks passed including Invar Guard. Now let me complete the step:


✓ Final: committed [Y] | guard [PASS] | Step 50.1 → ✅

---

## Summary

### 1. What I Changed

**`packages/eidos-mainframe/src/eidos_mainframe/shell/forms/loader.py`** (2 changes):
- **Line 18-31**: Added imports for `CapabilityEntry`, `RuntimeConfig`, `RuntimeEnvVar`, `SkillImport`, `ToolDefinition`
- **Line 343-455**: Replaced `spec = FormSpec()` with `self._parse_spec(spec_data)` — a new 95-line method that parses the full YAML `spec:` section into typed `FormSpec` fields: `toolsets`, `capabilities`, `tools`, `imports`, `runtime`

**`tests/unit/mainframe/shell/forms/test_loader.py`** (8 new tests):
- `test_builtin_assistant_manifest_spec_parsed` — loads real manifest content, asserts toolsets/capabilities/runtime populated
- `test_spec_toolsets_parsed` — verifies toolset strings are parsed as tuple
- `test_spec_capabilities_parsed` — verifies scope-based capabilities
- `test_spec_runtime_parsed` — verifies custom image parsed
- `test_spec_tools_parsed` — verifies explicit MCP tool definitions
- `test_spec_imports_parsed` — verifies skill package imports
- `test_missing_spec_section_gives_empty_defaults` — backward compatibility
- `test_spec_with_full_capability_entry` — verifies structured effect/resource/action entries

### 2. Test Results
```
420 passed, 0 failed (all manifest/loader/form related tests)
32 passed in test_loader.py specifically (8 new + 24 existing)
```

### 3. mypy Results
```
0 errors in loader.py (3 pre-existing errors in unrelated files: request_input.py, request_form.py, request_approval.py)
```

### 4. Commit Hash
```
d0e3c47
```
