feat(controller): add team-scoped worker knowledge base file endpoints (+ checkpoint scoped-access fix) - #1208
Conversation
shiyiyue1102
left a comment
There was a problem hiding this comment.
Thanks for the update. The team-scoped knowledge-base direction is reasonable, and the current CI plus the focused controller tests are green, but I think three issues should be addressed before merging:
-
An empty
workspaceFileAccessis treated asreadwrite. This means upgrading the Controller silently grants every existing L2 Human a new ability to modify Worker knowledge files. Please make write access an explicit opt-in (for example, empty/default →read) and add upgrade/default regression coverage, or document and obtain agreement on the intended permission migration. -
validateKbPathacceptsMEMORY.md/foobecause it checks only the first segment. This contradicts the documented contract thatMEMORY.mdis one top-level file. Please require exactly one segment for file roots and add read/write/download regression cases. -
Please split the checkpoint scope fix and the
mcp<2dependency pin into focused PRs. The same dependency patch is currently duplicated in #1209–#1212, while this PR is also becoming the base of the Human-update change; keeping these unrelated commits bundled makes the dependency and rebase order unnecessarily fragile.
Because this changes public storage and authorization behavior, please also link an issue or design discussion before merge.
感谢更新。按团队范围访问知识库的方向是合理的,当前 CI 和 Controller 定向测试也已通过,但合并前仍建议处理三个问题:
-
workspaceFileAccess为空时会按readwrite处理。这意味着 Controller 升级后,所有既有 L2 Human 会被静默授予修改 Worker 知识文件的新权限。建议将写权限改成显式开启,例如空值/默认值按read处理,并补充升级及默认权限回归测试;如果确实需要默认开放写权限,则应明确记录并确认该权限迁移决策。 -
validateKbPath目前只检查首个路径段,因此会放行MEMORY.md/foo,与文档中“MEMORY.md是单个顶层文件”的契约不一致。请对文件根路径要求恰好一个 segment,并补充读、写、下载三条路径的回归测试。 -
请将 checkpoint scope 修复和
mcp<2依赖修复拆成独立 PR。相同的依赖 patch 目前还重复存在于 #1209–#1212,同时本 PR 又开始成为 Human 更新功能的基础依赖;继续混合会让依赖关系和 rebase 顺序变得不必要地脆弱。
另外,本 PR 修改了公开的存储及授权行为,合并前请关联对应 Issue 或设计讨论。
36bc4f2 to
8c388ac
Compare
|
@shiyiyue1102 One more note on the red CI on this PR (separate from the review points): all four Suggested order: land #1215 first; I'll then rebase this PR (and #1214 / #1216, same situation — none of them touch |
8c388ac to
92073c5
Compare
oss-maintainer
left a comment
There was a problem hiding this comment.
Summary
Adds team-scoped read/write proxy endpoints for worker knowledge base files (MEMORY.md, memory/**, digest/**) with a Human CRD workspaceFileAccess opt-in write gate — a well-designed, security-conscious change. Verified against main@ac22c88: allowlist validation is fail-closed (dot segments, traversal, backslash/NUL, depth and segment-length caps all rejected before reaching the worker); the scoped-caller chain uses findTeamMember(...).Name correctly (the #1214 fix pattern), standalone workers hide as 404 via TeamMatches("")==false; worker SAs cannot read other workers' files (authorizeWorkerSelfAction requireSelf); the Human CR lookup key matches the Matrix authenticator's Username: h.Name assignment; the write path defaults to read-only on empty workspaceFileAccess (no silent grant on upgrade), denies leaders, enforces the If-Match policy via existence probe, caps bodies at 1 MiB, and audit-logs every write. CRD manifests (config + helm) are enum-validated and in sync. 41 tests cover the allowlist, traversal, cross-team hiding, write-role matrix, ETag policy, and passthrough semantics. Only two informational notes (inline).
Findings
- [Info] worker_workspace_files.go:567 — negligible create-path probe/write race (documented as accepted)
- [Info] worker_workspace_files.go:510 — EqualFold vs case-sensitive CRD enum (harmless today)
Automated review by github-manager-bot
| httputil.WriteError(w, http.StatusBadRequest, "If-Match header is required to update an existing file") | ||
| return | ||
| } | ||
| if !exists && ifMatch != "" { |
There was a problem hiding this comment.
[Info] Tiny TOCTOU window on the create path: if the worker app itself creates this exact path between the file-metadata probe (404 → create branch) and the bare PUT, the upstream write goes through without an ETag guard and would overwrite the just-created file (lost update). The window is milliseconds and MEMORY.md/memory//digest/ are created at workspace init, so this is negligible in practice — noting it only so the race is documented as accepted. The mandatory If-Match on the probe-existing path is the right call.
| // workspaceFileAccess means "read". Defaulting to readwrite would | ||
| // silently grant every pre-existing L2 human a new ability to | ||
| // modify worker knowledge files on controller upgrade. | ||
| if !strings.EqualFold(human.Spec.WorkspaceFileAccess, "readwrite") { |
There was a problem hiding this comment.
[Info] strings.EqualFold compares case-insensitively while the CRD enum is case-sensitive lowercase (read/readwrite), so this is equivalent to an exact match today. Mentioning only because the lenient comparison would silently accept e.g. ReadWrite if the enum validation is ever relaxed — an exact == "readwrite" would keep the handler strictly bounded by the CRD contract.
92073c5 to
090c09a
Compare
|
Re-reviewed head [P1] The registered PUT route rejects every authorized write with HTTP 400. I reproduced this through the actual Please keep the fixed PUT route and remove the handler's dependency on a nonexistent 已复查最新提交 [P1] 实际注册的 PUT 路由会让所有通过授权的写请求返回 HTTP 400。 已使用 admin 身份通过实际 建议保留固定 PUT 路由,移除 handler 对不存在的 |
…L2 humans
L2 humans have no read path to their team's knowledge base: the /docker/
proxy is admin/Manager-only, and the checkpoint endpoints expose file
indexes, not the knowledge text. Proxy three read-only QwenPaw workspace
file endpoints (QwenPaw >= 2.1) through the Controller using the fixed-
subpath pattern from the worker checkpoint proxy:
GET /api/v1/workers/{name}/workspace-files/tree
GET /api/v1/workers/{name}/workspace-files/file-metadata
GET /api/v1/workers/{name}/workspace-files/file-content
- Path allowlist (not a denylist): only MEMORY.md, memory/** and
digest/** are addressable. Every other workspace location - SOUL.md,
PROFILE.md, TODO.md, checkpoints/, skills/ and all dot directories
(.copaw/agent.json carries the worker's credentials) - is rejected
with 400 before the request reaches the worker. The upstream path
resolver serves directly requested dot paths, which is why the
allowlist lives in the Controller; covered by an explicit test.
- root=workspace pinned server-side (the QwenPaw default root=project is
the primary bound project directory, not the knowledge base); the
client query surface has no root parameter and the upstream query is
rebuilt from validated values only.
- Strict per-subpath query whitelist (unknown/duplicate parameters are
400); limit bounds mirror the upstream caps (tree 1..500, file-content
1..1048576); cursor is an opaque passthrough; offset >= 0.
- Same address resolution as the checkpoint proxy: effective container
prefix + system-wins console port (a conflicting spec.env port is
discarded, so the proxy always dials the port the container listens
on). Embedded mode only; kube mode returns a uniform 503 before any
worker lookup.
- Authorization reuses the existing chain: RequireAuthz(ActionGet,
"worker") (RoleHuman already granted by the L2 authentication work)
plus the handler-level team-scope check, so unknown, out-of-scope and
standalone workers are all hidden as 404. No authorizer changes.
- Version gate: a worker on QwenPaw < 2.1 has no workspace file router;
the upstream 404 is passed through verbatim and the documented
file-metadata?path=MEMORY.md probe distinguishes "worker upgrade
required" from "file missing".
- Write endpoints (file-content PUT, file-upload) and binary streaming
(file-download) are not in the subpath whitelist.
Tests: 24 new (forwarding with root-pinning assertion, allowlist
positive/negative tables, sensitive-path and traversal rejection,
write-subpath rejection, scope 200/404 matrix, kube 503 before lookup,
unreachable 502, 404 passthrough, bounded 500, prefix+port matrix).
go test ./... green against the main baseline (only pre-existing failure
is the sandbox-missing unzip binary in internal/executor); gofmt/vet
clean; rename gate passes.
…e download
- PUT /api/v1/workers/{name}/workspace-files/file-content: L1 writes any
team; L2 humans write only own teams while Human CR
workspaceFileAccess != "read" (default readwrite, L1-locked to read);
team leaders stay read-only; cross-team hides as 404 (W8)
- authorizer: new workspace-files-write action, deliberately
authorizer-allowed cross-team for RoleHuman — the middleware resolves
the worker's team, and a 403 there would leak worker existence (the
read path hides cross-team as 404; the handler is the real boundary,
pinned by authorizer tests); separate action so it does not collide
with the L2 worker-update PR
- If-Match mandatory for existing files (lost-update guard vs worker
auto-append), forbidden for new files; 1 MiB write cap; empty content
rejected; every write audit-logged
- GET /api/v1/workers/{name}/workspace-files/file-download: bounded stream
with attachment headers forwarded, same KB allowlist and scope
- Human CRD gains workspaceFileAccess (read|readwrite); CRD synced to Helm
- 41 tests green (24 read + 16 write/download + 1 checkpoint regression +
2 authorizer W8 cases); full controller suite green (1 pre-existing
env-only failure: executor ZIP test needs unzip, present in CI)
… route
The registered write route is the fixed literal
/api/v1/workers/{name}/workspace-files/file-content (http.go) with no {sub}
capture, so r.PathValue("sub") in proxyWorkspaceFileWrite was always ""
and every authorized write (admin / human with
workspaceFileAccess=readwrite) was rejected with 400
"unsupported workspace file write subpath" before any worker lookup.
- keep the fixed PUT route as the single source of truth for the write
subpath and drop the handler's dependency on the nonexistent sub path
value (the now-unreferenced workspaceFileWriteSubpaths map is removed)
- add TestWorkspaceFilesWrite_RouteAcceptsAuthorizedWrite, which drives the
request through the actual NewHTTPServer(...).Mux registration (no
SetPathValue shims): an admin PUT reaches the upstream probe stage
(502 against the dead pod URL in tests) instead of the phantom 400, and
an unknown worker yields the 404 from the worker lookup
090c09a to
53e2601
Compare
|
Re-reviewed Merge is still waiting on CI: the QwenPaw/QwenPaw SHARD_C job failed in Job: https://github.com/agentscope-ai/AgentTeams/actions/runs/34571257155/job/103176490682 Could you investigate this assertion and fix it, or establish that it is transient and rerun the failed job? The failure alone does not establish a regression from the workspace-files changes; we still need a passing current-head CI run before merging. |
shiyiyue1102
left a comment
There was a problem hiding this comment.
LGTM. Re-reviewed head 53e2601: the fixed PUT route no longer reads a nonexistent sub capture, with regression coverage through the actual NewHTTPServer mux. Default write access remains opt-in, MEMORY.md is restricted to a single top-level file, unrelated changes were split out, and #1217 documents the design. Local server/auth tests pass. Both previously failing integration jobs passed on the same-head rerun; current checks are successful with the expected conditional skips. The blocking findings from my previous review are addressed.
Team-scoped worker knowledge base file endpoints: read, write, download
Summary
L2 humans (and team leaders) currently have no path to their team's knowledge base — not read, not write. The only way to reach a worker's workspace files is the Controller's
/docker/proxy, which is admin/Manager-only: an L2 human's Matrix token gets403on every attempt, and the checkpoint endpoints (#1186) expose file indexes and change summaries, not the knowledge text itself.This PR proxies four QwenPaw workspace-file endpoints through the Controller, following the exact fixed-subpath proxy pattern established by the worker checkpoint endpoints (#1186):
GET /api/v1/workers/{name}/workspace-files/tree?path=memory|digest[/...]— paginated directory listing of the knowledge treesGET /api/v1/workers/{name}/workspace-files/file-metadata?path=...— size/etag/modified of one knowledge fileGET /api/v1/workers/{name}/workspace-files/file-content?path=...[&offset=&limit=]— bounded UTF-8 chunk read withnext_offsetcontinuationPUT /api/v1/workers/{name}/workspace-files/file-content?path=...— ETag-guarded save of one knowledge file (create or update)GET /api/v1/workers/{name}/workspace-files/file-download?path=...— bounded stream of one knowledge file with attachment headersThe addressable surface is a path allowlist, identical for reads and writes:
MEMORY.md(long-term memory),memory/**(daily notes) anddigest/**(distilled knowledge). Everything else in the workspace —SOUL.md,PROFILE.md,TODO.md,checkpoints/,skills/, and all dot directories (.copaw/agent.jsoncarries the worker's Matrix token and storage credentials) — is rejected with400before the request reaches the worker, even though the upstream path resolver would serve a directly requested dot path.Write access is per-user, L1-configurable, and an explicit opt-in. The Human CRD gains
spec.workspaceFileAccess: read | readwrite(default isreadwhen empty). Admin/manager (L1) may write any team's knowledge base; an L2 human may write only workers in their own teams and only while their Human CR is explicitly set toreadwrite— an empty or missing value means read-only, so a controller upgrade cannot silently grant pre-existing L2 humans the new ability to modify worker knowledge files. L1 grants a user write access by setting the field toreadwrite(and revokes it by clearing the field or settingread). Team leaders stay read-only on this API. The concurrent-write hazard this design exists to close: workers auto-append to their memory files, so the proxy requiresIf-Matchon existing files (a skipped ETag check is a lost update) and forbids it on new files; upstream ETag mismatches pass through as409.Split out per review: the checkpoint proxy scoped-caller fix (member name vs team name) and the copaw-worker
mcp>=1, <2dependency pin are now separate, focused PRs — neither is included in this PR. This PR is the workspace-files feature only.What's included
agentteams-controller/api/v1beta1/types.go+ CRD manifests:HumanSpecgainsworkspaceFileAccess(read|readwrite, optional, defaultreadwhen empty — write is an explicit opt-in) — the per-user knowledge base write permission L1 sets per user.config/crd/humans.agentteams.io.yaml+helm/agentteams/crds/humans.agentteams.io.yamlupdated in sync (enum-validated;make check-crd-syncgreen). No deepcopy regeneration needed (plain string, covered by the struct copy).agentteams-controller/internal/auth/authorizer.go:workspace-files-write. ForRoleHumanon resourceworker: allowed at the authorizer level for the same W8 reason as reads — the middleware resolves the worker's team, and a cross-team denial there would leak worker existence via403(the read path hides it as404). The handler is the real boundary (404 hiding + per-user flag + allowlist). Deliberately a separate action fromupdate, so this PR does not entangle with the L2 worker-update authorization work.agentteams-controller/internal/server/worker_workspace_files.go(new):WorkspaceFilesHandler— fixed-subpath proxy (tree / file-metadata / file-content GET+PUT / file-download) to the worker's qwenpaw app on the shared docker network. Embedded mode only; kube mode returns a uniform503before any worker lookup (no 404/503 existence split).validateKbPath— the knowledge allowlist: exact root matching (memory,digest,MEMORY.md—memories/andmemoryX/are not prefixes ofmemory/), file roots are single top-level files (MEMORY.md/foois rejected —MEMORY.mdis one file, not a directory; nested files must live undermemory/ordigest/), no dot segments (kills.copaw/,.git/,.qwenpaw/,.reme_store_v1/), no.././absolute/backslash/NUL, ≤4 segments, ≤255 bytes per segment (mirroring the upstream segment limit).400, same semantics as the checkpoint proxy);limitbounds mirror the upstream caps (tree 1..500 =MAX_PAGE_SIZE; file-content 1..1048576 =MAX_CHUNK_SIZE);cursoris an opaque passthrough;offset≥ 0.root=workspaceis pinned server-side and is not part of the client query surface — the QwenPaw defaultroot=projectis the primary bound project directory, not the knowledge base. The upstream query is rebuilt from the validated values only.proxyWorkspaceFileWrite(PUT file-content): write-role boundary (admin/manager any team; L2 human own teams + explicitworkspaceFileAccess="readwrite"— empty/missing = read-only; team leader403; cross-team hidden as404), then afile-metadataexistence probe deciding the If-Match policy (existing → header mandatory; new → header forbidden), 1 MiB body cap, non-emptycontent(an empty write would truncate a worker's memory file), verbatim passthrough of400/404/409/422, and an audit log line per successful write (worker, path, caller, role, bytes, create).file-download: the upstreamStreamingResponse(256 KiB chunks,Content-Disposition: attachment,Content-Length,ETag) is streamed through with those headers forwarded — clients save the file under the worker's file name.service.EffectiveWorkerConsolePort(system-wins env chain — a conflictingspec.envport is discarded, so the proxy always dials the port the container listens on).agentteams-controller/internal/server/http.go: +5 lines across the PR — handler construction, theGET /api/v1/workers/{name}/workspace-files/{sub}route, and thePUT .../workspace-files/file-contentroute registered withRequireAuthz(workspace-files-write, "worker")..copaw/agent.jsonand friends / traversal rejection / non-KB rejection incl. prefix-confusion names / unknown+dup query rejection / bound violations / missing-path rejection / write subpaths (file-upload, restore) rejected via GET / invalid worker name / unknown worker 404 / L2 in-scope 200 (the purpose case) / L2 cross-team 404 / team-leader cross-team 404 / standalone worker hidden from scoped callers but visible to admin / kube 503 before lookup / unreachable 502 / upstream-404 passthrough / bounded upstream-500 / prefix+port resolution matrix (8 cases) / end-to-end effective prefix+port dialing) + 17 write/download (create without If-Match / update with matching If-Match / existing-file-without-If-Match rejected before the worker is touched / new-file-with-If-Match rejected / 409 conflict passthrough / cross-team 404 / read-only human 403 / emptyworkspaceFileAccess(upgrade default) 403 / in-scope leader 403 / admin 200 / 1 MiB+1 rejected before the worker / write allowlist table incl. SOUL.md andMEMORY.md/foo/ kube 503 / non-write subpaths + invalid bodies 400 / download 200 with attachment headers forwarded / download cross-team 404 / download allowlist even for admin incl.MEMORY.md/foo).docs/usage/project-workflow-api.md(+docs/zh-cn/usage/project-workflow-api.md): "Worker knowledge base (workspace files) endpoints" sections — endpoint table (4 endpoints), read scope + write scope, concurrency (ETag) contract, write limits, theworkspaceFileAccessfield, allowlist, root-pinning, version-gate contract (thefile-metadata?path=MEMORY.mdprobe that distinguishes "worker on QwenPaw < 2.1" from "file missing"), error-code table.Data boundary & security
MEMORY.md/memory/**/digest/**) applies to both.SOUL.mdis the team owner's domain and is never writable through this API — not by L1, not by anyone (the identity-and-soul boundary is enforced by the path allowlist, not by the role matrix).404— the same W8 rule as reads; the authorizer deliberately does not reject cross-team for this action, because the middleware resolves the worker's team and a403there would leak existence) → per-userworkspaceFileAccess(403unless explicitlyreadwrite; also403when the Human CR cannot be read) → leader403→ If-Match policy (400) → 1 MiB / non-empty body (400) → allowlist (400). Any single layer failing closed still protects the fleet.If-Match) of an existing file, which would silently lose the worker's own appends between the L2 client's read and write. The proxy probesfile-metadatafirst and makesIf-Matchmandatory in that case — the client cannot downgrade the guarantee.400. The upstream app's own path hardening (no.., no absolute, symlink-confined, dot entries hidden in listings) is not relied upon for dot directories: a directly requested.copaw/agent.jsonresolves fine upstream, which is exactly why the Controller-side allowlist exists. Covered by explicit tests for both read and download.CompositeAuthenticator(feat(controller): add project/workflow query API for human-visible workflows #1169); scope is theiraccessibleTeams. No admin token, no new roles.404for every subpath; it is passed through verbatim, and the documentedfile-metadata?path=MEMORY.mdprobe (present in every initialized workspace) lets clients show "worker upgrade required" instead of "file missing". Current fleet workers (2.0.1) light up automatically after the 2.1+ migration (feat(qwenpaw): upgrade QwenPaw Worker runtime to 2.2 #1174 / feat(manager): upgrade Manager runtime to QwenPaw 2.2 #1175) — no Controller redeploy.404; kube mode returns503before any lookup.502fallback echoes a 4 KiB-bounded upstream body (same pattern as the checkpoint proxy) — upstream app bodies are not user-controlled, so this is information-disclosure-only, not injection.Tests
internal/server: +41 workspace-files tests, all green (full package green) — incl. the upgrade regression (emptyworkspaceFileAccess→403, no upstream write) and the file-root contract cases (MEMORY.md/foorejected on read/write/download).internal/auth: +2 authorizer cases pinning the W8 decision (the write action is deliberately authorizer-allowed cross-team so the handler can hide it as404).go test ./...against the latest main baseline — zero regressions (the only failure is a pre-existing environment issue ininternal/executor: the sandbox image lacks theunzipbinary, which CI has).gofmt/go vetclean;tests/check-agentteams-rename-defaults.shpasses (no legacy brand strings).Related
RoleHuman+accessibleTeams) and worker read authorization404)PUT /api/v1/humans/{name}) carriesworkspaceFileAccessin its updatable field set, so L1 can grant or revoke a user's write access withoutagt apply— that PR rebases on top of this one (it needs the CRD field)workspaceFileAccess, and the follow-up L2 tool-approval endpoint) is tracked in issue Design: L2 team-scoped workspace access (knowledge base files, per-user write opt-in, tool approval) #1217.mcp>=1, <2dependency pin are each their own focused PR (fix(controller): checkpoint proxy scoped check compared member name as team name #1214 / fix(copaw): pin mcp below 2.0.0 in copaw-worker #1215). The pin is still carried by the other in-flight PRs; after it merges,git rebaseauto-drops the duplicate from them.摘要
L2 人类(与团队 leader)目前没有触达本团队知识库的任何路径——读没有,写也没有。能到达 worker 工作区文件的唯一通道是 Controller 的
/docker/代理,而它仅放行 admin/Manager:L2 人类用自己的 Matrix 令牌请求一律403;checkpoint 端点(#1186)暴露的是文件索引与变更摘要,不是知识原文。本 PR 通过 Controller 代理 QwenPaw 工作区文件 API 的四个端点(读 3 + 写 1 + 下载 1,共 5 条路由中的 4 个子路径),沿用 #1186 worker checkpoint 端点已确立的固定子路径代理模式:
GET /api/v1/workers/{name}/workspace-files/tree?path=memory|digest[/...]— 知识目录树分页列表GET /api/v1/workers/{name}/workspace-files/file-metadata?path=...— 单个知识文件的 size/etag/修改时间GET /api/v1/workers/{name}/workspace-files/file-content?path=...[&offset=&limit=]— 有界 UTF-8 分块读取,next_offset续读PUT /api/v1/workers/{name}/workspace-files/file-content?path=...— ETag 保护的保存(新建或更新一个知识文件)GET /api/v1/workers/{name}/workspace-files/file-download?path=...— 有界流式下载(附件头透传)可寻址面是读写同界的路径白名单:
MEMORY.md(长期记忆)、memory/**(日记)、digest/**(沉淀知识)。工作区内其他一切位置——SOUL.md、PROFILE.md、TODO.md、checkpoints/、skills/以及所有 dot 目录(.copaw/agent.json承载 worker 的 Matrix 令牌与存储凭据)——在请求到达 worker 之前即被400拒绝,尽管上游路径解析器对直接请求的 dot 路径是会正常解析的。写权限逐用户、L1 可配、显式 opt-in。 Human CRD 新增
spec.workspaceFileAccess: read | readwrite(缺省为read)。admin/manager(L1)可写任意团队的知识库;L2 人类仅可写自己团队内的 worker,且其 Human CR 显式设为readwrite时——空值/未设置即只读,因此 Controller 升级不会静默授予既有 L2 人类新的写权限;L1 把字段设为readwrite即授予某用户写权限(清空字段或设read即收回)。团队 leader 在本 API 上保持只读。本设计要关掉的并发风险:worker 会自动向自己的记忆文件追加,因此代理对已存在文件强制要求If-Match(跳过 ETag 检查即丢更新),对新建文件禁止携带;上游 ETag 不匹配原样透传409。按 review 要求拆分(独立 PR):checkpoint 代理的 scoped 调用方修复(成员名 vs 团队名比较)与 copaw-worker
mcp>=1, <2依赖 pin 各自独立成 focused PR——本 PR 不再包含,只含 workspace-files 功能。包含内容
agentteams-controller/api/v1beta1/types.go+ CRD manifest:HumanSpec新增workspaceFileAccess(read|readwrite,可选,缺省为read、写权限显式 opt-in)——L1 可逐用户授予/收回的知识库写权限。config/crd/humans.agentteams.io.yaml+helm/agentteams/crds/humans.agentteams.io.yaml同步更新(enum 校验;make check-crd-sync绿)。纯 string 字段由结构体拷贝覆盖,无需重新生成 deepcopy。agentteams-controller/internal/auth/authorizer.go:workspace-files-write。RoleHuman对worker资源:authorizer 层放行——与读路径同款的 W8 理由:中间件会解析 worker 的团队,若在此处拒绝跨团队,403会泄露 worker 存在性(读路径隐藏为404)。handler 才是真边界(404 隐藏 + 逐用户标志 + 白名单)。刻意独立于update动作,与 L2 worker 更新授权的工作互不纠缠。agentteams-controller/internal/server/worker_workspace_files.go(新建):WorkspaceFilesHandler— 固定子路径代理(tree / file-metadata / file-content GET+PUT / file-download)转发至共享 docker 网络内 worker 的 qwenpaw app。仅 embedded 模式;kube 模式在任何 worker 查找之前统一返回503(不产生 404/503 存在性分裂)。validateKbPath— 知识白名单:根名精确匹配(memory、digest、MEMORY.md——memories/、memoryX/不构成memory/前缀)、文件根只能是单个顶层文件(MEMORY.md/foo被拒——MEMORY.md是一个文件不是目录,嵌套文件必须在memory/或digest/下)、禁 dot 段(.copaw/、.git/、.qwenpaw/、.reme_store_v1/全灭)、禁.././绝对路径/反斜杠/NUL、≤4 段、每段 ≤255 字节(对齐上游段长上限)。400,与 checkpoint 代理同款语义);limit边界镜像上游上限(tree 1..500 =MAX_PAGE_SIZE;file-content 1..1048576 =MAX_CHUNK_SIZE);cursor不透明透传;offset≥ 0。root=workspace由服务端固定,不属于客户端查询面——QwenPaw 默认root=project是主绑定项目目录,不是知识库。上游查询仅由校验后的值重建。proxyWorkspaceFileWrite(PUT file-content):写角色边界(admin/manager 任意团队;L2 人类自己团队 + 显式workspaceFileAccess="readwrite"——空值/未设置即只读;团队 leader403;跨团队隐藏为404)→file-metadata存在性探测决定 If-Match 策略(已存在→必填;新建→禁带)→ 1 MiB body 上限 → 非空content(空写会截断 worker 记忆文件)→400/404/409/422原样透传 → 每次成功写一条审计日志(worker、路径、调用者、角色、字节数、新建/更新)。file-download:上游StreamingResponse(256 KiB 分块、Content-Disposition: attachment、Content-Length、ETag)流式透传并转发这些头——客户端按 worker 的文件名保存。service.EffectiveWorkerConsolePort(system-wins env 链——冲突的spec.env端口被丢弃,代理恒指向容器实际监听端口)。agentteams-controller/internal/server/http.go:全 PR +5 行——handler 构造、GET .../workspace-files/{sub}路由、PUT .../workspace-files/file-content路由(RequireAuthz(workspace-files-write, "worker"))。.copaw/agent.json一族 / 穿越拒绝 / 非 KB 拒绝(含前缀混淆名)/ 未知+重复查询拒绝 / 越界值 / 缺 path 拒绝 / 写子路径(file-upload、restore)经 GET 拒绝 / 非法 worker 名 / 未知 worker 404 / L2 in-scope 200(本 PR 目的用例) / L2 跨团队 404 / 团队 leader 跨团队 404 / standalone worker 对 scoped 调用方隐藏、对 admin 可见 / kube 503 先于查找 / 不可达 502 / 上游 404 透传 / 上游 500 有界 / 前缀+端口解析矩阵(8 例)/ 端到端生效前缀+端口拨号)+ 17 个写/下载(无 If-Match 新建 / 带匹配 If-Match 更新 / 已存在文件无 If-Match 在触碰 worker 前被拒 / 新建文件带 If-Match 被拒 / 409 冲突透传 / 跨团队 404 / 只读人类 403 / 空workspaceFileAccess(升级默认)403 / 范围内 leader 403 / admin 200 / 超 1 MiB 在触碰 worker 前被拒 / 写白名单表(含 SOUL.md 与MEMORY.md/foo) / kube 503 / 非写子路径+非法 body 400 / 下载 200 且附件头透传 / 下载跨团队 404 / 下载白名单(admin 同样适用,含MEMORY.md/foo))。docs/usage/project-workflow-api.md(+docs/zh-cn/usage/project-workflow-api.md):"Worker 知识库(工作区文件)端点" 节——端点表(4 端点)、读范围 + 写范围、并发(ETag)契约、写限制、workspaceFileAccess字段、白名单、root 固定、版本门契约(用file-metadata?path=MEMORY.md探测区分"worker 为 QwenPaw < 2.1"与"文件不存在")、错误码表。数据边界与安全
MEMORY.md/memory/**/digest/**)对读写同样适用。SOUL.md属于团队 owner 域,任何人(含 L1)都不能经本 API 写——身份与灵魂边界由路径白名单强制,而不是靠角色矩阵。404——与读同款 W8 规则;authorizer 对此动作刻意不拒跨团队,因为中间件解析了 worker 的团队,此处403会泄露存在性)→ 逐用户workspaceFileAccess(非显式readwrite即403;Human CR 读不到同样403)→ leader403→ If-Match 策略(400)→ 1 MiB / 非空 body(400)→ 白名单(400)。任何单层 fail-closed 都保护着全集群。If-Match),那会静默丢掉 worker 自己在读-写间隙里的追加。代理先探测file-metadata,该情形下强制If-Match——客户端无法降级这个保证。400。不依赖上游自身的路径加固(禁..、禁绝对路径、符号链接受限于工作区、目录列表隐藏 dot 项)——直接请求的 dot 目录在上游是能解析的,这正是 Controller 侧白名单存在的原因,读与下载均有显式测试覆盖。CompositeAuthenticator(feat(controller): add project/workflow query API for human-visible workflows #1169)认证,范围 =accessibleTeams。无 admin 令牌、无新角色。404,原样透传;文档化的file-metadata?path=MEMORY.md探测(每个已初始化工作区必有该文件)让客户端能显示"需升级 worker"而非"文件不存在"。现网 2.0.1 worker 在 2.1+ 迁移(feat(qwenpaw): upgrade QwenPaw Worker runtime to 2.2 #1174 / feat(manager): upgrade Manager runtime to QwenPaw 2.2 #1175)后自动点亮,无需重部署 Controller。404;kube 模式在任何查找前返回503。502回显 4 KiB 有界上游 body(与 checkpoint 代理同模式)——上游 app 的 body 不受用户控制,属信息披露而非注入。测试
internal/server:+41 个 workspace-files 测试,全绿(整包绿)——含升级回归(空workspaceFileAccess→403,不触碰上游写)与文件根契约用例(MEMORY.md/foo在读/写/下载三条路径均被拒)。internal/auth:+2 个 authorizer 用例钉死 W8 定案(写动作在 authorizer 层刻意放行跨团队,由 handler 隐藏为404)。go test ./...— 零回归(唯一失败是internal/executor的既有环境问题:沙箱镜像缺unzip二进制,CI 有)。gofmt/go vet干净;tests/check-agentteams-rename-defaults.sh通过(无遗留品牌字符串)。相关
RoleHuman+accessibleTeams)与 worker 读授权404)PUT /api/v1/humans/{name})把workspaceFileAccess纳入可更新字段集,L1 不用agt apply即可授予/收回某用户的写权限——该 PR 在本 PR 之上 rebase(它依赖本 PR 的 CRD 字段)workspaceFileAccess、后续 L2 工具审批端点)在 issue Design: L2 team-scoped workspace access (knowledge base files, per-user write opt-in, tool approval) #1217 跟踪。mcp>=1, <2依赖 pin(fix(controller): checkpoint proxy scoped check compared member name as team name #1214 / fix(copaw): pin mcp below 2.0.0 in copaw-worker #1215)。pin 仍由其余在飞 PR 携带;其合入后git rebase会在它们身上自动丢弃重复。