Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6924afa
Add chief-of-staff scenario: Chief of Staff Teammate for Node.js
Yogeshp-MSFT Jul 23, 2026
0b08498
Address Copilot review comments (safe fixes only)
Yogeshp-MSFT Jul 23, 2026
8d96da2
Address second round of Copilot review comments (safe fixes only)
Yogeshp-MSFT Jul 23, 2026
3cbf794
fix(chief-of-staff): snapshot READY payload before markCaptureComplet…
Yogeshp-MSFT Jul 29, 2026
1eb0656
fix(chief-of-staff): wire A365 observability so MAC Activity tab popu…
Yogeshp-MSFT Aug 3, 2026
f71e0e6
refactor(chief-of-staff): shared observability module + real UPN in M…
Yogeshp-MSFT Aug 5, 2026
93c9119
refactor(chief-of-staff): sync latest cos-agent — obs polish + card/s…
Yogeshp-MSFT Aug 11, 2026
27be9a4
fix(chief-of-staff): secure direct actions and WorkIQ auth
Yogeshp-MSFT Oct 13, 2026
928b75a
Update scenarios/chief-of-staff/README.md
Yogeshp-MSFT Oct 14, 2026
8f2d131
fix(chief-of-staff): address review feedback (authz, reliability, set…
Yogeshp-MSFT Oct 15, 2026
ea7e17a
fix(chief-of-staff): address second review round (access, delivery, CI)
Yogeshp-MSFT Oct 15, 2026
1a8a319
Update scenarios/chief-of-staff/.env.template
Yogeshp-MSFT Oct 15, 2026
c0998b6
fix(chief-of-staff): address third review round (privacy, idempotency…
Yogeshp-MSFT Oct 15, 2026
4350f05
Merge branch 'main' of microsoft/Agent365-Samples into Chief-of-Staff
Yogeshp-MSFT Oct 16, 2026
0ba559b
fix(chief-of-staff): address fourth review round (capture integrity, …
Yogeshp-MSFT Oct 16, 2026
9066415
fix(chief-of-staff): address fifth review round (plan boundary, captu…
Yogeshp-MSFT Oct 17, 2026
be007e8
fix(chief-of-staff): address sixth review round (summary isolation, d…
Yogeshp-MSFT Oct 21, 2026
d3a2c7f
fix(chief-of-staff): bind MCP writes and address latest review
Yogeshp-MSFT Oct 22, 2026
eb15cbc
Update scenarios/chief-of-staff/package.json
Yogeshp-MSFT Oct 22, 2026
ee188f6
Update scenarios/chief-of-staff/README.md
Yogeshp-MSFT Oct 22, 2026
50a4478
fix(chief-of-staff): preserve resolved due dates and resume capture a…
Yogeshp-MSFT Oct 22, 2026
8d6bd40
fix(chief-of-staff): reconcile extension proposals and escalation state
Yogeshp-MSFT Oct 22, 2026
d04ec88
Update scenarios/chief-of-staff/README.md
Yogeshp-MSFT Oct 23, 2026
fee83ca
Fixed New PR comments as of 23-10-2026
Yogeshp-MSFT Oct 23, 2026
e25f6b6
Default observability exporter to false in .env.template
Yogeshp-MSFT Oct 23, 2026
4a57c75
fix(chief-of-staff): bound Graph calls and preserve safe extension de…
Yogeshp-MSFT Oct 27, 2026
e0a683a
fix(chief-of-staff): upgrade MSAL past authorization flow advisory
Yogeshp-MSFT Oct 27, 2026
b7f4900
fix(chief-of-staff): bound briefs, capture titles, and remaining polls
Yogeshp-MSFT Oct 27, 2026
a56e53c
fix(chief-of-staff): finalize review fixes and bootstrap login recovery
Yogeshp-MSFT Nov 2, 2026
9adbbf1
fix(chief-of-staff): require exact extension decision replies
Yogeshp-MSFT Nov 3, 2026
e392f0b
fix(chief-of-staff): serialize reassignment and compare secret expiry…
Yogeshp-MSFT Nov 3, 2026
634d966
fix(chief-of-staff): include leader calendar exclusions and correct c…
Yogeshp-MSFT Nov 3, 2026
6a6ecb7
fix(chief-of-staff): invalidate obsolete capture artifacts and serial…
Yogeshp-MSFT Nov 3, 2026
1007319
fix(chief-of-staff): guard reinvited captures and extension proposal …
Yogeshp-MSFT Nov 3, 2026
ff7e1cf
fix(chief-of-staff): route deterministic card actions without WorkIQ …
Yogeshp-MSFT Nov 3, 2026
64ec196
fix(chief-of-staff): prefer live calendar occurrence boundaries
Yogeshp-MSFT Nov 4, 2026
b6affe0
Merge branch 'main' into Chief-of-Staff
rbrighenti Nov 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci-orchestrator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ jobs:
name: Observability Offline Tests
uses: ./.github/workflows/ci-observability.yml

# Scenario Jobs
scenario-chief-of-staff:
name: Chief of Staff Scenario
uses: ./.github/workflows/ci-scenario-chief-of-staff.yml

# Final status check - ALWAYS runs and reports success
# This is the ONLY check you need to mark as "Required" in branch protection
ci-status:
Expand All @@ -77,14 +82,15 @@ jobs:
- python-openai
- python-claude
- observability
- scenario-chief-of-staff
if: always()
steps:
- name: Check CI Status
run: |
echo "Checking CI job results..."

# Get all job results (skipped jobs are fine, failed jobs are not)
results="${{ needs.dotnet-agentframework.result }} ${{ needs.dotnet-semantickernel.result }} ${{ needs.nodejs-claude.result }} ${{ needs.nodejs-langchain.result }} ${{ needs.nodejs-openai.result }} ${{ needs.nodejs-vercelsdk.result }} ${{ needs.python-agentframework.result }} ${{ needs.python-googleadk.result }} ${{ needs.python-openai.result }} ${{ needs.python-claude.result }} ${{ needs.observability.result }}"
results="${{ needs.dotnet-agentframework.result }} ${{ needs.dotnet-semantickernel.result }} ${{ needs.nodejs-claude.result }} ${{ needs.nodejs-langchain.result }} ${{ needs.nodejs-openai.result }} ${{ needs.nodejs-vercelsdk.result }} ${{ needs.python-agentframework.result }} ${{ needs.python-googleadk.result }} ${{ needs.python-openai.result }} ${{ needs.python-claude.result }} ${{ needs.observability.result }} ${{ needs.scenario-chief-of-staff.result }}"

echo "Job results: $results"

Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/ci-scenario-chief-of-staff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

name: CI - Build Chief of Staff Scenario

on:
workflow_call: # Called by orchestrator
workflow_dispatch: # Manual trigger

jobs:
scenario-chief-of-staff:
name: Chief of Staff Scenario
permissions:
contents: read
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./scenarios/chief-of-staff

strategy:
matrix:
node-version: ['20', '22']

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: npm install

- name: Build and run regression tests
run: npm test

- name: Run offline bootstrap login tests
shell: pwsh
run: ./tests/bootstrap-graph-app.test.ps1
229 changes: 229 additions & 0 deletions scenarios/chief-of-staff/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# ─── A365 platform / server ───
# NODE_ENV and HOST below are LOCAL-DEV values. Do not copy them to Azure App
# Service: set NODE_ENV=production there (static BEARER_TOKEN_* auth is only
# accepted when NODE_ENV=development), omit HOST (or set HOST=0.0.0.0), and
# let App Service supply PORT.
NODE_ENV=development
HOST=127.0.0.1
PORT=3978
DEBUG=agents:*

# ─── Logging ───
# LOG_LEVEL: error | warn | info | debug | trace (case-insensitive, default=info)
# LOG_HTTP: when 'true', prints every outgoing Graph/HTTP call with status + latency (secrets redacted)
LOG_LEVEL=info
LOG_HTTP=false

# Default-off safety gate. When true, authenticated direct Teams requests may
# invoke action tools (for example, send email or book a meeting). Instructions
# embedded in email, documents, transcripts, web content, or tool results stay untrusted.
ALLOW_DIRECT_USER_ACTIONS=false

# ─── Agent identity (from `a365 develop setup`) ───
agent_id=
# Agentic auth toggle. `true` uses the agentic OBO exchange to get a
# per-server token for each WorkIQ MCP server (production). `false` reads
# per-server dev tokens BEARER_TOKEN_<MCP_SERVER_NAME> (see the MCP section
# below) — local development only: it is refused unless NODE_ENV=development,
# and any BEARER_TOKEN_* left set while this is `true` is rejected as a
# configuration error rather than silently used. Leave `true` for the
# reference deployment.
USE_AGENTIC_AUTH=true
connections__service_connection__settings__clientId=
connections__service_connection__settings__clientSecret=
connections__service_connection__settings__tenantId=
connections__service_connection__settings__scopes=5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default
connectionsMap__0__serviceUrl=*
connectionsMap__0__connection=service_connection
agentic_type=agentic
agentic_connectionName=AgenticAuthConnection
agentic_altBlueprintConnectionName=service_connection
agentic_scopes=https://graph.microsoft.com/.default

# ─── WorkIQ MCP servers ───
# Servers are read from ToolingManifest.json (deploy it next to dist/). Each
# server has its own audience and the Tools.ListInvoke.All scope; the agent
# acquires a separate agentic token per server — no shared MCP token.
#
# Dev only (USE_AGENTIC_AUTH=false and NODE_ENV=development): set one
# token per server, acquired for that server's audience
# (`a365 develop get-token`), e.g.
# BEARER_TOKEN_MCP_MAILTOOLS=
# BEARER_TOKEN_MCP_TEAMSSERVER=
# BEARER_TOKEN_MCP_CALENDARTOOLS=
#
# When `false` (default), a WorkIQ MCP registration failure fails the turn
# with a clear error. Set `true` only in constrained dev tenants where some
# MCP servers are expected to be unavailable — the agent then continues
# without those tools and logs a warning.
SKIP_TOOLING_ON_ERRORS=false

# ─── Foundry — Azure OpenAI (gpt-4o) ───
AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_DEPLOYMENT=gpt-4o
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_API_VERSION=2024-10-21

# ─── Planner (single source of truth) ───
# Two paths — you only need ONE.
#
# Path A (recommended, simplest): leave PLANNER_PLAN_ID + PLANNER_BUCKET_NEW
# blank and let the agent auto-resolve them from LEADERSHIP_TEAM_ID at
# runtime. Requirements:
# - LEADERSHIP_TEAM_ID is set (any format — GUID / display name / channel email)
# - The Team has exactly ONE Planner plan → auto-picked
# (or set PLANNER_PLAN_NAME to disambiguate when the Team has multiple)
# - The plan contains a bucket named "New" (case-insensitive) → auto-picked
# (or set PLANNER_BUCKET_NAME to override the search name)
#
# Path B (explicit): set both IDs directly. From the Planner URL + Graph Explorer:
# URL: …/plan/<PLAN_ID>/view/board
# GET /planner/plans/<PLAN_ID>/buckets → copy the "New" bucket id
PLANNER_PLAN_ID=
PLANNER_BUCKET_NEW=
# Optional overrides for Path A:
PLANNER_PLAN_NAME=
PLANNER_BUCKET_NAME=New

# ─── Leader ───
# LEADER_AAD_ID auto-resolves from LEADER_UPN on first turn — leave blank if you like.
LEADER_UPN=
LEADER_AAD_ID=
# Display name used in card copy (e.g. "Assigned by: Mario"). Optional — falls
# back to "the Leader" when unset.
LEADER_NAME=
# ─── Standalone Graph worker app (REQUIRED) ───
# EVERY Graph API call (Planner, people, calendar, transcripts, insights)
# uses this app's client credentials (application permissions). There is no
# agentic OBO fallback — the agent reports a clear error if these are blank.
# Create them with scripts/bootstrap-graph-app.ps1 (README §3b).
GRAPH_APP_ID=
GRAPH_APP_SECRET=
GRAPH_TENANT_ID=

# ─── Team access control (Recall gate) ───
# The leadership Team. This is the access boundary for Planner/calendar data
# (Teams chat AND email turns). Accepts any of:
# * M365 Group GUID e.g. 44db7598-1234-abcd-… (recommended)
# * Teams channel email e.g. 44db7598.contoso.onmicrosoft.com@amer.teams.ms
# * Team display name e.g. Leadership Operations
# The last two are resolved to the GUID on first turn and cached for the
# process lifetime; if more than one group matches, the agent refuses to
# guess and you must set the GUID. If blank, Recall is open to any sender the
# agent can identify as a tenant user (unidentified email senders are always
# restricted). When set, the gate fails closed: if the Team can't be resolved
# or membership can't be read, non-leader senders are treated as non-members.
LEADERSHIP_TEAM_ID=

# ─── In-process scheduler (cron + pollers) ───
SCHEDULER_ENABLED=true
# Gate for the daily Brief cron. `true` = Brief card fires per CRON_BRIEF.
# `false` = silenced (all other crons still run). Default in code is false.
BRIEF_ENABLED=false
CRON_BRIEF=0 8 * * 1-5
# Follow-up check-ins. Each tick also runs the deterministic stale-follow-up
# escalation sweep (no separate escalate cron).
CRON_FOLLOWUP=0 * * * *
# IANA time zone for CRON_* patterns above. If blank, patterns are interpreted
# in the SERVER's local time — which differs between local dev and Azure App
# Service (UTC). Set explicitly for reproducibility across environments.
# Examples: America/Los_Angeles, Asia/Kolkata, Europe/London, UTC
CRON_TIMEZONE=
# Meeting-capture orchestrator (calendar-driven — leader AND CoS on the invite)
POLL_MEETINGS_MS=60000 # 1 min — cheap; discovery + retry sweep per tick
POLL_TASKS_MS=300000
# Hours to wait for the owner to reply to a follow-up card before escalating
# to the leader. Sweep runs at every follow-up cron tick.
FOLLOWUP_ESCALATE_AFTER_HOURS=3
# Suppress a fresh check-in for the SAME owner within this window (hours)
# after they responded / resolved / had a meeting scheduled. Per-owner —
# reassignment clears the cooldown for the new owner.
FOLLOWUP_COOLDOWN_HOURS=4
# Follow-up cycles to keep retrying an owner's extension/blocker request when
# the leader's card can't be delivered, before telling the owner to contact
# the leader directly.
# LEADER_DELIVERY_MAX_ATTEMPTS=24

# ─── Meeting capture (transcripts + Copilot AI insights) ───
# The CoS agent's inviteable UPN — used to filter calendar events. A meeting
# is captured whenever BOTH the leader and this UPN are on the invite (as
# organizer or attendee), regardless of who organized it. In-progress and
# upcoming meetings are tracked too; capture runs once the transcript exists.
COS_AGENT_UPN=
# Which user's Graph endpoints do we hit? Two modes:
# Both modes also read the leader's calendar for transcript exclusion
# boundaries; this does not change the mailbox used for artifact access.
# cos-agent (default) — we read /users/{COS_AGENT_UPN}/... — Teams
# application-access policy needs to be granted ONLY to the CoS agent
# UPN. Zero per-leader setup; any meeting with both the leader and the
# CoS on the invite gets captured. Works iff attendee-role access is
# sufficient for the transcript/insights endpoints in your tenant.
# leader — we read /users/{LEADER_UPN}/... — policy must be granted to
# each leader (or -Global). Guaranteed to work but per-leader setup.
CAPTURE_GRAPH_OWNER=cos-agent
# How far back to scan the leader's calendar each tick.
TRANSCRIPT_WATCH_HOURS=4
# How far FORWARD to scan the calendar. We include upcoming/in-progress
# meetings so the transcript-fetch retry loop can start early and pick up
# transcripts the moment Teams publishes them — even for meetings the
# leader joined-and-left before their scheduled end.
TRANSCRIPT_WATCH_FORWARD_HOURS=24
# Wait for Copilot AI insights: waitMinutes = clamp(durationMin * mult, min, max)
INSIGHTS_WAIT_MULTIPLIER=0.5
INSIGHTS_MIN_WAIT_MINUTES=3
INSIGHTS_MAX_WAIT_MINUTES=30
# If transcript still isn't there after this many hours since meeting end, drop.
CAPTURE_GIVE_UP_AFTER_HOURS=4
# Attempt count after which we consider transcript-only capture READY (skips
# the polite retry for Copilot insights). Default 2 — fires on second attempt
# if transcript is inlined and insights are still empty. Set to 1 for demo
# tenants where insights never arrive (fires on first attempt).
CAPTURE_MIN_ATTEMPTS_TRANSCRIPT_ONLY=2

# ─── Display formatting ───
# IANA time zone used for wall-clock text in Adaptive Cards (Brief, follow-up,
# extension, escalation, and assignment due dates) and for the blocker
# meeting slots (10:00 / 14:00 / 16:00 in this zone, DST-aware). Defaults to
# Asia/Kolkata for the demo tenant. Set to your leader's home TZ.
BRIEF_DISPLAY_TZ=Asia/Kolkata

# ─── State persistence (single-instance file-backed) ───
# STATE_BACKEND=file — persist to STATE_DIR (default; survives restart)
# STATE_BACKEND=null — in-memory only (lost on restart; use for tests)
STATE_BACKEND=file
# Directory for state files. Local dev default: ./.cos-state
# Azure App Service (single-instance): set to /home/data/cos-state — /home
# is the per-app persistent volume mounted across restarts.
STATE_DIR=./.cos-state
# TTL for finished captures. In-flight (pending/ready) records are always
# kept; complete/gave-up records older than this many days are pruned on
# next hydration to keep the JSON file small.
CAPTURE_STATE_RETENTION_DAYS=30
# TTL for terminal follow-ups (responded/resolved). In-flight (pending/
# escalated) records are always kept. Set well above the cooldown window
# in cos/followup.ts so cooldowns survive restart.
FOLLOWUP_STATE_RETENTION_HOURS=72

# ─── Observability ───
ENABLE_A365_OBSERVABILITY_EXPORTER=false
A365_OBSERVABILITY_LOG_LEVEL=info
# The A365 exporter logger stays silent unless the OTel SDK logger is on too.
# Uncomment both to debug export failures.
# OTEL_LOG_LEVEL=INFO
# When `true`, the observability SDK uses a caller-supplied resolver for
# activity attribution instead of the default. Leave `false` unless you've
# wired a custom resolver. (Note: this key is intentionally mixed-case to
# match the observability SDK.)
Use_Custom_Resolver=false
# Runtime agent identity (the AGENTIC INSTANCE app id, not the blueprint).
# Normally left blank — it is read per turn from activity.recipient.agenticAppId.
# Set it only for a deployment where inbound activities don't carry it; a wrong
# value here makes every span unbindable and MAC Activity stays empty.
Comment thread
Yogeshp-MSFT marked this conversation as resolved.
Resolved
agent365Observability__agentInstanceId=
# NOTE: agent365Observability__agentId holds the BLUEPRINT id and is used only
# as span metadata + for warming the blueprint's observability token.
agent365Observability__agentId=
agent365Observability__agentName=Chief of Staff
agent365Observability__agentDescription=Chief of Staff
agent365Observability__tenantId=
agent365Observability__agentBlueprintId=
12 changes: 12 additions & 0 deletions scenarios/chief-of-staff/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
node_modules/
dist/
.env
.env.bak-*
.env.local
# Chat/session transcripts — capture raw env values including secrets.
chat.json
*.log
.DS_Store
# In-process state (pending captures, follow-ups, conversation refs, planner
# baseline). Regenerated at runtime — never commit.
.cos-state/
Loading
Loading