Files
claude-engineering-plugin/plugins/compound-engineering/skills/agent-native-architecture/references/shared-workspace-architecture.md
Dan Shipper 1bc6bd9164 [2.18.0] Add Dynamic Capability Discovery and iCloud sync patterns (#62)
* [2.17.0] Expand agent-native skill with mobile app learnings

Major expansion of agent-native-architecture skill based on real-world
learnings from building the Every Reader iOS app.

New reference documents:
- dynamic-context-injection.md: Runtime app state in system prompts
- action-parity-discipline.md: Ensuring agents can do what users can
- shared-workspace-architecture.md: Agents and users in same data space
- agent-native-testing.md: Testing patterns for agent-native apps
- mobile-patterns.md: Background execution, permissions, cost awareness

Updated references:
- architecture-patterns.md: Added Unified Agent Architecture, Agent-to-UI
  Communication, and Model Tier Selection patterns

Enhanced agent-native-reviewer with comprehensive review process covering
all new patterns, including mobile-specific verification.

Key insight: "The agent should be able to do anything the user can do,
through tools that mirror UI capabilities, with full context about the
app state."

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [2.18.0] Add Dynamic Capability Discovery and iCloud sync patterns

New patterns in agent-native-architecture skill:

- **Dynamic Capability Discovery** - For agent-native apps integrating with
  external APIs (HealthKit, HomeKit, GraphQL), use a discovery tool (list_*)
  plus a generic access tool instead of individual tools per endpoint.
  (Note: Static mapping is fine for constrained agents with limited scope.)

- **CRUD Completeness** - Every entity needs create, read, update, AND delete.

- **iCloud File Storage** - Use iCloud Documents for shared workspace to get
  free, automatic multi-device sync without building a sync layer.

- **Architecture Review Checklist** - Pushes reviewer findings earlier into
  design phase. Covers tool design, action parity, UI integration, context.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-25 12:03:07 -06:00

20 KiB

Agents and users should work in the same data space, not separate sandboxes. When the agent writes a file, the user can see it. When the user edits something, the agent can read the changes. This creates transparency, enables collaboration, and eliminates the need for sync layers.

Core principle: The agent operates in the same filesystem as the user, not a walled garden.

<why_shared_workspace>

Why Shared Workspace?

The Sandbox Anti-Pattern

Many agent implementations isolate the agent:

┌─────────────────┐     ┌─────────────────┐
│   User Space    │     │   Agent Space   │
├─────────────────┤     ├─────────────────┤
│ Documents/      │     │ agent_output/   │
│ user_files/     │  ←→ │ temp_files/     │
│ settings.json   │sync │ cache/          │
└─────────────────┘     └─────────────────┘

Problems:

  • Need a sync layer to move data between spaces
  • User can't easily inspect agent work
  • Agent can't build on user contributions
  • Duplication of state
  • Complexity in keeping spaces consistent

The Shared Workspace Pattern

┌─────────────────────────────────────────┐
│           Shared Workspace              │
├─────────────────────────────────────────┤
│ Documents/                              │
│ ├── Research/                           │
│ │   └── {bookId}/        ← Agent writes │
│ │       ├── full_text.txt               │
│ │       ├── introduction.md  ← User can edit │
│ │       └── sources/                    │
│ ├── Chats/               ← Both read/write │
│ └── profile.md           ← Agent generates, user refines │
└─────────────────────────────────────────┘
         ↑                    ↑
       User                 Agent
       (UI)               (Tools)

Benefits:

  • Users can inspect, edit, and extend agent work
  • Agents can build on user contributions
  • No synchronization layer needed
  • Complete transparency
  • Single source of truth </why_shared_workspace>

<directory_structure>

Designing Your Shared Workspace

Structure by Domain

Organize by what the data represents, not who created it:

Documents/
├── Research/
│   └── {bookId}/
│       ├── full_text.txt        # Agent downloads
│       ├── introduction.md      # Agent generates, user can edit
│       ├── notes.md             # User adds, agent can read
│       └── sources/
│           └── {source}.md      # Agent gathers
├── Chats/
│   └── {conversationId}.json    # Both read/write
├── Exports/
│   └── {date}/                  # Agent generates for user
└── profile.md                   # Agent generates from photos

Don't Structure by Actor

# BAD - Separates by who created it
Documents/
├── user_created/
│   └── notes.md
├── agent_created/
│   └── research.md
└── system/
    └── config.json

This creates artificial boundaries and makes collaboration harder.

Use Conventions for Metadata

If you need to track who created/modified something:

<!-- introduction.md -->
---
created_by: agent
created_at: 2024-01-15
last_modified_by: user
last_modified_at: 2024-01-16
---

# Introduction to Moby Dick

This personalized introduction was generated by your reading assistant
and refined by you on January 16th.

</directory_structure>

<file_tools>

File Tools for Shared Workspace

Give the agent the same file primitives the app uses:

// iOS/Swift implementation
struct FileTools {
    static func readFile() -> AgentTool {
        tool(
            name: "read_file",
            description: "Read a file from the user's documents",
            parameters: ["path": .string("File path relative to Documents/")],
            execute: { params in
                let path = params["path"] as! String
                let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
                let fileURL = documentsURL.appendingPathComponent(path)
                let content = try String(contentsOf: fileURL)
                return ToolResult(text: content)
            }
        )
    }

    static func writeFile() -> AgentTool {
        tool(
            name: "write_file",
            description: "Write a file to the user's documents",
            parameters: [
                "path": .string("File path relative to Documents/"),
                "content": .string("File content")
            ],
            execute: { params in
                let path = params["path"] as! String
                let content = params["content"] as! String
                let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
                let fileURL = documentsURL.appendingPathComponent(path)

                // Create parent directories if needed
                try FileManager.default.createDirectory(
                    at: fileURL.deletingLastPathComponent(),
                    withIntermediateDirectories: true
                )

                try content.write(to: fileURL, atomically: true, encoding: .utf8)
                return ToolResult(text: "Wrote \(path)")
            }
        )
    }

    static func listFiles() -> AgentTool {
        tool(
            name: "list_files",
            description: "List files in a directory",
            parameters: ["path": .string("Directory path relative to Documents/")],
            execute: { params in
                let path = params["path"] as! String
                let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
                let dirURL = documentsURL.appendingPathComponent(path)
                let contents = try FileManager.default.contentsOfDirectory(atPath: dirURL.path)
                return ToolResult(text: contents.joined(separator: "\n"))
            }
        )
    }

    static func searchText() -> AgentTool {
        tool(
            name: "search_text",
            description: "Search for text across files",
            parameters: [
                "query": .string("Text to search for"),
                "path": .string("Directory to search in").optional()
            ],
            execute: { params in
                // Implement text search across documents
                // Return matching files and snippets
            }
        )
    }
}

TypeScript/Node.js Implementation

const fileTools = [
  tool(
    "read_file",
    "Read a file from the workspace",
    { path: z.string().describe("File path") },
    async ({ path }) => {
      const content = await fs.readFile(path, 'utf-8');
      return { text: content };
    }
  ),

  tool(
    "write_file",
    "Write a file to the workspace",
    {
      path: z.string().describe("File path"),
      content: z.string().describe("File content")
    },
    async ({ path, content }) => {
      await fs.mkdir(dirname(path), { recursive: true });
      await fs.writeFile(path, content, 'utf-8');
      return { text: `Wrote ${path}` };
    }
  ),

  tool(
    "list_files",
    "List files in a directory",
    { path: z.string().describe("Directory path") },
    async ({ path }) => {
      const files = await fs.readdir(path);
      return { text: files.join('\n') };
    }
  ),

  tool(
    "append_file",
    "Append content to a file",
    {
      path: z.string().describe("File path"),
      content: z.string().describe("Content to append")
    },
    async ({ path, content }) => {
      await fs.appendFile(path, content, 'utf-8');
      return { text: `Appended to ${path}` };
    }
  ),
];

</file_tools>

<ui_integration>

UI Integration with Shared Workspace

The UI should observe the same files the agent writes to:

Pattern 1: File-Based Reactivity (iOS)

class ResearchViewModel: ObservableObject {
    @Published var researchFiles: [ResearchFile] = []

    private var watcher: DirectoryWatcher?

    func startWatching(bookId: String) {
        let researchPath = documentsURL
            .appendingPathComponent("Research")
            .appendingPathComponent(bookId)

        watcher = DirectoryWatcher(url: researchPath) { [weak self] in
            // Reload when agent writes new files
            self?.loadResearchFiles(from: researchPath)
        }

        loadResearchFiles(from: researchPath)
    }
}

// SwiftUI automatically updates when files change
struct ResearchView: View {
    @StateObject var viewModel = ResearchViewModel()

    var body: some View {
        List(viewModel.researchFiles) { file in
            ResearchFileRow(file: file)
        }
    }
}

Pattern 2: Shared Data Store

When file-watching isn't practical, use a shared data store:

// Shared service that both UI and agent tools use
class BookLibraryService: ObservableObject {
    static let shared = BookLibraryService()

    @Published var books: [Book] = []
    @Published var analysisRecords: [AnalysisRecord] = []

    func addAnalysisRecord(_ record: AnalysisRecord) {
        analysisRecords.append(record)
        // Persists to shared storage
        saveToStorage()
    }
}

// Agent tool writes through the same service
tool("publish_to_feed", async ({ bookId, content, headline }) => {
    let record = AnalysisRecord(bookId: bookId, content: content, headline: headline)
    BookLibraryService.shared.addAnalysisRecord(record)
    return { text: "Published to feed" }
})

// UI observes the same service
struct FeedView: View {
    @StateObject var library = BookLibraryService.shared

    var body: some View {
        List(library.analysisRecords) { record in
            FeedItemRow(record: record)
        }
    }
}

Pattern 3: Hybrid (Files + Index)

Use files for content, database for indexing:

Documents/
├── Research/
│   └── book_123/
│       └── introduction.md   # Actual content (file)

Database:
├── research_index
│   └── { bookId: "book_123", path: "Research/book_123/introduction.md", ... }
// Agent writes file
await writeFile("Research/\(bookId)/introduction.md", content)

// And updates index
await database.insert("research_index", {
    bookId: bookId,
    path: "Research/\(bookId)/introduction.md",
    title: extractTitle(content),
    createdAt: Date()
})

// UI queries index, then reads files
let items = database.query("research_index", where: bookId == "book_123")
for item in items {
    let content = readFile(item.path)
    // Display...
}

</ui_integration>

<collaboration_patterns>

Agent-User Collaboration Patterns

Pattern: Agent Drafts, User Refines

1. Agent generates introduction.md
2. User opens in Files app or in-app editor
3. User makes refinements
4. Agent can see changes via read_file
5. Future agent work builds on user refinements

The agent's system prompt should acknowledge this:

## Working with User Content

When you create content (introductions, research notes, etc.), the user may
edit it afterward. Always read existing files before modifying them—the user
may have made improvements you should preserve.

If a file exists and has been modified by the user (check the metadata or
compare to your last known version), ask before overwriting.

Pattern: User Seeds, Agent Expands

1. User creates notes.md with initial thoughts
2. User asks: "Research more about this"
3. Agent reads notes.md to understand context
4. Agent adds to notes.md or creates related files
5. User continues building on agent additions

Pattern: Append-Only Collaboration

For chat logs or activity streams:

<!-- activity.md - Both append, neither overwrites -->

## 2024-01-15

**User:** Started reading "Moby Dick"

**Agent:** Downloaded full text and created research folder

**User:** Added highlight about whale symbolism

**Agent:** Found 3 academic sources on whale symbolism in Melville's work

</collaboration_patterns>

<security_considerations>

Security in Shared Workspace

Scope the Workspace

Don't give agents access to the entire filesystem:

// GOOD: Scoped to app's documents
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]

tool("read_file", { path }) {
    // Path is relative to documents, can't escape
    let fileURL = documentsURL.appendingPathComponent(path)
    guard fileURL.path.hasPrefix(documentsURL.path) else {
        throw ToolError("Invalid path")
    }
    return try String(contentsOf: fileURL)
}

// BAD: Absolute paths allow escape
tool("read_file", { path }) {
    return try String(contentsOf: URL(fileURLWithPath: path))  // Can read /etc/passwd!
}

Protect Sensitive Files

let protectedPaths = [".env", "credentials.json", "secrets/"]

tool("read_file", { path }) {
    if protectedPaths.any({ path.contains($0) }) {
        throw ToolError("Cannot access protected file")
    }
    // ...
}

Audit Agent Actions

Log what the agent reads/writes:

func logFileAccess(action: String, path: String, agentId: String) {
    logger.info("[\(agentId)] \(action): \(path)")
}

tool("write_file", { path, content }) {
    logFileAccess(action: "WRITE", path: path, agentId: context.agentId)
    // ...
}

</security_considerations>

## Real-World Example: Every Reader

The Every Reader app uses shared workspace for research:

Documents/
├── Research/
│   └── book_moby_dick/
│       ├── full_text.txt           # Agent downloads from Gutenberg
│       ├── introduction.md         # Agent generates, personalized
│       ├── sources/
│       │   ├── whale_symbolism.md  # Agent researches
│       │   └── melville_bio.md     # Agent researches
│       └── user_notes.md           # User can add their own notes
├── Chats/
│   └── 2024-01-15.json             # Chat history
└── profile.md                       # Agent generated from photos

How it works:

  1. User adds "Moby Dick" to library
  2. User starts research agent
  3. Agent downloads full text to Research/book_moby_dick/full_text.txt
  4. Agent researches and writes to sources/
  5. Agent generates introduction.md based on user's reading profile
  6. User can view all files in the app or Files.app
  7. User can edit introduction.md to refine it
  8. Chat agent can read all of this context when answering questions

<icloud_sync>

iCloud File Storage for Multi-Device Sync (iOS)

For agent-native iOS apps, use iCloud Drive's Documents folder for your shared workspace. This gives you free, automatic multi-device sync without building a sync layer or running a server.

Why iCloud Documents?

Approach Cost Complexity Offline Multi-Device
Custom backend + sync $$$ High Manual Yes
CloudKit database Free tier limits Medium Manual Yes
iCloud Documents Free (user's storage) Low Automatic Automatic

iCloud Documents:

  • Uses user's existing iCloud storage (free 5GB, most users have more)
  • Automatic sync across all user's devices
  • Works offline, syncs when online
  • Files visible in Files.app for transparency
  • No server costs, no sync code to maintain

Implementation Pattern

// Get the iCloud Documents container
func iCloudDocumentsURL() -> URL? {
    FileManager.default.url(forUbiquityContainerIdentifier: nil)?
        .appendingPathComponent("Documents")
}

// Your shared workspace lives in iCloud
class SharedWorkspace {
    let rootURL: URL

    init() {
        // Use iCloud if available, fall back to local
        if let iCloudURL = iCloudDocumentsURL() {
            self.rootURL = iCloudURL
        } else {
            // Fallback to local Documents (user not signed into iCloud)
            self.rootURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        }
    }

    // All file operations go through this root
    func researchPath(for bookId: String) -> URL {
        rootURL.appendingPathComponent("Research/\(bookId)")
    }

    func journalPath() -> URL {
        rootURL.appendingPathComponent("Journal")
    }
}

Directory Structure in iCloud

iCloud Drive/
└── YourApp/                          # Your app's container
    └── Documents/                    # Visible in Files.app
        ├── Journal/
        │   ├── user/
        │   │   └── 2025-01-15.md     # Syncs across devices
        │   └── agent/
        │       └── 2025-01-15.md     # Agent observations sync too
        ├── Experiments/
        │   └── magnesium-sleep/
        │       ├── config.json
        │       └── log.json
        └── Research/
            └── {topic}/
                └── sources.md

Handling Sync Conflicts

iCloud handles conflicts automatically, but you should design for it:

// Check for conflicts when reading
func readJournalEntry(at url: URL) throws -> JournalEntry {
    // iCloud may create .icloud placeholder files for not-yet-downloaded content
    if url.pathExtension == "icloud" {
        // Trigger download
        try FileManager.default.startDownloadingUbiquitousItem(at: url)
        throw FileNotYetAvailableError()
    }

    let data = try Data(contentsOf: url)
    return try JSONDecoder().decode(JournalEntry.self, from: data)
}

// For writes, use coordinated file access
func writeJournalEntry(_ entry: JournalEntry, to url: URL) throws {
    let coordinator = NSFileCoordinator()
    var error: NSError?

    coordinator.coordinate(writingItemAt: url, options: .forReplacing, error: &error) { newURL in
        let data = try? JSONEncoder().encode(entry)
        try? data?.write(to: newURL)
    }

    if let error = error {
        throw error
    }
}

What This Enables

  1. User starts experiment on iPhone → Agent creates Experiments/sleep-tracking/config.json
  2. User opens app on iPad → Same experiment visible, no sync code needed
  3. Agent logs observation on iPhone → Syncs to iPad automatically
  4. User edits journal on iPad → iPhone sees the edit

Entitlements Required

Add to your app's entitlements:

<key>com.apple.developer.icloud-container-identifiers</key>
<array>
    <string>iCloud.com.yourcompany.yourapp</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
    <string>CloudDocuments</string>
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
    <string>iCloud.com.yourcompany.yourapp</string>
</array>

When NOT to Use iCloud Documents

  • Sensitive data - Use Keychain or encrypted local storage instead
  • High-frequency writes - iCloud sync has latency; use local + periodic sync
  • Large media files - Consider CloudKit Assets or on-demand resources
  • Shared between users - iCloud Documents is single-user; use CloudKit for sharing </icloud_sync>
## Shared Workspace Checklist

Architecture:

  • Single shared directory for agent and user data
  • Organized by domain, not by actor
  • File tools scoped to workspace (no escape)
  • Protected paths for sensitive files

Tools:

  • read_file - Read any file in workspace
  • write_file - Write any file in workspace
  • list_files - Browse directory structure
  • search_text - Find content across files (optional)

UI Integration:

  • UI observes same files agent writes
  • Changes reflect immediately (file watching or shared store)
  • User can edit agent-created files
  • Agent reads user modifications before overwriting

Collaboration:

  • System prompt acknowledges user may edit files
  • Agent checks for user modifications before overwriting
  • Metadata tracks who created/modified (optional)

Multi-Device (iOS):

  • Use iCloud Documents for shared workspace (free sync)
  • Fallback to local Documents if iCloud unavailable
  • Handle .icloud placeholder files (trigger download)
  • Use NSFileCoordinator for conflict-safe writes