@knowttl/mg-axi 0.1.0

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.
Files changed (57) hide show
  1. package/README.md +1138 -0
  2. package/dist/api.js +953 -0
  3. package/dist/app-auth.js +47 -0
  4. package/dist/auth.js +59 -0
  5. package/dist/bin/mg-axi.js +7 -0
  6. package/dist/catalogue.js +2191 -0
  7. package/dist/cli.js +1314 -0
  8. package/dist/docs.js +559 -0
  9. package/dist/doctor.js +75 -0
  10. package/dist/entra-access-reviews.js +425 -0
  11. package/dist/entra-apps.js +374 -0
  12. package/dist/entra-audit-logs.js +381 -0
  13. package/dist/entra-auth-methods.js +296 -0
  14. package/dist/entra-ca-policy-update.js +353 -0
  15. package/dist/entra-certificate-auth.js +384 -0
  16. package/dist/entra-conditional-access.js +317 -0
  17. package/dist/entra-contacts.js +827 -0
  18. package/dist/entra-contracts.js +252 -0
  19. package/dist/entra-custom-security-attributes.js +464 -0
  20. package/dist/entra-data-policy-operations.js +274 -0
  21. package/dist/entra-delegated-admin.js +468 -0
  22. package/dist/entra-deleted-items.js +461 -0
  23. package/dist/entra-directory-objects.js +274 -0
  24. package/dist/entra-directory.js +411 -0
  25. package/dist/entra-domains.js +399 -0
  26. package/dist/entra-entitlement-management.js +645 -0
  27. package/dist/entra-federation-configurations.js +290 -0
  28. package/dist/entra-grants.js +272 -0
  29. package/dist/entra-group-lifecycle.js +349 -0
  30. package/dist/entra-group-member-add.js +278 -0
  31. package/dist/entra-groups.js +336 -0
  32. package/dist/entra-identity-providers.js +297 -0
  33. package/dist/entra-lifecycle-workflows.js +673 -0
  34. package/dist/entra-multi-tenant-organization.js +309 -0
  35. package/dist/entra-on-premises-synchronization.js +231 -0
  36. package/dist/entra-organization.js +398 -0
  37. package/dist/entra-risk-dismiss.js +191 -0
  38. package/dist/entra-risk.js +404 -0
  39. package/dist/entra-roles.js +310 -0
  40. package/dist/entra-subscriptions.js +289 -0
  41. package/dist/entra-tenant-information.js +152 -0
  42. package/dist/entra-terms-of-use.js +380 -0
  43. package/dist/entra-user-revoke-sessions.js +170 -0
  44. package/dist/entra-user-update.js +170 -0
  45. package/dist/entra-users.js +211 -0
  46. package/dist/graph-session.js +1034 -0
  47. package/dist/msal-app-provider.js +42 -0
  48. package/dist/msal-provider.js +82 -0
  49. package/dist/mutations.js +529 -0
  50. package/dist/profiles.js +139 -0
  51. package/dist/setup.js +116 -0
  52. package/dist/version.js +2 -0
  53. package/docs/coverage.md +1028 -0
  54. package/inventory/operations.json +4 -0
  55. package/inventory/schema.json +71 -0
  56. package/package.json +32 -0
  57. package/skills/mg-axi/SKILL.md +293 -0
package/dist/docs.js ADDED
@@ -0,0 +1,559 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { DESCRIPTION, HOME_HELP, LEAVES } from "./catalogue.js";
3
+ // PACK-01: single source for generated release records. The committed
4
+ // skills/mg-axi/SKILL.md and docs/coverage.md are generated from the templates
5
+ // below, the executable catalogue and the discovery inventory, so help, skill
6
+ // and coverage cannot drift apart. tools/generate-docs.mjs writes both
7
+ // files; test/pack.test.mjs fails when they are stale.
8
+ //
9
+ // The skill file is generated in full by skillDocument() below: its trigger
10
+ // description, its next-step hints (the same hints the no-args home view
11
+ // prints) and its command table all come from the catalogue, so a new leaf
12
+ // without an area label, or a hand-edit to the committed file, fails the
13
+ // --check step instead of drifting silently.
14
+ // One row per executable leaf in catalogue order.
15
+ // Every leaf is implemented by an mg-axi handler (native). Graph
16
+ // leaves carry the read effect, except reviewed named writes.
17
+ function leafEffect(leaf) {
18
+ if (leaf.operation !== undefined)
19
+ return leaf.operation.startsWith("GET:") ? "read" : "write";
20
+ return leaf.path === "api get" || leaf.path === "doctor" ? "read" : "local";
21
+ }
22
+ export function skillCommandTable() {
23
+ return [
24
+ "| Command | Capability | Effect |",
25
+ "|---|---|---|",
26
+ ...LEAVES.map(leaf => `| \`mg-axi ${leaf.path}\` | native | ${leafEffect(leaf)} |`),
27
+ ].join("\n");
28
+ }
29
+ // SKILL-01: the installable skill is generated in full from the catalogue.
30
+ // Area labels for the trigger description, keyed by leaf-path prefix with
31
+ // specific prefixes before general ones. The first matching prefix owns each
32
+ // read leaf: a new read leaf without coverage here throws, so the
33
+ // committed description can never silently omit shipped coverage.
34
+ const READ_AREA_LABELS = [
35
+ ["entra user authentication-method ", "authentication methods"],
36
+ ["entra registration ", "authentication methods"],
37
+ ["entra user ", "users"],
38
+ ["entra group ", "groups"],
39
+ ["entra directory-role ", "roles"],
40
+ ["entra role-assignment ", "roles"],
41
+ ["entra pim ", "roles"],
42
+ ["entra device ", "devices"],
43
+ ["entra administrative-unit ", "administrative units"],
44
+ ["entra organization ", "organization and branding"],
45
+ ["entra certificate-auth-pki ", "certificate authentication"],
46
+ ["entra certificate-authority ", "certificate authentication"],
47
+ ["entra subscription ", "commercial subscriptions"],
48
+ ["entra on-premises-synchronization ", "on-premises synchronization"],
49
+ ["entra agreement ", "terms-of-use agreements and acceptances"],
50
+ ["entra agreement-acceptance ", "terms-of-use agreements and acceptances"],
51
+ ["entra directory-object ", "directory objects"],
52
+ ["entra deleted-user ", "deleted directory items"],
53
+ ["entra deleted-group ", "deleted directory items"],
54
+ ["entra deleted-application ", "deleted directory items"],
55
+ ["entra deleted-service-principal ", "deleted directory items"],
56
+ ["entra deleted-administrative-unit ", "deleted directory items"],
57
+ ["entra deleted-item ", "deleted directory items"],
58
+ ["entra contact ", "organizational contacts"],
59
+ ["entra domain ", "domains and DNS records"],
60
+ ["entra domain-dns-record ", "domains and DNS records"],
61
+ ["entra group-lifecycle-policy ", "group lifecycle policies"],
62
+ ["entra group-setting-template ", "group setting templates"],
63
+ ["entra attribute-set ", "attribute sets and custom security attributes"],
64
+ ["entra custom-security-attribute-definition ", "attribute sets and custom security attributes"],
65
+ ["entra allowed-value ", "attribute sets and custom security attributes"],
66
+ ["entra contract ", "partner contracts"],
67
+ ["entra delegated-admin-customer ", "delegated administration"],
68
+ ["entra delegated-admin-relationship ", "delegated administration"],
69
+ ["entra multi-tenant-organization ", "multi-tenant organization"],
70
+ ["entra tenant-information ", "tenant information"],
71
+ ["entra sign-in ", "sign-ins and audit logs"],
72
+ ["entra directory-audit ", "sign-ins and audit logs"],
73
+ ["entra application ", "applications and consent grants"],
74
+ ["entra service-principal ", "applications and consent grants"],
75
+ ["entra risky-user ", "risk"],
76
+ ["entra risk-detection ", "risk"],
77
+ ["entra conditional-access ", "Conditional Access"],
78
+ ["entra identity-provider ", "identity providers"],
79
+ ["entra federation-configuration ", "federation configurations"],
80
+ ["entra data-policy-operation ", "data policy operations"],
81
+ ["entra access-review ", "access reviews"],
82
+ ["entra entitlement catalog ", "entitlement catalogs and access packages"],
83
+ ["entra entitlement access-package ", "entitlement catalogs and access packages"],
84
+ ["entra entitlement assignment ", "entitlement assignments and requests"],
85
+ ["entra entitlement assignment-request ", "entitlement assignments and requests"],
86
+ ["entra entitlement assignment-policy ", "entitlement catalogs and access packages"],
87
+ ["entra entitlement resource-role-scope ", "entitlement catalogs and access packages"],
88
+ ["entra lifecycle ", "lifecycle workflows, templates, task definitions, settings, runs, processing results and task reports"],
89
+ ["api get", "reviewed raw reads"],
90
+ ];
91
+ // Short outcome labels for the shipped gated writes, keyed by leaf path.
92
+ // A new write leaf without an entry here throws for the same reason.
93
+ const WRITE_LABELS = {
94
+ "entra user update": "account enable/disable",
95
+ "entra user revoke-sessions": "session revocation",
96
+ "entra group member add": "group-membership add",
97
+ "entra risky-user dismiss": "risky-user dismissal",
98
+ "entra conditional-access policy update": "CA policy update",
99
+ };
100
+ function skillReadAreas() {
101
+ const areas = [];
102
+ for (const leaf of LEAVES) {
103
+ if (leafEffect(leaf) !== "read" || leaf.path === "doctor")
104
+ continue;
105
+ const match = READ_AREA_LABELS.find(([prefix]) => leaf.path.startsWith(prefix) || leaf.path === prefix.trim());
106
+ if (!match)
107
+ throw new Error(`SKILL.md has no area label for read leaf: ${leaf.path}`);
108
+ if (!areas.includes(match[1]))
109
+ areas.push(match[1]);
110
+ }
111
+ return areas;
112
+ }
113
+ function skillWriteLabels() {
114
+ return LEAVES.filter(leaf => leafEffect(leaf) === "write").map(leaf => {
115
+ const label = WRITE_LABELS[leaf.path];
116
+ if (!label)
117
+ throw new Error(`SKILL.md has no write label for write leaf: ${leaf.path}`);
118
+ return label;
119
+ });
120
+ }
121
+ // Terse, outcome-focused trigger description: current read coverage plus
122
+ // the shipped gated writes. Generated so EXT reads and new writes land in
123
+ // the frontmatter with the slice that ships them.
124
+ export function skillDescription() {
125
+ const areas = skillReadAreas();
126
+ const writes = skillWriteLabels();
127
+ return `Inspect Microsoft Entra ${areas.join(", ")} and run ${writes.length} gated writes (${writes.join(", ")}) through token-efficient TOON output.`;
128
+ }
129
+ // Next-step hints printed by the no-args home view. The two leading entries mirror the
130
+ // profile/login hints localHome() in cli.ts prepends to HOME_HELP; the
131
+ // rest is HOME_HELP verbatim, so skill and home view cannot drift apart.
132
+ export function skillHomeHints() {
133
+ return ["npx -y @knowttl/mg-axi profile list", "npx -y @knowttl/mg-axi login --help", ...HOME_HELP];
134
+ }
135
+ // The full installable skill. Durable guidance only: read-only by default,
136
+ // writes need preview plus typed confirmation, no secrets in output, the
137
+ // profile/auth model, offline tests. Long walkthroughs live in leaf --help
138
+ // and docs/ instead.
139
+ export function skillDocument() {
140
+ return [
141
+ "---",
142
+ "name: mg-axi",
143
+ "description: >",
144
+ ` ${skillDescription()}`,
145
+ "user-invocable: false",
146
+ "---",
147
+ "",
148
+ "<!-- Generated by tools/generate-docs.mjs from src/catalogue.ts. Do not hand-edit. -->",
149
+ "",
150
+ "# mg-axi",
151
+ "",
152
+ `Agent-ergonomic CLI for Microsoft Graph. ${DESCRIPTION}.`,
153
+ "Entra SOC reads through token-efficient TOON output.",
154
+ "The generated [command table](#orientation) lists supported named writes through the gated mutation coordinator; raw API remains read-only and every other mutation is refused.",
155
+ "See [README.md](../../README.md) for write usage, execution gates, identity pinning, permissions, target-role hierarchy and write configuration.",
156
+ "",
157
+ "Run commands non-interactively as `npx -y @knowttl/mg-axi ...`: no global install needed and no interactive prompts.",
158
+ "Version pinning is the installer's choice: use `npx -y @knowttl/mg-axi@<version> ...` to select a specific release.",
159
+ "Run `npx -y @knowttl/mg-axi doctor` first.",
160
+ "See [README.md](../../README.md#release) for doctor profile selection, checks and failure behavior.",
161
+ "",
162
+ "## Orientation",
163
+ "",
164
+ "The exact current leaf registry is `src/catalogue.ts`.",
165
+ "Its capability label is `native` (implemented by an mg-axi handler).",
166
+ "Its effect is `read` for Graph read leaves and the doctor health check, `write` for the gated named writes, or `local` for home, profile, login and setup views.",
167
+ "Doctor acquires credentials silently and contacts Graph; only its help view stays offline.",
168
+ "The list below records current executable leaves; it makes no coverage claim for other Graph operations.",
169
+ "See `docs/coverage.md` for the per-operation disposition records.",
170
+ "`docs/coverage.md` and this file are generated from the same catalogue and inventory sources; `node tools/generate-docs.mjs --check` fails when they are stale.",
171
+ "",
172
+ "<!-- command-registry:start -->",
173
+ skillCommandTable(),
174
+ "<!-- command-registry:end -->",
175
+ "",
176
+ "Run `npx -y @knowttl/mg-axi <leaf-path> --help` for that leaf's accepted flags and reference.",
177
+ "Unknown flags fail before any credential or HTTP work.",
178
+ "",
179
+ "## Next steps",
180
+ "",
181
+ "The no-args home view prints these hints alongside live profile status:",
182
+ "",
183
+ "```sh",
184
+ ...skillHomeHints(),
185
+ "```",
186
+ "",
187
+ "## Setup (explicit only)",
188
+ "",
189
+ "Follow [release guidance](../../README.md#release) for npm installation and explicit skill installation; [README.md](../../README.md#mg-axi) also documents checkout builds.",
190
+ "Create profiles explicitly and keep secrets out of argv and config files:",
191
+ "",
192
+ "```sh",
193
+ "npx -y @knowttl/mg-axi setup # build steps, config path and capabilities; writes nothing",
194
+ "npx -y @knowttl/mg-axi profile create --name soc --tenant <tenant-id> --client <client-id> --cloud commercial",
195
+ "npx -y @knowttl/mg-axi login --profile soc --scopes https://graph.microsoft.com/User.Read.All",
196
+ "npx -y @knowttl/mg-axi doctor # one bounded read per selected profile",
197
+ "npx -y @knowttl/mg-axi entra user list --profile soc --limit 10",
198
+ "```",
199
+ "",
200
+ "Configuration defaults to `~/.mg-axi/config.json`; `MG_AXI_CONFIG` selects a separate file.",
201
+ "`mg-axi setup` shows the selected path and writes nothing.",
202
+ "",
203
+ "## Selecting a profile",
204
+ "",
205
+ "Read leaves accept `--profile <name>`.",
206
+ "Selection uses `--profile` or the configured default.",
207
+ "Without either, read leaves report `AUTH_REQUIRED`, even if only one profile exists.",
208
+ "Doctor selection is documented in [README.md](../../README.md#release).",
209
+ "Without profiles, home, setup and profile list show unconfigured state; Graph read leaves report `AUTH_REQUIRED`.",
210
+ "",
211
+ "## Safety",
212
+ "",
213
+ "Reads run through a session that authorizes only catalogued operations before silent credential acquisition; only explicit `login` opens a browser or prints a device-code challenge.",
214
+ "Writes preview before sending and need `--execute` plus a typed `--confirm` repeating the target; an already-desired value is a no-op with exit 0, never an error.",
215
+ "Output is TOON on stdout; diagnostics use stderr.",
216
+ "Text truncates at 500 characters (`--full` restores text, never redaction); row caps resume through opaque cursors.",
217
+ "Secrets stay in protected storage references, never in argv, config files or output; credential fields carry expiry metadata only.",
218
+ "Tests run offline with fixture credentials; never point tests at a real tenant.",
219
+ "",
220
+ ].join("\n");
221
+ }
222
+ // Initial write families resolve to shipped when the catalogue carries a
223
+ // leaf for one of their v1.0 operations, and stay pending while every row
224
+ // remains unimplemented. Family membership comes from the inventory
225
+ // owningSlice and shipped status comes from the catalogue, so a merged
226
+ // write slice flips its own row without touching this template. Blocked,
227
+ // deprecated and unavailable variants never count as pending.
228
+ function initialWriteStatus(rows) {
229
+ const families = new Map();
230
+ for (const row of rows) {
231
+ const operation = /^v1\.0:(.+)$/.exec(row.id)?.[1];
232
+ if (operation === undefined || row.owningSlice === null || !/^WRITE-0[1-9]$/.test(row.owningSlice))
233
+ continue;
234
+ if (row.disposition === "intentionally-blocked" || row.disposition === "deprecated" || row.disposition === "unavailable")
235
+ continue;
236
+ families.set(row.owningSlice, [...(families.get(row.owningSlice) ?? []), operation]);
237
+ }
238
+ const shipped = [];
239
+ const pending = [];
240
+ for (const [slice, operations] of [...families.entries()].sort()) {
241
+ const leaf = LEAVES.find(candidate => candidate.operation !== undefined && operations.includes(candidate.operation));
242
+ if (leaf)
243
+ shipped.push(`${slice} (\`mg-axi ${leaf.path}\`)`);
244
+ else
245
+ pending.push(`${slice} (${operations.map(operation => `\`${operation}\``).join(", ")})`);
246
+ }
247
+ return { shipped, pending };
248
+ }
249
+ const DISPOSITIONS = ["named-command", "reviewed-raw-read", "scheduled", "intentionally-blocked", "deprecated", "unavailable", "excluded"];
250
+ // The capability report is generated from the catalogue: every executable
251
+ // read leaf owns one row with its inventory disposition and owning slice,
252
+ // and the counts separate implemented leaves from inventory dispositions.
253
+ // Scheduled work never counts as complete and excluded rows stay separate
254
+ // from scoped capabilities, per docs/inventory.md. There is no completeness
255
+ // percentage: the report names the supported Entra read surface, not full
256
+ // Entra coverage.
257
+ export function capabilityDocument() {
258
+ const inventory = JSON.parse(readFileSync(new URL("../inventory/operations.json", import.meta.url), "utf8"));
259
+ const rows = inventory.operations;
260
+ const count = (disposition) => rows.filter(row => row.disposition === disposition).length;
261
+ const writeStatus = initialWriteStatus(rows);
262
+ const readLeaves = LEAVES.filter(leaf => leaf.operation !== undefined && leaf.operation.startsWith("GET:"));
263
+ const writeLeaves = LEAVES.filter(leaf => leaf.operation !== undefined && !leaf.operation.startsWith("GET:"));
264
+ const commandRows = readLeaves.map(leaf => {
265
+ const row = rows.find(candidate => candidate.id === `v1.0:${leaf.operation}`);
266
+ return `| \`mg-axi ${leaf.path}\` | \`${leaf.operation}\` | ${row ? row.disposition : "no v1.0 inventory row"} | ${row?.owningSlice ?? "-"} |`;
267
+ });
268
+ const writeRows = writeLeaves.map(leaf => {
269
+ const row = rows.find(candidate => candidate.id === `v1.0:${leaf.operation}`);
270
+ return `| \`mg-axi ${leaf.path}\` | \`${leaf.operation}\` | ${row?.owningSlice ?? "-"} |`;
271
+ });
272
+ const deferredDomainRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate R1 "));
273
+ const deferredProviderRows = rows.filter(row => row.owningSlice === "EXT-03" && row.reason.startsWith("Deferred to a later EXT-03 "));
274
+ const deferredFederationRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate federation-configuration scope "));
275
+ const deferredDataPolicyRows = rows.filter(row => row.owningSlice === "EXT-03" && row.reason.startsWith("Deferred by firstmate data-policy-operations scope "));
276
+ const unavailableInvitationRows = rows.filter(row => row.owningSlice === "EXT-03" && row.reason.startsWith("Marked unavailable by firstmate EXT-03b decision: "));
277
+ const deferredOrganizationRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate organization scope "));
278
+ const unavailableCertAuthRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Marked unavailable by firstmate mg-ext-01d decision: "));
279
+ const deferredOnPremSyncRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate on-premises-synchronization scope "));
280
+ const deferredTermsOfUseRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate terms-of-use scope "));
281
+ const deferredDirectoryObjectRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate directory-objects scope "));
282
+ const deferredDeletedItemsRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate deleted-items scope"));
283
+ const deferredContactRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate contacts scope "));
284
+ const unavailableContactRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Marked unavailable by firstmate mg-ext-01m decision: "));
285
+ const deferredContractRows = rows.filter(row => row.owningSlice === "EXT-04" && row.reason.startsWith("Deferred by firstmate contracts scope "));
286
+ const deferredDelegatedAdminRows = rows.filter(row => row.owningSlice === "EXT-04" && row.reason.startsWith("Deferred by firstmate delegated-admin scope "));
287
+ const deferredMtoRows = rows.filter(row => row.owningSlice === "EXT-04" && row.reason.startsWith("Deferred by firstmate multi-tenant-organization scope "));
288
+ const deferredGroupLifecycleRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate group-lifecycle scope "));
289
+ const deferredCustomSecurityRows = rows.filter(row => row.owningSlice === "EXT-01" && row.reason.startsWith("Deferred by firstmate custom-security-attributes scope "));
290
+ const deferredAccessReviewRows = rows.filter(row => row.owningSlice === "EXT-02c");
291
+ const deferredEntitlementRows = rows.filter(row => row.owningSlice === "EXT-02" && row.reason.startsWith("Deferred by firstmate entitlement-management scope "));
292
+ const readCount = readLeaves.length + 2;
293
+ const localCount = LEAVES.length - readCount - writeLeaves.length;
294
+ return [
295
+ "<!-- Generated by tools/generate-docs.mjs from src/catalogue.ts and inventory/operations.json. Do not hand-edit. -->",
296
+ "",
297
+ "# mg-axi coverage",
298
+ "",
299
+ "This is the supported Entra read surface, not full Entra coverage.",
300
+ "Only the commands below have an implemented, tested leaf; every other",
301
+ "operation remains scheduled, blocked, deprecated or excluded until its own slice ships.",
302
+ "Inventory dispositions are discovery-time records from INV-01; the command",
303
+ "list below is the implemented truth and never counts scheduled work as complete.",
304
+ "",
305
+ "## Counts",
306
+ "",
307
+ `- implemented read leaves: ${readCount} (${readLeaves.length} named Entra reads plus reviewed raw api get and doctor health check)`,
308
+ `- implemented write leaves: ${writeLeaves.length} (named gated mutations below)`,
309
+ `- local leaves: ${localCount} (home, profile, login and setup views)`,
310
+ ...DISPOSITIONS.map(disposition => `- inventory ${disposition}: ${count(disposition)}`),
311
+ "",
312
+ "## Commands",
313
+ "",
314
+ "| Command | Operation | Inventory (v1.0) | Owning slice |",
315
+ "|---|---|---|---|",
316
+ ...commandRows,
317
+ "| `mg-axi api get` | reviewed raw reads (see src/api.ts) | reviewed-raw-read catalogue | API-01 |",
318
+ "| `mg-axi doctor` | bounded `GET:/users` health check | uses the named user-list read | PACK-01 |",
319
+ "",
320
+ "## EXT-01 domain scope decisions",
321
+ "",
322
+ "Firstmate decision R1: approve narrowing this change to the eight v1.0 domain reads above.",
323
+ "The operations below remain scheduled with an explicit deferred disposition to a later EXT-01 subfamily; no new commands or raw access are approved.",
324
+ "Federation stays out because it can carry signing-certificate material.",
325
+ "Firstmate decision R2: keep the write-family status section as the coverage fix added to scope (resolved-kept).",
326
+ "Both firstmate decisions must also be stated in the PR body by the delivery phase.",
327
+ "",
328
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
329
+ "|---|---|---|---|",
330
+ ...deferredDomainRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
331
+ "",
332
+ "## EXT-03 identity-provider scope decisions",
333
+ "",
334
+ "This change covers the four v1.0 workforce identity-provider reads above (list, show, count, available-types).",
335
+ "The four beta operations below remain scheduled with an explicit deferred disposition to a later EXT-03 subfamily.",
336
+ "Identity-provider reads support v1.0 only; beta needs its own review.",
337
+ "Workforce context only; no external-customer (B2C/External ID tenant) support is claimed.",
338
+ "",
339
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
340
+ "|---|---|---|---|",
341
+ ...deferredProviderRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
342
+ "",
343
+ "## EXT-01 federation-configuration scope decisions",
344
+ "",
345
+ "This change covers the four v1.0 workforce directory federation-configuration reads above (federation-configuration list, show, count and available-types).",
346
+ "The four beta operations below remain scheduled with an explicit deferred disposition to a later EXT-01 subfamily; no new commands or raw access are approved.",
347
+ "Federation-configuration reads support v1.0 only; beta needs its own review.",
348
+ "signingCertificate carries the public token-signing key only and is omitted from default selects; no private key material exists on these resources and the domains navigation property needs its own review.",
349
+ "Workforce context only; domain federationConfiguration sub-reads (/domains/{domain-id}/federationConfiguration) stay scheduled in the later domain federation subfamily.",
350
+ "",
351
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
352
+ "|---|---|---|---|",
353
+ ...deferredFederationRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
354
+ "",
355
+ "## EXT-03 data-policy-operations scope decisions",
356
+ "",
357
+ "This change covers the three v1.0 workforce data-policy-operation reads above (data-policy-operation list, show and count).",
358
+ "The three beta operations below remain scheduled with an explicit deferred disposition to a later EXT-03 subfamily; no new commands or raw access are approved.",
359
+ "Data-policy-operation reads support v1.0 only; beta needs its own review.",
360
+ "storageLocation always renders as the redaction marker; export submission (POST /users/{id}/exportPersonalData) belongs to no read slice.",
361
+ "Workforce context only; no external-customer support is claimed.",
362
+ "",
363
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
364
+ "|---|---|---|---|",
365
+ ...deferredDataPolicyRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
366
+ "",
367
+ "## EXT-03 invitations scope decisions",
368
+ "",
369
+ "Firstmate decision (mg-ext-03b inbox 001): do not implement these reads and add no scope.",
370
+ "The eight v1.0 invitation reads below carry an explicit reviewed unavailable disposition; no new commands or raw access are approved.",
371
+ "The [v1.0 invitation resource documentation](https://github.com/microsoftgraph/microsoft-graph-docs-contrib/blob/main/api-reference/v1.0/resources/invitation.md) lists Create only in its Methods table and provides no documented GET contract for these invitation reads.",
372
+ "Workforce B2B context only; no invitation creation or any mutation is claimed here.",
373
+ "",
374
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
375
+ "|---|---|---|---|",
376
+ ...unavailableInvitationRows.map(row => `| \`${row.id}\` | ${row.disposition} | ${row.owningSlice} | ${row.reason} |`),
377
+ "",
378
+ "## EXT-01 organization scope decisions",
379
+ "",
380
+ "The organization scope covers the five organization/branding/localization reads above; the remaining subfamilies need the separate contracts described below.",
381
+ "The operations below remain scheduled with an explicit deferred disposition to a later EXT-01 subfamily; no new commands or raw access are approved.",
382
+ "Stream image and CSS bytes stay out because they need a separate binary-output contract; beta-only settings, partner and theme contracts need separate review.",
383
+ "",
384
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
385
+ "|---|---|---|---|",
386
+ ...deferredOrganizationRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
387
+ "",
388
+ "## EXT-01 certificate-auth scope decisions",
389
+ "",
390
+ "This change covers the six v1.0 PKI and certificate-authority reads above (certificate-auth-pki and certificate-authority list, show and count).",
391
+ "The three root /certificateBasedAuthConfiguration reads below carry an explicit reviewed unavailable disposition; no new commands or raw access are approved for them.",
392
+ "The v1.0 certificateBasedAuthConfiguration resource Methods table documents List/Create/Get/Delete only on the org-scoped routes, which belong to the deferred organization certificate-auth subfamily.",
393
+ "Certificate-authority entries carry public certificates only; the base64 certificate blob is omitted from default selects and needs an explicit --select naming it.",
394
+ "Certificate-auth reads support v1.0 only; beta needs its own review.",
395
+ "",
396
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
397
+ "|---|---|---|---|",
398
+ ...unavailableCertAuthRows.map(row => `| \`${row.id}\` | ${row.disposition} | ${row.owningSlice} | ${row.reason} |`),
399
+ "## EXT-01 directory-subscriptions scope decisions",
400
+ "",
401
+ "This change covers the three v1.0 commercial-subscription reads above (subscription list, show and count).",
402
+ "The show leaf additionally serves the commerceSubscriptionId alternate-key lookup via --commerce-subscription-id, and the reviewed raw surface carries the matching function route; both bind the key only through the allowlisted session function-argument contract (validated, OData-quoted and encoded).",
403
+ "Subscription reads support v1.0 only; beta needs its own review.",
404
+ "",
405
+ "## EXT-01 group lifecycle scope decisions",
406
+ "",
407
+ "This change covers the six v1.0 group lifecycle policy and group setting template reads above (list, show, count per family).",
408
+ "The operations below remain scheduled with an explicit deferred disposition to a later EXT-01 subfamily; no new commands or raw access are approved.",
409
+ "Beta policies and templates need their own review; delta-token sync needs its own change-tracking contract beyond the approved list/show/count reads; the POST lookup actions need their own request and projection review.",
410
+ "",
411
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
412
+ "|---|---|---|---|",
413
+ ...deferredGroupLifecycleRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
414
+ "",
415
+ "## EXT-01 custom-security-attributes scope decisions",
416
+ "",
417
+ "This change covers the nine v1.0 attribute-set, custom-security-attribute-definition and allowed-value reads above (list, show, count per family).",
418
+ "The nine beta operations below remain scheduled with an explicit deferred disposition to a later EXT-01 subfamily; no new commands or raw access are approved.",
419
+ "Beta attribute sets, definitions and allowed values need their own review; definition $expand (inline allowedValues) is not reviewed here and stays on the dedicated allowed-value list/show reads.",
420
+ "",
421
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
422
+ "|---|---|---|---|",
423
+ ...deferredCustomSecurityRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
424
+ "",
425
+ "## EXT-01 on-premises-synchronization scope decisions",
426
+ "",
427
+ "This change covers the two v1.0 on-premises-synchronization reads above (on-premises-synchronization list and show).",
428
+ "The $count scalar and the three beta operations below remain scheduled with an explicit deferred disposition to later EXT-01 subfamilies; no new commands or raw access are approved.",
429
+ "On-premises-synchronization reads support v1.0 only; beta needs its own review.",
430
+ "Scalar counts need a separate query and response contract from the approved list/show reads.",
431
+ "",
432
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
433
+ "|---|---|---|---|",
434
+ ...deferredOnPremSyncRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
435
+ "",
436
+ "## EXT-01 terms-of-use scope decisions",
437
+ "",
438
+ "This change covers the six v1.0 agreement and agreement-acceptance reads above (agreement list/show, one agreement's acceptance list/show, and tenant-wide acceptance list/show).",
439
+ "The acceptances $count scalar, the agreement file/localization/files/versions sub-reads and the beta operations below remain scheduled with an explicit deferred disposition to later EXT-01 subfamilies; no new commands or raw access are approved.",
440
+ "Terms-of-use reads support v1.0 only; beta needs its own review.",
441
+ "Scalar counts need a separate query and response contract from the approved list/show reads; agreement file contents and localizations need a separate binary-output and projection contract, and file bytes are never downloaded or printed.",
442
+ "",
443
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
444
+ "|---|---|---|---|",
445
+ ...deferredTermsOfUseRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
446
+ "",
447
+ "## EXT-01 directory-objects scope decisions",
448
+ "",
449
+ "This change covers the three v1.0 directory-object reads above (directory-object list, show and count).",
450
+ "The delta sync, the POST lookup/validation actions and the beta operations below remain scheduled with an explicit deferred disposition to later EXT-01 subfamilies; no new commands or raw access are approved.",
451
+ "Directory-object reads support v1.0 only; beta needs its own review.",
452
+ "Delta-token sync needs its own change-tracking contract beyond the approved list/show/count reads; the POST lookup and validation actions need their own request and projection review.",
453
+ "Rows are polymorphic: only the base-type properties plus the @odata.type discriminator are projected, so subtype secrets or credentials can never appear.",
454
+ "",
455
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
456
+ "|---|---|---|---|",
457
+ ...deferredDirectoryObjectRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
458
+ "",
459
+ "## EXT-01 deleted-items scope decisions",
460
+ "",
461
+ "This change covers the eleven v1.0 deleted directory-item reads above (deleted-user, deleted-group, deleted-application, deleted-service-principal and deleted-administrative-unit list and count, plus deleted-item show).",
462
+ "The rows below remain scheduled with an explicit deferred disposition; no new commands or raw access are approved for them.",
463
+ "Deleted-item reads support v1.0 only; beta needs its own review.",
464
+ "Untyped list/count commands do not exist because upstream requires the OData cast as part of the list URI.",
465
+ "Device casts carry no documented v1.0 permission contract: the list/get pages name the supported types and device is not among them.",
466
+ "Typed-cast singles are covered by the approved untyped show, which returns the same object for every type; the cast suffix only asserts the expected type.",
467
+ "The POST lookup and validation actions need their own request and projection review; restore and permanent delete are mutations and stay out entirely.",
468
+ "",
469
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
470
+ "|---|---|---|---|",
471
+ ...deferredDeletedItemsRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
472
+ "",
473
+ "## EXT-01 contacts scope decisions",
474
+ "",
475
+ "This change covers the three v1.0 top-level contact reads above (contact list, show and count), the ten v1.0 manager/directReports navigation reads above (show-manager, list/show/count-direct-reports with user/contact casts), and the eighteen v1.0 memberOf/transitiveMemberOf navigation reads above (list/show/count-member-of with transitive and group/administrativeUnit casts).",
476
+ "The delta sync, the POST lookup actions and the beta operations below remain scheduled with an explicit deferred disposition to later EXT-01 subfamilies; no new commands or raw access are approved.",
477
+ "Contact reads support v1.0 only; beta needs its own review.",
478
+ "Delta-token sync needs its own change-tracking contract beyond the approved reads; the POST lookup actions need their own request and projection review.",
479
+ "Contacts are personal data: top-level default rows carry id, displayName, mail and companyName only; navigation rows carry the @odata.type discriminator plus id and displayName only, and identifying fields need an explicit --select.",
480
+ "The three error/sync navigation reads below are unavailable by firstmate mg-ext-01m decision: no operation-level permission documentation exists for them, so no read-scope contract can be established.",
481
+ "",
482
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
483
+ "|---|---|---|---|",
484
+ ...deferredContactRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
485
+ ...unavailableContactRows.map(row => `| \`${row.id}\` | ${row.disposition} | ${row.owningSlice} | ${row.reason} |`),
486
+ "",
487
+ "## EXT-04 partner contracts scope decisions",
488
+ "",
489
+ "Firstmate scope: approve narrowing this change to the three v1.0 partner-contract reads above (list, show, count).",
490
+ "The operations below remain scheduled with an explicit deferred disposition to a later EXT-04 subfamily; no new commands or raw access are approved.",
491
+ "Beta contracts need their own review; delta-token sync needs its own change-tracking contract beyond the approved list/show/count reads.",
492
+ "",
493
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
494
+ "|---|---|---|---|",
495
+ ...deferredContractRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`), "",
496
+ "## EXT-04 delegated-admin scope decisions",
497
+ "",
498
+ "Firstmate scope: approve narrowing this change to the twelve v1.0 delegated-admin reads above (customer list/show, relationship list/show, access-assignment list/show, operation list/show, request list/show and service-management-detail list/show).",
499
+ "The operations below remain scheduled with an explicit deferred disposition to a later EXT-04 subfamily; no new commands or raw access are approved.",
500
+ "Scalar counts need a separate query and response contract from the approved list/show reads; the tenantRelationship container root needs its own projection review; multi-tenant-organization reads shipped in the section below, and tenant-lookup functions ship as tenant-information show under the mg-ext-04e function-argument contract.",
501
+ "",
502
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
503
+ "|---|---|---|---|",
504
+ ...deferredDelegatedAdminRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`), "",
505
+ "## EXT-04 multi-tenant-organization scope decisions",
506
+ "",
507
+ "Firstmate scope: approve narrowing this change to the four v1.0 multi-tenant-organization reads above (organization show, join-request show, tenant list/count).",
508
+ "The single-member read below remains scheduled with an explicit deferred disposition: its documented least privilege is the write scope MultiTenantOrganization.ReadWrite.All in both delegated and application modes, so no named command or raw access is approved for it until its own least-privilege review lands.",
509
+ "",
510
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
511
+ "|---|---|---|---|",
512
+ ...deferredMtoRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`), "",
513
+ "## EXT-04 tenant-lookup scope decisions",
514
+ "",
515
+ "Firstmate scope: approve the mg-ext-04e function-argument contract with the three v1.0 reads it unblocks (tenant-information show by domain name or tenant ID, subscription show by commerce subscription ID).",
516
+ "The shared session binds parenthesised function arguments only for these three allowlisted catalogue operations, each with its exact function/key name and parameter name; every other path keeps the refusal. Values arrive only as explicit CLI flags, are validated per type (DNS name, tenant GUID, commerce identifier), OData-quoted with single quotes doubled, then percent-encoded past the RFC3986 unreserved set; control characters, separators, reserved characters and overlong values fail before credentials. The reviewed raw surface carries the same three routes with the same binding.",
517
+ "Tenant-information lookups need CrossTenantInformation.ReadBasic.All with no Entra role; the commerce lookup needs Organization.Read.All plus a supported subscription role. Personal Microsoft accounts are not supported for any of the three. Tenant-information reads support v1.0 only; beta needs its own review.",
518
+ "",
519
+ "## EXT-02 access-review scope decisions",
520
+ "",
521
+ "The access-review scope covers the ten reads above (definition, instance, decision, contacted-reviewer and stage list/show reads).",
522
+ "The legacy accessReviews and unified alias reads below are split into the EXT-02c follow-up to limit this piece's size; beta-only operations stay deferred (\"beta needs its own review\").",
523
+ "The history reads below are blocked: their documented least privilege is the write scope AccessReview.ReadWrite.All (no read scope), and history instances return SAS download URLs in downloadUri.",
524
+ "No new commands or raw access are approved for any row below.",
525
+ "",
526
+ "| Inventory operation | Disposition | Deferral reason |",
527
+ "|---|---|---|",
528
+ ...deferredAccessReviewRows.map(row => `| \`${row.id}\` | ${row.disposition}: ${row.owningSlice} | ${row.reason} |`),
529
+ "",
530
+ "## EXT-02 entitlement-management scope decisions",
531
+ "",
532
+ "The entitlement-management scope covers the eighteen reads above (catalog, access-package, assignment-policy and resource-role-scope list/show/count reads plus assignment and assignment-request list/show/count reads).",
533
+ "Approval and subject reads carry personal data and stay scheduled for later parts with no new commands or raw access approved; so do assignment and request navigation sub-reads (assignment target/accessPackage/assignmentPolicy, request accessPackage/assignment/requestor), the top-level assignment-policy and resource-role-scope lists, catalog navigation reads (accessPackages, resources, resourceScopes, resourceRoles, customWorkflowExtensions), package navigation reads (catalog, incompatible sets), policy navigation reads (questions, custom-extension stages), role/scope link expansion, filterByCurrentUser and additionalAccess functions (parenthesised function segments need their own session-guard binding review), and every beta operation below. Raw $expand review is a separate change: the named assignment and request reads flatten one fixed documented expansion in code, and no user-supplied $expand exists on any route.",
534
+ "",
535
+ "| Inventory operation | Disposition | Owning slice | Deferral reason |",
536
+ "|---|---|---|---|",
537
+ ...deferredEntitlementRows.map(row => `| \`${row.id}\` | ${row.disposition} (deferred) | ${row.owningSlice} | ${row.reason} |`),
538
+ "",
539
+ "## EXT-02 lifecycle-workflows scope decisions",
540
+ "",
541
+ "This change covers the twenty-four v1.0 lifecycle-workflows first-, second- and third-part reads above (workflow, workflow-template and task-definition list/show/count reads, the tenant settings show, top-level run and user/subject processing-result list/show/count reads, plus task-report list/show/count reads with the task and taskDefinition singles).",
542
+ "Run-nested and third-level processing results, taskProcessingResults sub-reads, reprocessed runs, subject sub-reads, workflow tasks, template tasks, versions, insights, deleted items and custom task extensions stay scheduled for follow-up parts with no new commands or raw access approved; so do the six top-level summary functions, which stay deferred because they need bracketed start/end arguments outside the session-guard function-binding allowlist while the request-path guard cannot change beyond READ_SCOPES additions. Every beta operation and every mutation (no workflow create/update/delete/activate/run, no settings update, no restore) stays out.",
543
+ "No new commands or raw access are approved for any lifecycle-workflows row outside the twenty-four above.",
544
+ "",
545
+ "## Named writes",
546
+ "",
547
+ "Each write below runs the WRITE-00 mutation coordinator: hand-enabled profile, immutable scope, preview, explicit `--execute`, typed target confirmation for disruptive effects, durable journal intent/outcome and no replay. Inventory dispositions stay discovery-time records until a metadata refresh reviews them; the implemented review lives beside each command.",
548
+ "",
549
+ "| Command | Operation | Owning slice |",
550
+ "|---|---|---|",
551
+ ...writeRows,
552
+ "",
553
+ `Shipped initial writes: ${writeStatus.shipped.length ? writeStatus.shipped.join(", ") : "none"}.`,
554
+ `Pending initial writes: ${writeStatus.pending.length ? writeStatus.pending.join(", ") : "none"}.`,
555
+ "Extended families (EXT-01 through EXT-04) and the full-Entra audit (FULL-01, COMPLETE-01) own the remaining",
556
+ "scheduled rows; see docs/build-plan.md for their dispatch.",
557
+ "",
558
+ ].join("\n");
559
+ }