# Skill / Memory ↔ AGS 资源 — 权限矩阵与生命周期

本文件定义 Skill / Memory 与 AGS 仓库、AgentTeam 主体（user / workspace /
agent）和凭据（token / collaborator / grant）全生命周期的权限矩阵，并记录
MeshX 必须保持的仓库生命周期不变量。

---

## 1. 资源-主体映射（前置事实）

| AgentTeam 资源 | meshx 数据模型 | AGS 仓库角色 (`RepoRole`) | 仓库命名 |
| --- | --- | --- | --- |
| Memory | `memorystore.Memory` + `MemoryGrant` + `MemoryAgentBinding` + `AgentAssetAccessChangeOutbox` | `RepoRoleCanonical` （唯一一份，与 memory 1:1） | personal: `<user_id>/<slug>.mem`; workspace: `<workspace_id>/<slug>.mem` |
| Skill | `store.Skill` + `SkillGrant` + `SkillAgentBinding` + `AgentAssetAccessChangeOutbox` | `RepoRoleCanonical` （唯一一份，与 skill 1:1） | private visibility: `<user_id>/<slug>.sk`; workspace visibility: `<workspace_id>/<slug>.sk` |
| KB | 见 `specs/kb/kb-ags-permission-matrix.md` | `RepoRoleCanonical` 等 | personal: `<user_id>/<slug>.kb`; workspace: `<workspace_id>/<slug>.kb` |

**AGS Repository Identity** 由 `RepositoryIdentity{Owner, Name, FullName, ID, DefaultBranch}` 唯一刻画。

Memory / Skill 的 AGS owner segment 始终是拥有方身份本身：personal 使用
authenticated user id，workspace 使用 workspace id。资源后缀只属于 repository
name；owner 在任何 storage scope 下都不得追加 `.mem` 或 `.sk`。

## 1.1 统一产品决策（2026-06-25）

- MeshX 产品权限是 source of truth；AGS 权限只是仓库执行平面，不得反向决定 KB / Memory / Skill 的产品可见性或管理权。
- Memory / Skill 的 AGS 仓库默认不作为 workspace 目录资源暴露给人类，也不提供默认直达 AGS 的产品化操作入口；人类操作走 MeshX 产品面。
- Memory / Skill 用户权限面移除 `edit`，只保留 `owner` / `manage` / `view` / `none`（其中 `none` 可用缺省不可见表达）。遗留 `edit` 数据只能作为迁移兼容输入，UI 与新 grant 不得继续产生 `edit`。
- Skill 不作为 workspace catalog 默认可见；workspace 成员默认角色为 `none`（或等效缺省不可见），只有 owner、显式 user grant 或显式 workspace grant 才可见。workspace-wide Skill 可见性用一条 `skill_grants(subject_type=workspace, subject_id=<workspace_id>, role=view)` 表达，不按成员展开。
- Memory / Skill 智能体访问统一使用短期 `sync_lease` / Repository Access Token，不以长期 AGS collaborator 作为主要访问模型。
- 创建资源时如需让创建者获得 AGS 执行面能力，必须写入可观测、可重放的 access intent 并由 reconciliation 落地；不得依赖隐式“创建即永久 collaborator”。

**生命周期不变量**：

- MeshX 对 Memory / Skill 托管仓库的增删改查只通过 AGS API 完成，不直接访问 AGS 数据库。
- `resource_repositories` 是 MeshX 侧资源到 AGS repo identity 的权威映射；更新 Memory / Skill 元数据不会隐式 rename 或 move AGS repo。
- 删除或归档 Memory / Skill 时，MeshX 先通过 AGS `DELETE /api/v3/repos/{owner}/{repo}` 删除托管仓库；AGS 删除成功后才删除 / 归档 MeshX 业务资源并清理 `resource_repositories` 与 repository access grant 映射。
- AGS 删除失败时，MeshX 保留业务资源和 mapping，允许用户重试；异常写入 `resource_repository_lifecycle_events`。
- AGS repo 已创建但后续初始化、fork、业务元数据或 mapping 持久化失败时，MeshX 写入 `create_bind` lifecycle failure 作为补偿线索。
- AGS 删除成功但 MeshX 后续清理失败时，MeshX 写入 `delete_cleanup` lifecycle failure 作为补偿线索。

---

## 2. 主体与作用域

| 主体 | 字段来源 | 校验路径 |
| --- | --- | --- |
| **AgentTeam User** (`subject`) | authctx.Subject / `X-AgentTeam-User`（经 forwarder 注入，最终由 meshx 校验） | `validateSubject`（仅格式校验，不校验 workspace 成员关系 — Risk C-2） |
| **Workspace** (`workspaceID`) | `X-Workspace-Id` header | `validateScope`（仅格式校验；不校验调用方是否为该 workspace 成员） |
| **Agent** (`agentID`) | bind / unbind / batch-read 请求体或 path | `requireOwnedAgent` / `requireOwnedAgents`（基于 `AgentOwnershipSource.ListOwnedAgents` — fail-closed on nil source） |

**关键不变量**：
- `validateSubject` 只做格式校验，不是授权闸门
- `validateScope` 只做格式校验，**不做 workspace 成员关系校验**（见 §6 Risk C-2）
- `requireOwnedAgent*` 是真正的 agent-level 授权闸门，且对 nil source fail-closed

---

## 3. 权限矩阵（核心）

> 读法：每行 = (资源, 操作)。每列 = 调用方持有的角色。✅ = 允许 / ❌ = 拒绝（`ErrForbidden` 或同等）/ ⚠️ = 当前需要特别保持的边界。

### 3.1 Memory 资源操作 × Memory Grant Role

`edit` 不再属于 Memory 用户权限面；下表是目标产品矩阵。遗留 `edit`
grant 在迁移完成前只能作为兼容输入处理，不能由 UI 或新 grant 写入。

| 操作 (Memory) | `owner` | `manage` | `view` | `none` / outsider |
| --- | :---: | :---: | :---: | :---: |
| `GetMemory` / `ListMemories` | ✅ | ✅ | ✅ | ❌ |
| `UpdateMemory` / `PinMemory` / `DeleteMemory` | ✅ | ✅ | ❌ | ❌ |
| **`ListMemoryPermissions`** | ✅ | ✅ ⚠️ | ❌ | ❌ |
| **`UpsertMemoryPermission`** | ✅ | ✅ ⚠️ | ❌ | ❌ |
| **`DeleteMemoryPermission`** | ✅ | ✅ ⚠️ | ❌ | ❌ |
| `BindAgentMemory` / `UnbindAgentMemory` | ✅ | ✅ | ❌ | ❌ |

⚠️ = **C-1 invariant**：这三处 grant mutation gate 必须使用 `roleCanManage`。遗留 `edit` 角色不能增删 `view`/`manage` grant。

> 实现位置：
> - `internal/service/memory/memory_permissions.go::ListMemoryPermissions` (`canGrant := roleCanManage(myRole)`)
> - `internal/service/memory/memory_permissions.go::UpsertMemoryPermission`（权限闸门）
> - `internal/service/memory/memory_permissions.go::DeleteMemoryPermission`（权限闸门）

### 3.2 Skill 资源操作 × Skill Role

Skill 不作为 workspace catalog 默认可见；workspace 成员缺省为 `none`。目标
产品面与 Memory 一致移除 `edit`，只暴露 `owner` / `manage` / `view` / `none`。
创建时选择 workspace 可见会写入一条 workspace `view` grant；private 是默认值。

| 操作 (Skill) | `owner` | `manage` | `view` | `none` / outsider |
| --- | :---: | :---: | :---: | :---: |
| `GetSkill` / `ListSkills` | ✅ | ✅ | ✅ | ❌ |
| `UpdateSkill` / `ArchiveSkill` / `CreateSkill` | ✅ | ✅ | ❌ | ❌ |
| `BindAgentSkill` / `UnbindAgentSkill` | ✅ | ✅ | ❌ | ❌ |
| **`BatchListAgentAccessibleSkills`** | 见 §3.4 | 见 §3.4 | 见 §3.4 | — |

Skill role 判定优先级为 creator / owner，其次 explicit user grant，其次 workspace `view` grant，最后为 `none` / not visible。

### 3.3 Agent 绑定读写 × Agent Ownership

| 操作 | caller 拥有该 agent | caller 不拥有 |
| --- | :---: | :---: |
| `BindAgentMemory` / `BindAgentSkill` | ✅（仍需 memory/skill manage） | ❌ `ErrForbidden`（已存在） |
| `UnbindAgentMemory` / `UnbindAgentSkill` | ✅ | ❌ `ErrForbidden`（已存在） |
| **`GetAgentAccessibleMemory` (single)** | ✅ ⚠️ | ❌ `ErrForbidden` ⚠️ |
| **`BatchListAgentAccessibleMemories`** | ✅（全部 caller-owned） ⚠️ | ❌ `ErrForbidden` ⚠️（任一非 owned 即 fail-closed） |
| **`BatchListAgentAccessibleSkills`** | ✅（全部 caller-owned） ⚠️ | ❌ `ErrForbidden` ⚠️（任一非 owned 即 fail-closed） |

⚠️ = **D-2 invariant**：这三条 agent-access read 路径必须校验 ownership，并全部 fail-closed。

> 实现位置：
> - `internal/service/memory/memory_permissions.go:13-37` (`GetAgentAccessibleMemory` 增 `requireOwnedAgent`)
> - `internal/service/memory/memory_permissions.go:48-95` (`BatchListAgentAccessibleMemories` 增 `requireOwnedAgents`)
> - `internal/service/skills/service.go::BatchListAgentAccessibleSkills`（调用 `requireOwnedAgents`）

### 3.4 Batch Read 失败语义

`requireOwnedAgents` 的语义是**整体 fail-closed**：如果请求中的 `agentIDs` 列表里**任何**一个 agent 不属于 caller，整个请求返回 `ErrForbidden`，**不返回任何 agent 的数据**。

这一语义选择有两个理由：
1. 与 `BindAgentMemory` / `BindAgentSkill` 的单条 `requireOwnedAgent` 行为一致（fail-closed on first non-owned）
2. 防止 partial-response 信息泄漏：如果只屏蔽非 owned 的 agent，攻击者可以通过观察响应中缺失的 key 来枚举哪些 agentID 存在于该 workspace

Desktop 实际使用中，`/api/v1/{memory,skills}/agents/access:batch` 只发送 Desktop 当前 host 的 agentIDs（caller 必然全部 own），因此不会触发该 fail-closed。

---

## 4. 凭据 / Token / Outbox 生命周期

### 4.1 Grant (Memory)

| 事件 | 触发方 | 闸门 | 副作用 |
| --- | --- | --- | --- |
| 创建 grant | manage/owner | `roleCanManage`（C-1 后） | 写 `MemoryGrant` 表；**不写 outbox**（user-level grant 不影响 agent 访问） |
| 更新 grant role | manage/owner | `roleCanManage` | upsert |
| 删除 grant | manage/owner | `roleCanManage` + 拒绝删 owner grant | delete |

> ⚠️ owner grant 转让语义：`UpsertMemoryPermission` 允许 owner grant 一个新 owner（不阻止）。如果产品要求"owner 只能转让不能新增 owner"，需要额外规则（参考 KB 的 `kb_permissions.go:235-237`）。

### 4.2 Agent Binding (Memory / Skill)

| 事件 | 触发方 | 闸门 | Outbox (`AgentAssetAccessChangeOutbox`) |
| --- | --- | --- | --- |
| Bind agent | memory/skill manage | `requireOwnedAgent` + memory/skill role gate | **同一 DB tx** 内写 outbox 行 |
| Unbind agent | memory/skill manage | 同上 | 同上 |

> 关键不变量：bind/unbind mutation 与 outbox intent **在同一事务**内提交。daemon 通过 `agent:assets:access-changed` 事件 drain outbox，触发本地 cache 拉新。
> 浏览器 Settings 面也必须保持同一个 source of truth：Skill detail Settings
> 直接列出当前绑定到该 Skill 的 owned agents，并在行内调用 revoke /
> `UnbindAgentSkill`；Memory detail Settings 直接显示当前 Memory 的单个
> agent binding，并在行内调用 `UnbindAgentMemory`。批量 bindings 页可以继续
> 存在，但不应成为资源详情撤销访问的唯一入口。

### 4.3 AGS Repository Access Token

- Desktop 的 Memory / Skill Git 同步使用 AGS `sync_lease`，由
  `repository_access_tokens` service 通过嵌入式 AGS contract 签发。
- `/api/v1/{memory,skills}/agents/access:batch` 是管理面调用，使用 AgentTeam
  user token；响应中的 `syncToken` / `syncTokenExpires` 是数据面凭据，
  scope 精确覆盖本次响应里的 canonical repos。
- Desktop Git clone / fetch / push 使用
  `Authorization: Bearer <ags_rat_...>`。AgentTeam user token 不能作为 Git
  credential，也不能 fallback 成 Git credential。
- Skill lease capabilities 是 `repo:read` + `repo:clone`；Memory lease
  capabilities 是 `repo:read` + `repo:clone` + `repo:write`，用于本地 memory
  sync/push。
- lease 是短生命周期凭据；过期后 Desktop 通过下一次 access-batch 重新获取，
  不在本地续租或持久化 plaintext。
- token 失效：当 `UnbindAgent*` 写 outbox 后，daemon 收到
  `agent:assets:access-changed` → 触发 `RefreshMemoryAccess` /
  `RefreshSkillAccess`。Memory 走 `RetainOffline` detach remote 并保留磁盘内容；
  Skill 清空该 agent 的 in-memory binding cache，但本地 clone retention 策略仍需
  产品决策（见 §6 Risk E）。

### 4.4 Outbox 与 Desktop refresh 时机

- bind/unbind mutation 与 `AgentAssetAccessChangeOutbox` 在同一事务提交；
  outbox 是 MeshX → AgentTeam server → Desktop realtime event 的 durable handoff，
  不是 Skill/Memory 内容拉取本身。
- AgentTeam server 的 asset-access worker lease due outbox rows，并通过 daemon
  WebSocket 推送 `agent:assets:access-changed`。Desktop 收到事件后按
  `asset_types` 分发到 `RefreshMemoryAccess` / `RefreshSkillAccess`。
- Skill 启动链路必须 cache-only：agent/session launch 与 execution-launch
  只读 `SkillSyncer` 最近一次 authoritative cache，不调用 `SyncForAgent`，
  不 clone/fetch。
- Skill 内容拉取由 realtime access-change event 和 daemon 低频 background
  refresh 驱动。后台 refresh 可以使用较低频率；多个 agent 绑定同一个 Skill
  repo 时，同一 refresh 窗口内共享同一份本地 clone/fetch 结果。

### 4.5 Desktop 本地缓存三态（`LookupBound` / `LookupUnbound` / `LookupUnknown`）

| 状态 | 含义 | cache 处理 |
| --- | --- | --- |
| `LookupBound` | 服务端确认有 binding | 保留 / 拉取 |
| `LookupUnbound` | 服务端确认无 binding（authoritative empty） | 清空 in-memory cache |
| `LookupUnknown` | 服务端不可达 / 临时错误 | **保留** 当前 cache（不破坏，不擦除） |

> 关键不变量：`LookupUnknown` 必须**保留** cache，不能误清。这是 "unknown ≠ unbound" 原则。

---

## 5. 已收敛的高优先级缺口

### C-1: Memory grant gate edit → manage

**问题**：`ListMemoryPermissions` / `UpsertMemoryPermission` / `DeleteMemoryPermission` 用 `roleCanEdit`，导致 `edit` 角色可以增删 `MemoryGrant`（包括给其他人授 `edit` 或 `view`，从而提权或绕过 manager 控制）。

**当前规则**：三处全部使用 `roleCanManage`，错误信息为 `"memory requires manage access"`。

**测试**：
- `internal/service/memory/memory_test.go::TestMemoryServiceEnforcesMemoryGrantRoles`（既有，把 editor 期望从 200 改为 403）
- `internal/service/memory/memory_test.go::TestListMemoryPermissionsRequiresManageRole`（新增，覆盖 list / upsert / delete 三处 gate）

### D-2: Agent-access read 端点缺 ownership 校验

**问题**：`GetAgentAccessibleMemory` / `BatchListAgentAccessibleMemories` / `BatchListAgentAccessibleSkills` 只做 `validateSubject`（格式校验），不校验 caller 是否 own 请求中的 agentID(s)。任意持有合法 auth 的调用方可读取**他人 agent** 当前绑定的 memory/skill（信息泄漏：memory name、skill name、repo URL、branch、AGSRepoID）。

**当前规则**：
- 单条 `GetAgentAccessibleMemory` 增 `requireOwnedAgent`
- 批量端点新增 `requireOwnedAgents` 辅助函数（一次拉 owned 集合，全部命中才放行），在 memory_permissions.go 与 skills/service.go 各落地一份（镜像 `requireOwnedAgent` 单条模式，避免引入跨 service 共享 helper / shared contract 变更）

**测试**：
- `internal/service/memory/memory_test.go::TestGetAgentAccessibleMemoryRejectsNonOwnedAgent`（新增）
- `internal/service/memory/memory_test.go::TestBatchListAgentAccessibleMemoriesRejectsNonOwnedAgent`（新增）
- `internal/service/skills/service_test.go::TestBatchListAgentAccessibleSkillsRejectsNonOwnedAgent`（新增）

---

## 6. 已知残留风险

### Risk C-2 — Workspace 成员关系校验缺失

`validateSubject` / `validateScope` 都只做格式校验。`MemoryGrant.SubjectID` 可以填任意 user ID，不校验是否为该 workspace 成员。

- **影响**：manage/owner 可以给一个不在 workspace 里的 user 授 grant（但该 user 仍需通过 forwarder auth 才能调用 meshx API）
- **决策需要**：meshx 是否新增 `WorkspaceMembershipSource` 依赖，由 AgentTeam 注入？
- **优先级**：P1（需要新增 DI 面）

### Risk E — Skill local clone retention gap

当 skill revoke（`LookupUnbound`）时，agent scope 的 in-memory cache 会被清空，
但磁盘 clone `~/.agentteam/managed-skills/<repoID>/` 仍保留，且没有 Memory 那种
`RetentionState` 元数据。

- **影响**：磁盘泄漏 + 残留内容可能包含 token 历史 / 旧版本 SKILL.md
- **不对称**：Memory 一侧有 `desktop/internal/vault/retention.go::RetainOffline`，会在 revoke 时 detach git 但保留内容 + 写 `RetentionState{RevokedAt: ...}`；Skill 没有对应机制
- **决策需要**：Skill revoke 应该 (a) 对齐 Memory 的 `RetainOffline` 模式（保留磁盘内容 + 元数据），还是 (b) 硬删除磁盘 clone？这需要产品决策
- **优先级**：P1（需要产品决策 + 跨模块实现）

### Risk F — Skill non-hot-reload 语义

Skill binding 变更不触发运行中 leader agent 进程的 skill reload（与 Memory 不同）。daemon 通过 outbox 触发 `RefreshSkillAccess`，更新本地 cache 和 running session 的 `managedSkillPaths`，后续 execution-launch sub-agent 和下一次 agent 启动会生效；已启动的 leader 进程不热加载。

- **影响**：用户在 agent 运行中改 skill binding，需要重启 agent 才能看到新 skill
- **当前状态**：by-design
- **优先级**：低（产品已知，文档已说明）

---

## 7. 模块所有权边界

Memory / Skill 权限与 agent-access 读路径由以下模块维护：
- `internal/service/memory/memory_permissions.go`（Memory grant + agent-access read）
- `internal/service/skills/service.go`（Skill batch agent-access read）
- 对应 `*_test.go`

KB 权限规则由 KB service 模块维护：
- `internal/service/kb/kb_permissions.go`
- `internal/service/kb/kb*.go`

跨模块约束：
- 任何 shared helper / shared contract（`requireOwnedAgent` / `requireOwnedAgents` 在 memory 和 skill 各落地一份，避免跨 service 依赖）
- Desktop / daemon 行为独立维护；Skill clone retention 需要产品决策后再调整。

如需引入共享 helper（如抽取 `requireOwnedAgents`），需要先确认稳定的权限 owner 和调用方语义，避免把 KB、Memory、Skill 的权限 contract 混在一起。
