Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
130 changes: 130 additions & 0 deletions scenarios/career-coach/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# ─────────────────────────────────────────────────────────────────────────────
# Career Coach autopilot — environment template
# Copy this file to .env and fill in the values marked <CHANGE_ME>.
# All non-secret defaults are kept — you can leave those as-is.
# Never commit the filled-in .env.
# ─────────────────────────────────────────────────────────────────────────────

# =============================================================================
# 1. OPENAI CONFIGURATION
# =============================================================================
# Use EITHER standard OpenAI OR Azure OpenAI (not both). Azure OpenAI takes
# precedence if AZURE_OPENAI_API_KEY is set.

# --- Option A: Standard OpenAI ---
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o

# --- Option B: Azure OpenAI (recommended for this project) ---
AZURE_OPENAI_API_KEY=<CHANGE_ME_azure_openai_key>
AZURE_OPENAI_ENDPOINT=https://<CHANGE_ME_resource_name>.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT=gpt-4o
AZURE_OPENAI_API_VERSION=2024-10-21


# =============================================================================
# 2. MCP + OBSERVABILITY
# =============================================================================
BEARER_TOKEN=<CHANGE_ME_bearer_token_optional_for_MCP_tools>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: BEARER_TOKEN placeholder replaces the agentic MCP token, so WorkIQ tools never attach

The template ships BEARER_TOKEN=<CHANGE_ME_bearer_token_optional_for_MCP_tools>. attachWorkIqTools passes process.env.BEARER_TOKEN || '' to addToolServersToAgent, so on every turn this non-empty placeholder is used as the MCP auth token. The agentic user token is only obtained when that argument is empty, so it is never fetched.

The README describes the variable as an optional dev override ('Leave empty normally'), so most people will leave the shipped value alone. Tool-server registration or connection then fails. The error is caught and only logged as a warning, and the Mail and Calendar tools silently never attach. As a result, 'email me my plan' and 'find time for my 1:1' (README §8) don't work. The sibling Node templates ship this key empty.

Evidence
  • .env.template:28: BEARER_TOKEN=<CHANGE_ME_bearer_token_optional_for_MCP_tools>.
  • src/client.ts:413: await toolService.addToolServersToAgent(this.agent, ctx.authorization, this.authHandlerName, ctx.turnContext, process.env.BEARER_TOKEN || '');. A non-empty value is passed through as the token.
  • src/client.ts:414-418: catches the failure, logs [WorkIQ] Failed to register MCP tool servers (continuing without them) and sets this.agent.mcpServers = [].
  • README.md:149: 'Token from a365 develop get-token, only for running MCP tools without the platform token. Leave empty normally. Tokens expire.'
  • AGENT-CODE-WALKTHROUGH.md:224: 'WorkIQ (dev only) | BEARER_TOKEN'.
  • Sibling templates use BEARER_TOKEN=: nodejs/openai/sample-agent/.env.template:21 and nodejs/claude/sample-agent/.env.template:11.

Scenario: copy the template, fill in the values the README calls required, then ask the agent 'email me my plan'. No Mail tool is available, and the only sign of the problem is the WorkIQ warning in the log.

Suggested fix: Ship BEARER_TOKEN= empty, with a comment that it is an optional, dev-only override from a365 develop get-token.

Suggested change
BEARER_TOKEN=<CHANGE_ME_bearer_token_optional_for_MCP_tools>
# Optional, local dev only: token from `a365 develop get-token`. Leave empty to use the agentic token.
BEARER_TOKEN=


# A365 observability exporter (Defender / Purview / Microsoft 365 admin center reporting
# needs true). Set to false only for console-only local runs.
ENABLE_A365_OBSERVABILITY_EXPORTER=true

# Exporter diagnostics: none | info | warn | error (pipe-separated). warn|error surfaces failed exports;
# add info to also log successful export / token activity (useful while verifying telemetry, noisy afterwards).
A365_OBSERVABILITY_LOG_LEVEL=info|warn|error
# OpenTelemetry SDK diagnostics. Uncomment together with the line above to see export results.
# OTEL_LOG_LEVEL=INFO


# =============================================================================
# 3. ENVIRONMENT SETTINGS
# =============================================================================
# NODE_ENV=development
# HOST=127.0.0.1
# PORT defaults to 3978.
# PORT=3978

# Debug namespace. Narrowed from 'agents:*' to cut MSAL/connector noise while
# keeping onTurnError + adapter events visible.
DEBUG=agents:app:*,agents:cloud-adapter:*


# =============================================================================
# 4. SHAREPOINT CONFIGURATION
# =============================================================================
SP_SITE_HOST=<CHANGE_ME_tenant>.sharepoint.com
SP_SITE_PATH=/sites/CareerCoach
SP_LIST_COMPETENCY_FRAMEWORK=CompetencyFramework_v2
SP_LIST_LEARNING_CATALOG=LearningCatalog_v2
SP_LIST_USER_STATE=UserState
SP_LIST_LEARNING_PORTAL_STATUS=LearningPortalStatus
SP_LIST_QUIZ_RESPONSES=QuizResponses


# =============================================================================
# 5. FEATURE 1 — REAL-TIME WEBHOOK (Graph subscription → proactive DM)
# =============================================================================
# Random shared secret. Generate one with:
# [Guid]::NewGuid().ToString() + [Guid]::NewGuid().ToString()
PORTAL_EVENT_SECRET=<CHANGE_ME_random_guid_pair>
Comment on lines +69 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Shipped PORTAL_EVENT_SECRET placeholder is accepted as a valid webhook secret

The template ships PORTAL_EVENT_SECRET=<CHANGE_ME_random_guid_pair>, which is a non-empty value. The webhook (index.ts) and ensureSubscription only fail closed when the secret is empty. A .env copied from the template therefore runs normally with a secret that is published in this repo, and nothing tells the developer it was never changed.

/api/portal-event is registered before authorizeJWT and is exposed through the --allow-anonymous dev tunnel (README §5.1). Anyone who learns the tunnel URL can send the placeholder as X-Portal-Secret. The agent then DMs any employee who has chatted with it, using preface text built from attacker-chosen CourseId/Status values. The response also reveals whether a given AAD object ID has a cached conversation. The same value works as a Graph clientState, so it can be used to trigger notification processing again and again.

README line 164 understates the risk by describing the value as 'Any random string (used for a manual POST test path)'. It is also the only check on Graph notifications. The repo guidance asks to 'Confirm required secrets are validated at startup with fail-fast behavior (no silent defaults for security-critical config)'.

Evidence
  • .env.template:71 contains PORTAL_EVENT_SECRET=<CHANGE_ME_random_guid_pair>. No code in the sample checks for CHANGE_ME; a grep over the career-coach .ts, .js, .json and .md files returns no matches.
  • src/index.ts:82-87: const expected = process.env.PORTAL_EVENT_SECRET || ''; if (!expected) { ... res.status(500) ... }. Only an empty value is rejected.
  • src/index.ts:111-124: compares req.headers['x-portal-secret'] with expected, then calls agentApplication.handleProactivePortalEvent(aad, body) and returns its {ok, reason} as JSON.
  • src/index.ts:95-108: accepts a Graph-shaped body when first.clientState === expected and runs handleGraphNotification.
  • src/subscription-manager.ts:79-87: also rejects only an empty clientState, so the Graph subscription is created with the placeholder.
  • src/agent.ts:788-792: builds preface from payload.CourseId and payload.Status, checking only that they are strings.
  • src/agent.ts:813-816: passes preface to sendPreface, which sends it verbatim with ctx.sendActivity(MessageFactory.text(text)) (agent.ts:46-47).
  • src/agent.ts:779-783: returns No cached conversation for aadObjectId=… for unknown users.
  • README.md:221: devtunnel create career-coach-tunnel --allow-anonymous.
  • PACKET.md:955 (security-compliance guidance): 'no silent defaults for security-critical config'. PACKET.md:1026: 'All samples should fail fast on missing required security configuration'.
  • Sibling samples reject placeholder secrets explicitly, e.g. nodejs/openai/sample-agent/src/observability-token-service.ts:52-58.

Scenario:

  1. A developer fills in the Azure, SharePoint and connection values but leaves line 71 as shipped. Everything works.
  2. An attacker sends POST https://<tunnel>/api/portal-event with header X-Portal-Secret: <CHANGE_ME_random_guid_pair> and body {"UserAADId":"<victim-oid>","CourseId":"<arbitrary text or link>"}.
  3. The victim receives a Teams DM from Career Coach containing that text, and a sync run follows.

Suggested fix: Ship PORTAL_EVENT_SECRET= empty, so the existing empty-secret guards stay closed until a real value is generated. Alternatively, reject placeholder values (for example, anything containing CHANGE_ME) in both index.ts and ensureSubscription. Also update README line 164 to say the value is the Graph clientState as well as the header for manual tests.

Suggested change
# Random shared secret. Generate one with:
# [Guid]::NewGuid().ToString() + [Guid]::NewGuid().ToString()
PORTAL_EVENT_SECRET=<CHANGE_ME_random_guid_pair>
# Random shared secret (required). Used as the Graph subscription clientState and as the
# X-Portal-Secret header for manual tests; the webhook refuses requests while it is empty.
# Generate one with:
# [Guid]::NewGuid().ToString() + [Guid]::NewGuid().ToString()
PORTAL_EVENT_SECRET=


# Public HTTPS URL that Microsoft Graph POSTs change notifications to.
# Point at your dev tunnel + /api/portal-event. Example:
# https://spiffy-dog-abc.inc1.devtunnels.ms/api/portal-event
PORTAL_WEBHOOK_URL=https://<CHANGE_ME_devtunnel>.inc1.devtunnels.ms/api/portal-event


# =============================================================================
# 6. A365 HOSTED-AGENT AUTH (agentic identity, no manual token refresh)
# =============================================================================
USE_AGENTIC_AUTH=true

# Values below come from the Entra ID app registration that A365 provisions for
# your hosted-agent project. Get them from: Azure portal → App registrations
connections__service_connection__settings__clientId=<CHANGE_ME_agent_client_id>
connections__service_connection__settings__clientSecret=<CHANGE_ME_agent_client_secret>
connections__service_connection__settings__tenantId=<CHANGE_ME_tenant_id>

connectionsMap__0__serviceUrl=*
connectionsMap__0__connection=service_connection

agentic_type=agentic
agentic_altBlueprintConnectionName=service_connection
agentic_scopes=https://graph.microsoft.com/.default
agent_id=<CHANGE_ME_agent_client_id>
connections__service_connection__settings__scopes=5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default
agentic_connectionName=AgenticAuthentication


# =============================================================================
# 7. A365 OBSERVABILITY (Agent 365 telemetry — safe to leave duplicated)
# =============================================================================
# agentId = the runtime Agent Identity (the app id of the agent instance, NOT the blueprint id). The
# exporter authenticates as this identity; the live id is read from each turn and this is only the fallback.
# Find it in the "[A365 Observability] App-only export token acquired for agent <id>" startup log line.
agent365Observability__agentId=<CHANGE_ME_agent_identity_app_id>
agent365Observability__agentName=EmployeeCareerCoachIdentity
agent365Observability__agentDescription=EmployeeCareerCoach
agent365Observability__tenantId=<CHANGE_ME_tenant_id>
# agentBlueprintId = the blueprint's app id (a365.generated.config.json -> agentBlueprintId).
agent365Observability__agentBlueprintId=<CHANGE_ME_blueprint_app_id>
agent365Observability__clientId=<CHANGE_ME_agent_client_id>
agent365Observability__clientSecret=<CHANGE_ME_agent_client_secret>


# =============================================================================
# 8. SETUP / SEED SCRIPTS (device-code auth, developer only)
# =============================================================================
# setup:sharepoint / seed:reference / mark:complete sign in with MSAL device-code.
# Defaults: a well-known public Microsoft Graph client + the 'common' tenant.
# Override only if your tenant blocks the default public client.
GRAPH_AUTH_CLIENT_ID=
GRAPH_AUTH_TENANT_ID=
# Default test user the helper scripts (mark:complete / reset:milestones / backup:user)
# act on. Set to your test user's AAD object id + display name.
TEST_USER_AAD_ID=
TEST_USER_NAME=
# Optional override for the seed CSV directory (defaults to ./SharePoint Data).
SP_SEED_CSV_DIR=
40 changes: 40 additions & 0 deletions scenarios/career-coach/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# dependencies + build
node_modules/
dist/
*.tsbuildinfo

# TS compile artefacts inside src (only source .ts should be tracked)
src/**/*.js
src/**/*.d.ts
src/**/*.js.map

# env & secrets — never commit
.env
.env.local
.env.*.local
.mstoken-cache.json

# generated at first `a365` run — contains tenant/agent IDs
a365.config.json
a365.generated.config.json
.a365-workspace-detection.local.json

# runtime state + backups
.proactive-storage.json
.proactive-refs.json
.sp-subscription.json
.email-plans.json
backups/

# dev logs
dev.log
*.log

# packaged Teams app
manifest/manifest.zip

# agent skills installed via `gh skill add microsoft/agent365-skills` — workspace-local
.agents/
.claude/
.github/copilot-instructions.md
.github/copilot-instructions.md.bak
Loading
Loading