CedarCopilot wants to merge 4 commits into staging from worktree-send-it-chat-field-write-permissions
Libra ran this behavior against the change and confirmed the check detects when it breaks.
Changed code: updateConversationFieldsTool.ts.
Libra ran this behavior against the change and confirmed the check detects when it breaks.
Changed code: fetchConversationTool.ts.
Libra ran this behavior against the change and confirmed the check detects when it breaks.
Changed code: surface-context.ts.
Live on prod, 1 of 4 surfaces working, 2 days left
Libra measured 1 of 4 production surfaces on prod. 3 surfaces had no requests at all. Libra checks hourly for 3 days after each deploy.
nextSteps / statusOverview on a deal, and the agent looped about why. Those core fields are stamped as owned by the next-steps and crm-updater agents (stampCoreFieldOwnership). The write guard forbade any owned field to a caller that is not the owner, and the chat agent is never the owner. The error then blamed a "human-only" flag that was never set, fetch-conversation reported the fields as writable, and a partial write returned success: true with the blocked field silently dropped.actingHumanUserId context key, set only at genuine human chat entry points (in-app chat, headless chat, proposal apply). Subagents the chat spawns (orchestrator, dispatch, SDK Task-delegates) do not get it and are classified like any automation. Authority: read-only seats never; deal owner or editor member via canWriteConversation; org admin via authorize() (write on conversation). agentEditsForbidden locks stay respected except for an admin of the conversation's own org. Any lookup failure falls back to no override.loadFieldWriteFacts + classifyFieldWrites). Fetch reports per-field writable / blockedBy (owner, locked, system, background) / ownerAgentName / blockedReason. agentEditsForbidden stays definition-only so automation consumers (next-steps, crm-updater) are unaffected. Writes return blockedFields with reasons on every result, including partial success, and the all-blocked error states the real reason and the way out. The chat error-handling prompt now says never to guess a cause for an unwritten field.Behavior changes to be aware of:
scope (it was already shown as read-only in fetch; nothing in skills, prompts or playbook defaults writes it through this tool).authorize(); deal membership lives in crm_conversation_users and is checked via the existing canWriteConversation predicate (also used by move-event). Folding membership into authorize() is a follow-up.For the reviewer to decide:
actingHumanUserId is the viewed user, so the audit log shows the viewed user, not the staff member.pnpm run autofix:local: everything passes except one apps/mail Jest test, tests/modules/chat-store/chatHistoryList.test.ts ("Yesterday" bucket, date-dependent, no apps/mail changes in this PR). The chain stops there, so the rest was run separately:pnpm --filter @zero/server exec vitest run src/: 1153 files, 14299 tests passed.pnpm --filter @zero/server run types and pnpm deps:check: clean.field-write-access.test.ts, field-write-facts.test.ts), fetch verdicts, field-write-actor, surface and harness contexts, and updateConversationFieldsTool.test.ts including a 20+ case matrix asserting fetch and the write guard return the same verdict for (owned, locked, system, plain) x (chat admin, chat editor, unauthorized, agent owner, agent non-owner, chat-spawned subagent).mastra/prompts/error-handling.ts; not fire-rate evaluated, since it only governs how an unwritten field is reported.| Surface | Requests | Errors | p95 | Users | Verdict |
|---|---|---|---|---|---|
| db.getConnectionRecord | 253 → 290 | 6 → 1 (0.3%) | 31 ms → 30 ms | 0 | Working 290 requests since the deploy with 1 errors (6 errors in the 253-request baseline before it). p95 30 ms, was 31 ms. |
| aws-primary.syncThread | 1 → 0 | 0 → 0 (0%) | 246 ms → not measured | 0 | No traffic No requests recorded since this deploy. |
| cedar.analytics.draft.create | 1 → 0 | 0 → 0 (0%) | 128 ms → not measured | 0 | No traffic No requests recorded since this deploy. |
| execute_tool write-document | 0 → 0 | 0 → 0 (0%) | not measured | 0 | No traffic No requests recorded since this deploy. |