Skip to content

Team-Based Authorization for Teams Meeting Transcript Review & Storage (Arc) – Technical Design Document

Introduction

Purpose: Arc reads Microsoft Teams transcripts on demand and returns a summary in Slack, with access limited to meetings the requester personally attended. This document designs the next step: a team-based authorization model plus a central store where reviewed meeting summaries are kept, so a team (initially CSM) can review its own meetings after the fact and authorized people can ask for aggregate briefings ("brief me on this week").

Tracks Linear AI-55 (supersedes AI-48, closed as duplicate) and AI-83; implemented in appcircleio/arc#53. This document reflects the shipped implementation, which refined a few points from the initial design: an update carries no relates_to link (the briefing joins by company + chronology), the authorization decision is inlined into mcp-records instead of a separate lib/authz.js, there is no records-specific GitHub token, and there is no interim deny-hook. Those refinements are described in the relevant sections below.

Scope: The team-scoped read/write authorization model (raw transcript / team record / aggregate briefing), a new per-team records ACL config, the central GitHub store for team records (meeting summaries and ad-hoc updates), the two new Arc skills (arc-records-save, arc-records-brief), and the new MCP server that enforces the team scope. Out of scope: the Slack to Teams adapter migration (AI-42), auto-enabling transcription (AI-63), and any consolidation of Arc's existing allowlists (USER_ALLOWLIST, MCP_USER_ACL, CHANNEL_ALLOWLIST) - those stay exactly as they are today.


System Overview

High-Level Description: Three capabilities layered on the existing mcp-teams integration:

  1. Save - a team member commits a record to a central GitHub repository under the owning team's folder. A record is either a meeting summary (minutes of meeting / MoM produced from a transcript) or an ad-hoc update (progress on an action item, a fixed open point) that stands on its own. Raw transcripts are never stored; only the reviewed/written record is.
  2. Authorize - every read and write is gated by the requester's team, resolved from a trusted identity Arc derives (not from anything the model or the chat can set). Raw transcripts stay attendee-only; team records are scoped by the per-team read/write ACL.
  3. Brief - an authorized requester asks for a team's briefing; Arc reads that team's records across the last and current week, groups them by company, and synthesizes one digest in which an open point's status reflects any later update.

Architecture

System Architecture: The flow stays inside the Arc runtime (Node.js, single container, Slack socket mode). No Appcircle product service is touched.

Slack DM ──> adapter-slack ──> runQuery (common.js)
                                  │  injects trusted userEmail (SLACK_USERS, by stable Slack ID)
                                  ▼
                  ┌──────────────────────────────────────────┐
                  │ skills: arc-records-save / arc-records-brief   │
                  └──────────────────────────────────────────┘
                       │                         │
                       ▼                         ▼
            mcp-teams (read transcript)   mcp-records (read/write central store)
            attendee-gated                team-gated  ──>  GitHub private repo
                                                            arc-records

Technology Stack:

Layer Technology
Runtime Node.js 22, ES modules (Arc)
Agent loop Claude Agent SDK
MCP servers @modelcontextprotocol/sdk (stdio)
Transcript source Microsoft Graph (delegated, arc@) via mcp-teams
Central store GitHub private repo, accessed via a dedicated MCP server
Identity config/index.js (SLACK_USERS, unchanged)
Records ACL config/index.js (ARC_RECORDS_ACL, per-team read/write)
Authorization mcp-records (team-scope check over ARC_RECORDS_ACL)

External Dependencies:

  • Microsoft Graph - transcript source, unchanged (mcp-teams, delegated arc@ token).
  • GitHub - the central summary repo; commits give free history and traceability.
  • KolayIK - optional future source for team membership; not the source of truth in v1 (see Open Decisions).

Key Components

Records ACL (config)

  • Purpose: Declare, per team, who may read and who may write that team's records. Separate from SLACK_USERS and the existing allowlists, which are untouched.
  • Responsibilities: Hold ARC_RECORDS_ACL, keyed by team, each with a read and a write email list. A member of write is implicitly also a reader (the effective read set is read ∪ write). Initially only csm is populated.
  • Interactions: Read by mcp-records. Unknown team or email resolves to no access (fail-closed).

mcp-teams (existing, unchanged)

  • Purpose: Read raw Teams transcripts.
  • Responsibilities: Keep the current hard gate - a transcript is returned only if the trusted ARC_REQUESTER_EMAIL was the organizer or an invited attendee. Raw transcript access is the most sensitive tier and does not widen to team scope.

mcp-records (new MCP server)

  • Purpose: The only intended path to read or write the central store, with team scoping enforced in code (the gate lives here, not in a separate module).
  • Responsibilities:
  • write-record - commit a record (meeting or update) under <team>/<year>/<week>/, rejecting a team the requester is not in the write set of.
  • list-records / read-record - return only records whose owning team is in the requester's readable set (read ∪ write).
  • Reads ARC_REQUESTER_EMAIL from its subprocess env (same trusted-identity pattern as mcp-teams); empty email fails closed.
  • The team-scope decision is a small local helper, allowed(team, kind), over ARC_RECORDS_ACL (see Records Authorization below).
  • Interactions: Reaches the GitHub private repo through its own GitHub contents-API client. In v1 the repo is under appcircleio, so this client uses Arc's main GITHUB_PAT; when arc-records moves to a separate account (target state) it switches to that account's scoped token, which is the credential boundary.

arc-records-save skill

  • Purpose: Persist a team record, of either kind.
  • Responsibilities:
  • Meeting: reuse the teams-transcript summarization to produce MoM from a transcript.
  • Update: capture an ad-hoc note (progress on an action item, a fixed open point) that stands on its own; the author does not link it to a specific meeting. The company field is what ties it to that customer's other records.
  • Show the draft, and on explicit approval call mcp-records write-record for the author's team. Records the author from the trusted email.

arc-records-brief skill

  • Purpose: Produce a team's briefing, with each open point reflecting its latest status.
  • Responsibilities: Take the team as input. Compute the previous and current week folders, call list-records over that two-week window, read the matching records, and synthesize one digest. Group by company and present two headings - last week and this week (this week only if records exist). Read each company's records in date order, so a later update supersedes an earlier meeting's open point on the same topic: a point open in a meeting reads as resolved when a later update for that company says so. There is no explicit link from an update to a meeting; the association is the shared company plus chronology. The output format is per-team (CSM defined in v1; a generic fallback for others). Authorization is enforced by the server, not by the skill prompt.

Detailed Design

Identity and records ACL

Identity stays exactly as today: the adapter resolves the requester's work email from SLACK_USERS by stable Slack ID, and common.js injects it into each MCP subprocess env. The model cannot set or forge it. SLACK_USERS itself is unchanged - no new fields.

Records authorization lives in a new, separate config keyed by team, each team carrying a read and a write email list:

ARC_RECORDS_ACL: {
  csm: {
    read:  ['osman@appcircle.io'],
    write: ['burako@appcircle.io', 'berk@appcircle.io', 'ozcan@appcircle.io'],
  },
  // other teams added later
}

write grants creating/committing a team's records via arc-records-save. read grants reading and briefing on them via arc-records-brief. A member of write is automatically a reader - the effective read set for a team is read ∪ write, so the write list is not duplicated into read. For CSM that makes the readable set Osman, Burak Ö., Berk, Özcan, and the writable set Burak Ö., Berk, Özcan. There is no "elevated viewer" concept; access is exactly these two lists.

Central store layout

A private GitHub repository, arc-records. Only reviewed records are committed; raw transcripts are not stored. The top level is the owning team, then the year, then the weekly folder, which always exists. A weekly folder holds all record types for that team (meetings and updates), distinguished by front matter type rather than by a sub-path. No index file.

csm/
  2026/
    2026-06-15_2026-06-21/          # weekly folder, Mon-Sun; always present
      2026-06-18-dunya-katilim-onboarding.md     # type: meeting
      2026-06-19-dunya-katilim-runner-fix.md     # type: update (same company, later date)
ai/
  2026/
backend/
  2026/
platform/
  2026/

The weekly folder name is <weekStart>_<weekEnd> (Monday to Sunday), which carries the intent without breaking month/year boundaries (e.g. 2026-06-29_2026-07-05). A record is filed in the weekly folder of its own date, so an update can land in a later week than the meeting it follows up on.

All dates and the week boundary are computed in Türkiye time (Europe/Istanbul). A record's date, the Monday-Sunday folder boundary, and the briefing's "previous/current week" window all use this single timezone so folder placement and brief windows stay consistent regardless of where the request originates.

Each record carries front matter so listing and briefing can filter without parsing prose:

---
type: meeting          # meeting | update
team: csm
company: dunya-katilim # groups the briefing (company-based teams)
subject: Appcircle onboarding - iOS
date: 2026-06-18
attendees: [burako@appcircle.io, ...]   # meetings only
author: burako@appcircle.io             # reviewer/writer, from the trusted email
status: in-progress    # updates only, person-based teams: in-progress | blocker | done
source: teams          # teams | manual
---

The shape is the same for both record kinds, but a few fields depend on the kind and the team's shape:

  • update record: type: update, source: manual, no attendees, no back-reference to a meeting.
  • company-based team (CSM): carries company (the briefing groups by it); no status.
  • person-based team (AI): omits company; each update carries an optional status (in-progress | blocker | done), surfaced per author in the briefing.

Below the front matter is the body: for a meeting, the standard MoM (summary, decisions, action items) produced by teams-transcript; for an update, a short self-contained note of what changed.

Authorization tiers

Tier What Who is authorized Enforced by
Raw transcript VTT with speaker labels Meeting attendees only mcp-teams (unchanged)
Write a record Commit a record (meeting MoM or update) Team write set mcp-records
Read a record Read a committed record Team read ∪ write set mcp-records
Aggregate briefing Last + current week digest, grouped by company, open points reflecting later updates Team read ∪ write set mcp-records + briefing skill

The gate is always evaluated against the trusted env-injected email, never against names or emails appearing in the chat or transcript. On a missing or unknown email, every tier denies (fail-closed), matching the current mcp-teams and MCP_USER_ACL behavior.

Write flow (arc-records-save)

  1. Author asks Arc to save a record - either a meeting they attended or an ad-hoc update.
  2. For a meeting, Arc fetches the transcript (mcp-teams, attendee gate) and produces MoM. For an update, Arc drafts the note from the conversation and sets its company so it files with that customer's other records.
  3. Arc shows the draft and waits for explicit approval (irreversible-write policy).
  4. On approval, write-record checks the author is in the target team's write set, rejects otherwise, builds the front matter (type, company, author from the trusted email, source), and commits the file in the weekly folder of its date. The commit message records who wrote it, from the trusted email.

Saving is manual (author-initiated) in v1. Automatic capture on meeting-end is deferred.

Read / briefing flow (arc-records-brief)

  1. Requester asks for a team's briefing (the team is the input; default window is the previous and current week).
  2. The server confirms the requester is in that team's readable set (read ∪ write); denies otherwise, fail-closed.
  3. list-records returns the team's records in both week folders, under two headings (last week, this week). The grouping axis is team-specific: company-based teams group by company, person-based teams group by author. For a company-based team Arc reads each company's records in date order so a later update supersedes an earlier meeting's open point on the same topic (an open point reads as resolved when a later update for that company says so); an update that clearly follows up on something before the window stands on its own self-contained body (Arc can read-record an older record only if it needs the detail). For a person-based team there is no company or open-point overlay: each person's updates are listed as subject + status.

The output shape is per-team. For CSM (company-based):

CSM briefing

Last week (2026-06-08_2026-06-14)
  Dünya Katılım
    - <meeting / progress highlights>
    - Open: <point still open at week end>

This week (2026-06-15_2026-06-21)
  Dünya Katılım
    - Onboarding (18 Jun): iOS setup done; runner offline blocked build tests
    - Open point "runner connection" -> resolved 19 Jun (update by Berk)
  ACME
    - ...

For AI (person-based) - grouped by author, each item is subject + a status marker (🔄 in-progress, 🚧 blocker, ✅ done):

AI briefing

Last week (2026-06-08_2026-06-14)
  Özcan
    ✅ token split
    🔄 AI brief format
    🚧 gitnexus index OOM (needs >=4GB RAM)
  Nur
    🔄 records ACL wiring

This week (2026-06-15_2026-06-21)
  Özcan
    🔄 AI brief format

Other company-based teams fall back to the CSM layout; other person-based teams fall back to the AI layout.

Security Considerations

  • No raw transcript persistence. CSM summaries may contain customer data; storing only the reviewed MoM bounds the PII surface. Even so, summaries can contain customer names - repo visibility must be private and access reviewed.
  • Single path to the store, phased.
  • Target state: arc-records lives under a separate appcircle-io account (info@appcircle.io). Arc's main GitHub token has no access, so the only way in is the mcp-records credential held in that subprocess env - a credential boundary, not a prompt rule.
  • Interim (v1): the repo lives under appcircleio and mcp-records reaches it with Arc's main GITHUB_PAT, so the credential boundary does not yet hold. mcp-records is the intended path and the only place team scoping is enforced, but a determined agent could route around it with Bash git/gh or a raw GitHub API call carrying the PAT. A PreToolUse deny-hook on the github MCP and Bash was considered and rejected as security theater - a substring/argument match is trivially evaded (a dynamically built URL, raw curl), so it would imply a boundary that isn't there. The interim exposure is therefore accepted risk, bounded by the Slack allowlist (Control 9), until the account split provides the real boundary. ARC.md instructs Arc to use only mcp-records for the store, which steers normal behavior but is not an enforced control.
  • Trusted identity only. Team and attendee checks use the env-injected email; the model never supplies identity. This is the same control that protects mcp-teams today (SECURITY.md Control 13).
  • Fail-closed everywhere. Unknown/missing email denies review, read, and briefing.
  • Attribution. mcp-records records the reviewer (front matter author + commit message) from the trusted env-injected email, following the same trusted-identity pattern as the attribution control (Control 14). It is done inside the server rather than via hook-attribution.js, so no footer is appended into stored record bodies.

Records Authorization (in mcp-records)

Records authorization is a small decision local to mcp-records, reading the new ARC_RECORDS_ACL config. It deliberately does not consolidate or refactor Arc's existing authorization surface (CHANNEL_ALLOWLIST, USER_ALLOWLIST, SLACK_USERS, MCP_USER_ACL) - those keep their current shape and behavior. A broader consolidation may be revisited later but is out of scope here.

Decision

A single helper inside mcp-records, allowed(team, kind), makes the call from the trusted env-injected email (ARC_REQUESTER_EMAIL) the model cannot forge:

  • kind = 'write' - true if the requester is in the team's write set.
  • kind = 'read' - true if the requester is in the team's read ∪ write set (write implies read).

Every check is fail-closed: an unknown email or team yields no access. Config (ARC_RECORDS_ACL) holds the data; mcp-records holds the decision. (An earlier design placed this in a separate lib/authz.js module; it was a thin wrapper with unused exports, so the logic was inlined into mcp-records.)

Enforcement

The decision is enforced at one point: inside mcp-records, from the trusted email in its subprocess env, exactly as mcp-teams enforces its attendee gate today. The skill prompts never make the call. Admission (who may DM/which channels) and capability (which MCP servers attach) continue to be handled by the existing, untouched mechanisms.


Integration and Interfaces

External interfaces: - Microsoft Graph (transcripts) - unchanged. - GitHub contents API (commit/list/read notes) through mcp-records; in v1 it authenticates with Arc's GITHUB_PAT (the repo is under appcircleio).

Internal interfaces: - mcp-records tools: write-record, list-records, read-record. - Skills arc-records-save and arc-records-brief registered in src/common.js skills: [...]. - ARC_RECORDS_ACL config, consumed by the allowed() check inside mcp-records.


Deployment and Infrastructure

Cloud Deployment

No Appcircle cluster or Helm change. New config only: the ARC_RECORDS_ACL config. No new secret in v1 - mcp-records reuses Arc's existing GITHUB_PAT (the repo is under appcircleio); a scoped token is introduced only when arc-records moves to its own account. The new MCP server is registered inline in src/common.js like the other stdio servers.

Prerequisites and order. Create the private arc-records repo (under appcircleio for v1) first. Suggested build order: ARC_RECORDS_ACL -> mcp-records (with the inline allowed() check) -> arc-records-save -> arc-records-brief.

Self-Hosted Deployment

Not applicable - Arc internal tooling, no Appcircle product surface.

This does not affect Appcircle customer behavior, so no product documentation issue is required. An internal Arc README/skill note is enough.


Testing and Quality Assurance

Test Strategy: - Authorization (the core risk): - A CSM write member (e.g. Burak Ö.) can review and commit, and can also read/brief (write implies read). - A CSM read-only member (Osman) can read and brief but cannot commit a record. - A saved record lands at the correct <team>/<year>/<week>/... path (weekly folder of its own date) with correct front matter (type, company, author, source). - A user in neither the read nor write set of any team cannot list, read, or brief. - Unknown/missing email denies at every tier. - A user attempting to write to a team where they are only in read (or in neither set) is rejected. - Bypass (interim limitation): until the account split, the generic github server and Bash git/gh can reach arc-records with Arc's GITHUB_PAT; team scoping is only enforced when access goes through mcp-records. This is accepted v1 risk (bounded by the Slack allowlist), closed by the credential boundary in the target state. - Record types & linking: a meeting and an update co-exist in one weekly folder, distinguished by type; an update for the same company as an earlier-week meeting is still associated by company + date order in the brief. - Briefing correctness: the digest covers exactly the readable records across the previous + current week, grouped by company under the two headings; reading a company's records in date order, an open point with a later resolving update reads as resolved.

Quality Assurance: Code review against appcircle-code-standard; the new MCP server and skills follow the conventions in CLAUDE.md (stdio server pattern, skill registration, fail-closed auth).


Performance Considerations

Requirements: Read/briefing latency is bounded by a small number of GitHub contents API calls per week of notes; well within interactive Slack response times.

Optimizations: None needed in v1. A briefing reads at most two weekly folders (previous + current), so the weekly-folder layout bounds the read regardless of total record volume.


Maintenance and Support

Monitoring and Logging: Reuses Arc's existing structured stdout audit log and Langfuse tracing. Each write and read records the trusted requester email and resolved team.

Support and Maintenance: ARC_RECORDS_ACL lives in config, reviewed on hire/move. When arc-records moves to the separate appcircle-io account, mcp-records switches to that account's scoped token (the credential boundary) and GITHUB_PAT loses access.


Risks and Mitigation Strategies

Risk Impact Mitigation
Notes repo reachable via a general-purpose path (generic github MCP, Arc's Bash git clone/gh, or a raw GitHub API call), bypassing mcp-records team scoping A chat user reads another team's summaries by asking Arc to clone the repo Target (durable): credential boundary - arc-records under the separate appcircle-io account, unreachable by Arc's main token. Interim (v1, repo under appcircleio): no hard boundary; accepted risk bounded by the Slack allowlist (Control 9). A deny-hook was rejected as trivially evadable security theater.
Customer PII in committed summaries Data exposure if repo access widens Private repo, store only reviewed MoM (never raw VTT), periodic access review; retention is indefinite in v1, revisited if data volume/sensitivity warrants
ARC_RECORDS_ACL drifts from reality (joiners/leavers) Wrong person authorized/denied Single source in config, reviewed on hire/move; revisit KolayIK as source of truth later
Reviewer commits a summary for the wrong/unattended meeting Incorrect record Review derives from mcp-teams (attendee-gated); reviewer recorded from trusted email
Cross-team attendees Ambiguous owning team Owning team is the reviewer's team at write time; one note has exactly one owning team

Appendix

Open Decisions

  1. Retention policy - decided: indefinite in v1. Records are kept indefinitely; a time-boxed policy is revisited later if data volume or sensitivity warrants it.
  2. Notes repo owner - v1 ships arc-records under appcircleio with direct Arc access (via GITHUB_PAT) and no hard store boundary (accepted risk). Planned follow-up: move it to the appcircle-io account (info@appcircle.io) for a true credential boundary (no hard date).
  3. Team source of truth - config now; promote to KolayIK units later? KolayIK list-people returns title but not the PL/BE/FE/QA/CSM/AI taxonomy, so config is the pragmatic v1.
  4. More teams - v1 populates only csm in ARC_RECORDS_ACL and the store. Other teams (ai, backend, platform, ...) are added as top-level folders + ACL entries when needed; each may define its own brief format.