toga-ai 1.0.70 → 1.0.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,10 +42,16 @@ Per run, after acquiring a Graph client-credentials token:
42
42
  - **Fallback:** the comma-separated `[teams] organizer_user_ids` list (used only when no group
43
43
  is configured or the group resolves no members; group-lookup failures are caught so they
44
44
  can't abort the run).
45
- 2. Per organizer: `getAllTranscripts(meetingOrganizerUserId='{guid}', startDateTime, endDateTime)`
45
+ 2. **Per organizer**, `resolveUserId()` ensures a GUID: a value already shaped like a GUID
46
+ (the group path) passes through; a UPN is looked up via `/users/{upn}?$select=id` and
47
+ returns the object-id, or **`null`** if that lookup fails. The loop **fails fast** on null —
48
+ it throws a clear per-organizer error (caught and recorded in `summary['errors']`, the run
49
+ continues for other organizers) rather than sending a UPN to Graph. `listAllTranscripts()`
50
+ also re-asserts the GUID shape before embedding it in the OData literal.
51
+ 3. `getAllTranscripts(meetingOrganizerUserId='{guid}', startDateTime, endDateTime)`
46
52
  over an incremental window (`MAX(dtCreated)` watermark from the ledger, else `now − lookbackDays`),
47
53
  following `@odata.nextLink`.
48
- 3. Per new transcript (dedup by `transcriptIdentifier`): fetch meeting `subject`+`startDateTime`
54
+ 4. Per new transcript (dedup by `transcriptIdentifier`): fetch meeting `subject`+`startDateTime`
49
55
  (cached per meeting), classify, download VTT (`?$format=text/vtt`), `putObject` to S3, insert
50
56
  ledger row.
51
57
 
@@ -91,8 +97,13 @@ process is identical for all.
91
97
  producing the same 400. Lesson: a "fix" that depends on an ungranted permission and silently
92
98
  falls back will look correct in code review but never work in prod. The group-members approach
93
99
  avoids the `/users` endpoint entirely.
94
- - `resolveUserId()` still exists for the `organizer_user_ids` fallback path and returns a GUID
95
- unchanged (regex match); it is bypassed entirely when the group is configured.
100
+ - `resolveUserId()` returns `?string`: a GUID passes through, a UPN is resolved via `/users`,
101
+ and it returns **`null`** when a non-GUID can't be resolved — it never echoes the UPN back.
102
+ The Export loop rejects null and skips that organizer with a clear error, so a UPN can never
103
+ reach Graph. `listAllTranscripts()` re-asserts the GUID shape (`preg_match`) before embedding
104
+ the id in the OData string literal — defense-in-depth so the no-injection invariant can't
105
+ silently regress. Note: the `organizer_user_ids` UPN fallback only actually works in a tenant
106
+ that grants `User.Read.All`; in this tenant it does not, so the group is the only working source.
96
107
  - Graph meeting/transcript ids are base64 and contain `+ / =` — they must be `rawurlencode()`'d
97
108
  at every Graph call site (`getMeeting`, `downloadTranscriptVtt` fallback URL).
98
109
  - `resolveWatermark()` guards against an unparseable stored `dtCreated`: without the guard,
@@ -102,17 +113,25 @@ process is identical for all.
102
113
 
103
114
  ## Diagnosing a 400
104
115
 
105
- A standalone bash diagnostic (`worker2/diagnose-transcripts.sh`) reproduces the Graph call
106
- chain step-by-step (token → group members → `getAllTranscripts`) and prints the **full** Graph
107
- error body that the worker discards (it keeps only `error.message`). It reads creds from the ini
108
- at runtime and prints none. Useful first stop for any future Graph failure here.
116
+ When a 400 recurs, reproduce the Graph call chain **outside** the worker, step-by-step
117
+ (OAuth token → `GET /groups/{id}/members/microsoft.graph.user?$select=id` → `getAllTranscripts`
118
+ with one member's object-id) with curl, reading creds from `Config/production.ini` `[teams]`.
119
+ This surfaces the **full** Graph error body (`error.code` + `innerError`) that the worker
120
+ discards — `curlRequest()` keeps only `error.message`, which is why prod logs showed an opaque
121
+ `UnknownError`. A `400 UnknownError` on a *valid GUID* points to a Teams Application Access
122
+ Policy gap; a `400` only on a UPN means the id was never resolved to a GUID. (A throwaway
123
+ `diagnose-transcripts.sh` was used this way during the fix but is not committed.)
109
124
 
110
125
  ## Change history
111
126
 
127
+ - 2026-06-12 — **Merged (PR #78) and verified in production**: a real `Team/Transcripts/Export`
128
+ run returned 6/6 organizers, 33 found, 33 exported, 0 errors (classified 30 general / 3 Elite).
129
+ Added fail-fast on unresolvable organizers (`resolveUserId` → null, loop throws clear error
130
+ instead of sending a UPN to Graph) and a GUID assertion in `listAllTranscripts`. (ajean)
112
131
  - 2026-06-12 — Resolve organizers from the `organizer_group_id` Entra group (object-ids via
113
132
  GroupMember.Read.All) instead of per-UPN lookup; fixes the recurring HTTP 400 that PR #77's
114
133
  `/users` approach couldn't (no User.Read.All). Hardened watermark parsing, URL-encoding, and
115
- silent catches. Verified live: 6/6 organizers, 32 transcripts. (ajean)
134
+ silent catches. (ajean)
116
135
 
117
136
  ## Related docs
118
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.70",
3
+ "version": "1.0.71",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",