feat(#93): implement agent spawn via federation
Implements FED-010: Agent Spawn via Federation feature that enables spawning and managing Claude agents on remote federated Mosaic Stack instances via COMMAND message type. Features: - Federation agent command types (spawn, status, kill) - FederationAgentService for handling agent operations - Integration with orchestrator's agent spawner/lifecycle services - API endpoints for spawning, querying status, and killing agents - Full command routing through federation COMMAND infrastructure - Comprehensive test coverage (12/12 tests passing) Architecture: - Hub → Spoke: Spawn agents on remote instances - Command flow: FederationController → FederationAgentService → CommandService → Remote Orchestrator - Response handling: Remote orchestrator returns agent status/results - Security: Connection validation, signature verification Files created: - apps/api/src/federation/types/federation-agent.types.ts - apps/api/src/federation/federation-agent.service.ts - apps/api/src/federation/federation-agent.service.spec.ts Files modified: - apps/api/src/federation/command.service.ts (agent command routing) - apps/api/src/federation/federation.controller.ts (agent endpoints) - apps/api/src/federation/federation.module.ts (service registration) - apps/orchestrator/src/api/agents/agents.controller.ts (status endpoint) - apps/orchestrator/src/api/agents/agents.module.ts (lifecycle integration) Testing: - 12/12 tests passing for FederationAgentService - All command service tests passing - TypeScript compilation successful - Linting passed Refs #93 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,9 +17,9 @@ The `wiki-link-parser.ts` utility provides parsing of wiki-style `[[links]]` fro
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { parseWikiLinks } from './utils/wiki-link-parser';
|
||||
import { parseWikiLinks } from "./utils/wiki-link-parser";
|
||||
|
||||
const content = 'See [[Main Page]] and [[Getting Started|start here]].';
|
||||
const content = "See [[Main Page]] and [[Getting Started|start here]].";
|
||||
const links = parseWikiLinks(content);
|
||||
|
||||
// Result:
|
||||
@@ -44,32 +44,41 @@ const links = parseWikiLinks(content);
|
||||
### Supported Link Formats
|
||||
|
||||
#### Basic Link (by title)
|
||||
|
||||
```markdown
|
||||
[[Page Name]]
|
||||
```
|
||||
|
||||
Links to a page by its title. Display text will be "Page Name".
|
||||
|
||||
#### Link with Display Text
|
||||
|
||||
```markdown
|
||||
[[Page Name|custom display]]
|
||||
```
|
||||
|
||||
Links to "Page Name" but displays "custom display".
|
||||
|
||||
#### Link by Slug
|
||||
|
||||
```markdown
|
||||
[[page-slug-name]]
|
||||
```
|
||||
|
||||
Links to a page by its URL slug (kebab-case).
|
||||
|
||||
### Edge Cases
|
||||
|
||||
#### Nested Brackets
|
||||
|
||||
```markdown
|
||||
[[Page [with] brackets]] ✓ Parsed correctly
|
||||
[[Page [with] brackets]] ✓ Parsed correctly
|
||||
```
|
||||
|
||||
Single brackets inside link text are allowed.
|
||||
|
||||
#### Code Blocks (Not Parsed)
|
||||
|
||||
```markdown
|
||||
Use `[[WikiLink]]` syntax for linking.
|
||||
|
||||
@@ -77,36 +86,41 @@ Use `[[WikiLink]]` syntax for linking.
|
||||
const link = "[[not parsed]]";
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
Links inside inline code or fenced code blocks are ignored.
|
||||
|
||||
#### Escaped Brackets
|
||||
|
||||
```markdown
|
||||
\[[not a link]] but [[real link]] works
|
||||
```
|
||||
|
||||
Escaped brackets are not parsed as links.
|
||||
|
||||
#### Empty or Invalid Links
|
||||
|
||||
```markdown
|
||||
[[]] ✗ Empty link (ignored)
|
||||
[[ ]] ✗ Whitespace only (ignored)
|
||||
[[ Target ]] ✓ Trimmed to "Target"
|
||||
[[]] ✗ Whitespace only (ignored)
|
||||
[[Target]] ✓ Trimmed to "Target"
|
||||
```
|
||||
|
||||
### Return Type
|
||||
|
||||
```typescript
|
||||
interface WikiLink {
|
||||
raw: string; // Full matched text: "[[Page Name]]"
|
||||
target: string; // Target page: "Page Name"
|
||||
raw: string; // Full matched text: "[[Page Name]]"
|
||||
target: string; // Target page: "Page Name"
|
||||
displayText: string; // Display text: "Page Name" or custom
|
||||
start: number; // Start position in content
|
||||
end: number; // End position in content
|
||||
start: number; // Start position in content
|
||||
end: number; // End position in content
|
||||
}
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
Comprehensive test suite (100% coverage) includes:
|
||||
|
||||
- Basic parsing (single, multiple, consecutive links)
|
||||
- Display text variations
|
||||
- Edge cases (brackets, escapes, empty links)
|
||||
@@ -116,6 +130,7 @@ Comprehensive test suite (100% coverage) includes:
|
||||
- Malformed input handling
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
pnpm test --filter=@mosaic/api -- wiki-link-parser.spec.ts
|
||||
```
|
||||
@@ -130,6 +145,7 @@ This parser is designed to work with the Knowledge Module's linking system:
|
||||
4. **Link Rendering**: Replace `[[links]]` with HTML anchors
|
||||
|
||||
See related issues:
|
||||
|
||||
- #59 - Wiki-link parser (this implementation)
|
||||
- Future: Link resolution and storage
|
||||
- Future: Backlink display and navigation
|
||||
@@ -151,33 +167,38 @@ The `markdown.ts` utility provides secure markdown rendering with GFM (GitHub Fl
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { renderMarkdown, markdownToPlainText } from './utils/markdown';
|
||||
import { renderMarkdown, markdownToPlainText } from "./utils/markdown";
|
||||
|
||||
// Render markdown to HTML (async)
|
||||
const html = await renderMarkdown('# Hello **World**');
|
||||
const html = await renderMarkdown("# Hello **World**");
|
||||
// Result: <h1 id="hello-world">Hello <strong>World</strong></h1>
|
||||
|
||||
// Extract plain text (for search indexing)
|
||||
const plainText = await markdownToPlainText('# Hello **World**');
|
||||
const plainText = await markdownToPlainText("# Hello **World**");
|
||||
// Result: "Hello World"
|
||||
```
|
||||
|
||||
### Supported Markdown Features
|
||||
|
||||
#### Basic Formatting
|
||||
|
||||
- **Bold**: `**text**` or `__text__`
|
||||
- *Italic*: `*text*` or `_text_`
|
||||
- _Italic_: `*text*` or `_text_`
|
||||
- ~~Strikethrough~~: `~~text~~`
|
||||
- `Inline code`: `` `code` ``
|
||||
|
||||
#### Headers
|
||||
|
||||
```markdown
|
||||
# H1
|
||||
|
||||
## H2
|
||||
|
||||
### H3
|
||||
```
|
||||
|
||||
#### Lists
|
||||
|
||||
```markdown
|
||||
- Unordered list
|
||||
- Nested item
|
||||
@@ -187,19 +208,22 @@ const plainText = await markdownToPlainText('# Hello **World**');
|
||||
```
|
||||
|
||||
#### Task Lists
|
||||
|
||||
```markdown
|
||||
- [ ] Unchecked task
|
||||
- [x] Completed task
|
||||
```
|
||||
|
||||
#### Tables
|
||||
|
||||
```markdown
|
||||
| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
| -------- | -------- |
|
||||
| Cell 1 | Cell 2 |
|
||||
```
|
||||
|
||||
#### Code Blocks
|
||||
|
||||
````markdown
|
||||
```typescript
|
||||
const greeting: string = "Hello";
|
||||
@@ -208,12 +232,14 @@ console.log(greeting);
|
||||
````
|
||||
|
||||
#### Links and Images
|
||||
|
||||
```markdown
|
||||
[Link text](https://example.com)
|
||||

|
||||
```
|
||||
|
||||
#### Blockquotes
|
||||
|
||||
```markdown
|
||||
> This is a quote
|
||||
> Multi-line quote
|
||||
@@ -233,6 +259,7 @@ The renderer implements multiple layers of security:
|
||||
### Testing
|
||||
|
||||
Comprehensive test suite covers:
|
||||
|
||||
- Basic markdown rendering
|
||||
- GFM features (tables, task lists, strikethrough)
|
||||
- Code syntax highlighting
|
||||
@@ -240,6 +267,7 @@ Comprehensive test suite covers:
|
||||
- Edge cases (unicode, long content, nested structures)
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
pnpm test --filter=@mosaic/api -- markdown.spec.ts
|
||||
```
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
renderMarkdown,
|
||||
renderMarkdownSync,
|
||||
markdownToPlainText,
|
||||
} from "./markdown";
|
||||
import { renderMarkdown, renderMarkdownSync, markdownToPlainText } from "./markdown";
|
||||
|
||||
describe("Markdown Rendering", () => {
|
||||
describe("renderMarkdown", () => {
|
||||
@@ -77,7 +73,7 @@ describe("Markdown Rendering", () => {
|
||||
|
||||
const html = await renderMarkdown(markdown);
|
||||
|
||||
expect(html).toContain('<input');
|
||||
expect(html).toContain("<input");
|
||||
expect(html).toContain('type="checkbox"');
|
||||
expect(html).toContain('disabled="disabled"'); // Should be disabled for safety
|
||||
});
|
||||
@@ -145,16 +141,17 @@ plain text code
|
||||
const markdown = "";
|
||||
const html = await renderMarkdown(markdown);
|
||||
|
||||
expect(html).toContain('<img');
|
||||
expect(html).toContain("<img");
|
||||
expect(html).toContain('src="https://example.com/image.png"');
|
||||
expect(html).toContain('alt="Alt text"');
|
||||
});
|
||||
|
||||
it("should allow data URIs for images", async () => {
|
||||
const markdown = "";
|
||||
const markdown =
|
||||
"";
|
||||
const html = await renderMarkdown(markdown);
|
||||
|
||||
expect(html).toContain('<img');
|
||||
expect(html).toContain("<img");
|
||||
expect(html).toContain('src="data:image/png;base64');
|
||||
});
|
||||
});
|
||||
@@ -164,7 +161,7 @@ plain text code
|
||||
const markdown = "# My Header Title";
|
||||
const html = await renderMarkdown(markdown);
|
||||
|
||||
expect(html).toContain('<h1');
|
||||
expect(html).toContain("<h1");
|
||||
expect(html).toContain('id="');
|
||||
});
|
||||
|
||||
@@ -282,7 +279,7 @@ plain text code
|
||||
});
|
||||
|
||||
it("should strip all HTML tags", async () => {
|
||||
const markdown = '[Link](https://example.com)\n\n';
|
||||
const markdown = "[Link](https://example.com)\n\n";
|
||||
const plainText = await markdownToPlainText(markdown);
|
||||
|
||||
expect(plainText).not.toContain("<a");
|
||||
|
||||
@@ -333,9 +333,7 @@ const link = "[[Not A Link]]";
|
||||
|
||||
expect(links[0].start).toBe(5);
|
||||
expect(links[0].end).toBe(23);
|
||||
expect(content.substring(links[0].start, links[0].end)).toBe(
|
||||
"[[Target|Display]]"
|
||||
);
|
||||
expect(content.substring(links[0].start, links[0].end)).toBe("[[Target|Display]]");
|
||||
});
|
||||
|
||||
it("should track positions in multiline content", () => {
|
||||
|
||||
Reference in New Issue
Block a user