agentfootprint 9.73.0 → 9.75.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 (122) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +201 -0
  3. package/CLAUDE.md +3 -2
  4. package/ai-instructions/claude-code/SKILL.md +1 -1
  5. package/dist/adapters/identity/azure.js +306 -0
  6. package/dist/adapters/identity/azure.js.map +1 -0
  7. package/dist/adapters/llm/FoundryLocalProvider.js +992 -0
  8. package/dist/adapters/llm/FoundryLocalProvider.js.map +1 -0
  9. package/dist/adapters/llm/FoundryProvider.js +273 -0
  10. package/dist/adapters/llm/FoundryProvider.js.map +1 -0
  11. package/dist/adapters/llm/OllamaProvider.js +171 -12
  12. package/dist/adapters/llm/OllamaProvider.js.map +1 -1
  13. package/dist/adapters/llm/OpenAIProvider.js +105 -2
  14. package/dist/adapters/llm/OpenAIProvider.js.map +1 -1
  15. package/dist/adapters/llm/createProvider.js +143 -10
  16. package/dist/adapters/llm/createProvider.js.map +1 -1
  17. package/dist/adapters/types.js.map +1 -1
  18. package/dist/core/Agent.js +23 -1
  19. package/dist/core/Agent.js.map +1 -1
  20. package/dist/core/agent/AgentBuilder.js +12 -0
  21. package/dist/core/agent/AgentBuilder.js.map +1 -1
  22. package/dist/core/agent/evidence/gate.js +22 -3
  23. package/dist/core/agent/evidence/gate.js.map +1 -1
  24. package/dist/core/agent/stagedRefs.js +161 -0
  25. package/dist/core/agent/stagedRefs.js.map +1 -0
  26. package/dist/core/agent/stages/callLLM.js +22 -2
  27. package/dist/core/agent/stages/callLLM.js.map +1 -1
  28. package/dist/core/agent/stages/evidenceRecheck.js +21 -2
  29. package/dist/core/agent/stages/evidenceRecheck.js.map +1 -1
  30. package/dist/esm/adapters/identity/azure.d.ts +188 -0
  31. package/dist/esm/adapters/identity/azure.js +302 -0
  32. package/dist/esm/adapters/identity/azure.js.map +1 -0
  33. package/dist/esm/adapters/llm/FoundryLocalProvider.d.ts +215 -0
  34. package/dist/esm/adapters/llm/FoundryLocalProvider.js +986 -0
  35. package/dist/esm/adapters/llm/FoundryLocalProvider.js.map +1 -0
  36. package/dist/esm/adapters/llm/FoundryProvider.d.ts +178 -0
  37. package/dist/esm/adapters/llm/FoundryProvider.js +268 -0
  38. package/dist/esm/adapters/llm/FoundryProvider.js.map +1 -0
  39. package/dist/esm/adapters/llm/OllamaProvider.js +171 -12
  40. package/dist/esm/adapters/llm/OllamaProvider.js.map +1 -1
  41. package/dist/esm/adapters/llm/OpenAIProvider.d.ts +92 -1
  42. package/dist/esm/adapters/llm/OpenAIProvider.js +103 -1
  43. package/dist/esm/adapters/llm/OpenAIProvider.js.map +1 -1
  44. package/dist/esm/adapters/llm/createProvider.d.ts +48 -12
  45. package/dist/esm/adapters/llm/createProvider.js +143 -10
  46. package/dist/esm/adapters/llm/createProvider.js.map +1 -1
  47. package/dist/esm/adapters/types.d.ts +4 -2
  48. package/dist/esm/adapters/types.js.map +1 -1
  49. package/dist/esm/core/Agent.js +23 -1
  50. package/dist/esm/core/Agent.js.map +1 -1
  51. package/dist/esm/core/agent/AgentBuilder.d.ts +12 -0
  52. package/dist/esm/core/agent/AgentBuilder.js +12 -0
  53. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  54. package/dist/esm/core/agent/evidence/gate.d.ts +11 -1
  55. package/dist/esm/core/agent/evidence/gate.js +22 -3
  56. package/dist/esm/core/agent/evidence/gate.js.map +1 -1
  57. package/dist/esm/core/agent/evidence/types.d.ts +23 -0
  58. package/dist/esm/core/agent/stagedRefs.d.ts +94 -0
  59. package/dist/esm/core/agent/stagedRefs.js +154 -0
  60. package/dist/esm/core/agent/stagedRefs.js.map +1 -0
  61. package/dist/esm/core/agent/stages/callLLM.d.ts +17 -0
  62. package/dist/esm/core/agent/stages/callLLM.js +22 -2
  63. package/dist/esm/core/agent/stages/callLLM.js.map +1 -1
  64. package/dist/esm/core/agent/stages/evidenceRecheck.d.ts +19 -1
  65. package/dist/esm/core/agent/stages/evidenceRecheck.js +21 -2
  66. package/dist/esm/core/agent/stages/evidenceRecheck.js.map +1 -1
  67. package/dist/esm/events/payloads.d.ts +39 -0
  68. package/dist/esm/events/registry.d.ts +3 -1
  69. package/dist/esm/events/registry.js +2 -0
  70. package/dist/esm/events/registry.js.map +1 -1
  71. package/dist/esm/identity.d.ts +1 -0
  72. package/dist/esm/identity.js +9 -0
  73. package/dist/esm/identity.js.map +1 -1
  74. package/dist/esm/index.d.ts +1 -1
  75. package/dist/esm/index.js.map +1 -1
  76. package/dist/esm/providers.d.ts +5 -0
  77. package/dist/esm/providers.js +14 -0
  78. package/dist/esm/providers.js.map +1 -1
  79. package/dist/events/registry.js +2 -0
  80. package/dist/events/registry.js.map +1 -1
  81. package/dist/identity.js +14 -1
  82. package/dist/identity.js.map +1 -1
  83. package/dist/index.js.map +1 -1
  84. package/dist/providers.js +20 -1
  85. package/dist/providers.js.map +1 -1
  86. package/dist/types/adapters/identity/azure.d.ts +189 -0
  87. package/dist/types/adapters/identity/azure.d.ts.map +1 -0
  88. package/dist/types/adapters/llm/FoundryLocalProvider.d.ts +216 -0
  89. package/dist/types/adapters/llm/FoundryLocalProvider.d.ts.map +1 -0
  90. package/dist/types/adapters/llm/FoundryProvider.d.ts +179 -0
  91. package/dist/types/adapters/llm/FoundryProvider.d.ts.map +1 -0
  92. package/dist/types/adapters/llm/OllamaProvider.d.ts.map +1 -1
  93. package/dist/types/adapters/llm/OpenAIProvider.d.ts +92 -1
  94. package/dist/types/adapters/llm/OpenAIProvider.d.ts.map +1 -1
  95. package/dist/types/adapters/llm/createProvider.d.ts +48 -12
  96. package/dist/types/adapters/llm/createProvider.d.ts.map +1 -1
  97. package/dist/types/adapters/types.d.ts +4 -2
  98. package/dist/types/adapters/types.d.ts.map +1 -1
  99. package/dist/types/core/Agent.d.ts.map +1 -1
  100. package/dist/types/core/agent/AgentBuilder.d.ts +12 -0
  101. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  102. package/dist/types/core/agent/evidence/gate.d.ts +11 -1
  103. package/dist/types/core/agent/evidence/gate.d.ts.map +1 -1
  104. package/dist/types/core/agent/evidence/types.d.ts +23 -0
  105. package/dist/types/core/agent/evidence/types.d.ts.map +1 -1
  106. package/dist/types/core/agent/stagedRefs.d.ts +95 -0
  107. package/dist/types/core/agent/stagedRefs.d.ts.map +1 -0
  108. package/dist/types/core/agent/stages/callLLM.d.ts +17 -0
  109. package/dist/types/core/agent/stages/callLLM.d.ts.map +1 -1
  110. package/dist/types/core/agent/stages/evidenceRecheck.d.ts +19 -1
  111. package/dist/types/core/agent/stages/evidenceRecheck.d.ts.map +1 -1
  112. package/dist/types/events/payloads.d.ts +39 -0
  113. package/dist/types/events/payloads.d.ts.map +1 -1
  114. package/dist/types/events/registry.d.ts +3 -1
  115. package/dist/types/events/registry.d.ts.map +1 -1
  116. package/dist/types/identity.d.ts +1 -0
  117. package/dist/types/identity.d.ts.map +1 -1
  118. package/dist/types/index.d.ts +1 -1
  119. package/dist/types/index.d.ts.map +1 -1
  120. package/dist/types/providers.d.ts +5 -0
  121. package/dist/types/providers.d.ts.map +1 -1
  122. package/package.json +5 -1
package/AGENTS.md CHANGED
@@ -524,7 +524,7 @@ files `agentfootprint.tools.coverage_declared`; with
524
524
  to the final answer, which is how a limit survives a model that would rather not
525
525
  mention it.
526
526
 
527
- ### Observability — 108 typed events across 24 domains
527
+ ### Observability — 109 typed events across 24 domains
528
528
 
529
529
  ```typescript
530
530
  agent.on('agentfootprint.context.injected', (e) =>
package/CHANGELOG.md CHANGED
@@ -7,6 +7,207 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [9.75.0] - 2026-08-28
11
+
12
+ ### Added
13
+
14
+ - **Grounded numbers: the staged-refs nudge, and a revise correction that
15
+ names the route.** The field failure this closes, from a consumer's recorded
16
+ run: four tool results carried real numbers, a compute tool that could sum
17
+ them was registered — with `wants` declared over the staged dataset kind —
18
+ and the app's prompt said to use it. The model summed the numbers in its
19
+ head anyway and stated the total; the evidence gate recorded *"appears in no
20
+ tool result"* and the answer shipped, because the posture only observed. The
21
+ app patched it with more prose. The library-shaped fix is two mechanisms it
22
+ already owns, on the one dial it already has:
23
+
24
+ **`nudge: true` on `.namesAndNumbersFromEvidence()`** — when an iteration's
25
+ context holds a tool result staged by reference (an `artifacts.placement`
26
+ ticket) AND a tool the model can currently call declares `wants` over that
27
+ ticket's kind, ONE short line is appended at the very END of that request,
28
+ naming the refs and the spender tool by its registered name: derived numbers
29
+ come from the tool, not from mental arithmetic. Composed entirely from
30
+ declarations (`Tool.resultKind`, `Tool.wants`, matched by the exact-string
31
+ law dispatch uses — never by tool name); no prose surface for apps. The
32
+ placement is the point: the measured failure was recency — the app's own
33
+ instruction sat at the top of a long context and the numbers at the bottom,
34
+ so this one sits beside the data. Request-only (never history, so it never
35
+ enters the gate's exempt corpus), recomposed per iteration so it exists
36
+ exactly while both conditions hold, judged against the tools REALLY served
37
+ this call (the wrap-up's withheld surface arms nothing). Each firing lands
38
+ as `agentfootprint.agent.grounding_nudged` (event 109) with refs and tools
39
+ as data — the line's one record, since the line itself is not conversation.
40
+
41
+ ```ts
42
+ const agent = Agent.create({ provider, model, artifacts: { store, placement } })
43
+ .tool(exportRows) // resultKind: 'dataset/rows' — staged over the threshold
44
+ .tool(compute) // wants: { dataset: 'dataset/rows' } — the declared spender
45
+ .namesAndNumbersFromEvidence({ posture: 'guard', nudge: true })
46
+ .build();
47
+ ```
48
+
49
+ **The guarantee stays the postures the gate has had since 9.35.0** — the
50
+ existing value extraction is THE detector, unchanged: `'assist'` records,
51
+ `'guard'` allows the one bounded revision then delivers with both attempts
52
+ on the record, `'rails'` refuses with `UnsupportedValuesError`. What the
53
+ revision gains: when the flagged turn holds staged refs a served `wants`
54
+ tool can spend, the correction now names them — *"pass 'art_…'
55
+ (dataset/rows) to `compute` — compute the number there and answer with what
56
+ it returns"* — inside the authored frame, so the quoted values still come
57
+ last and the exempt-corpus fence is untouched. The `revision-asked`
58
+ `evidence_checked` event carries the same facts additively (`stagedRefs`,
59
+ `spenderTools`). Absent everything — no gate, `nudge` unset, or no
60
+ `wants`-declaring tool — every request, record and correction keeps its
61
+ exact bytes, pinned by test.
62
+
63
+ ### Changed
64
+
65
+ - **A `guard`/`rails` agent that also registers a `wants`-declaring tool will
66
+ see its evidence correction gain the refs clause above when staged refs are
67
+ in context.** That is the fix, not a side effect: a correction that says
68
+ "call the tool that provides it" without naming WHICH tool over WHICH ref
69
+ leaves the model to head-math again. Agents without a `wants` tool — or
70
+ without staged refs in the flagged turn — keep the exact 9.35.0 sentence.
71
+
72
+ ## [9.74.0] - 2026-08-27
73
+
74
+ ### Added
75
+
76
+ - **The Azure/Foundry column exists — keyless auth and both Microsoft inference
77
+ doors, cloud and on-device.** AWS and GCP each had a full adapter column;
78
+ Azure had a static api-key string and nothing else. This release fills the
79
+ auth + inference tier, and every piece is a vendor adapter over ports that
80
+ did not change — the same seams the AWS and GCP columns already use.
81
+
82
+ **`foundry()` — the project-endpoint provider** (`agentfootprint/providers`).
83
+ The JS answer to Microsoft's `FoundryChatClient(project_endpoint, model,
84
+ credential)`: point it at a Foundry project endpoint (or let the hosted
85
+ platform's auto-injected `FOUNDRY_PROJECT_ENDPOINT` supply it), name the
86
+ deployment (`AZURE_AI_MODEL_DEPLOYMENT_NAME ?? MODEL_NAME`), and auth is an
87
+ Entra `TokenCredential`, an api key, or — given neither — the platform's own
88
+ blessed default, `DefaultAzureCredential` from the optional `@azure/identity`
89
+ peer. Inference rides the GA api-version-free `/openai/v1` route derived from
90
+ the project endpoint; the deployment name travels as the `model` field; the
91
+ token scope is `https://ai.azure.com/.default` (the management audience is a
92
+ different token — the docs say so out loud). Rotating tokens reuse the
93
+ per-request key-callback seam `openai()` has had since 9.29.0: the client is
94
+ rebuilt only when the token string actually changes.
95
+
96
+ ```ts
97
+ import { foundry } from 'agentfootprint/providers';
98
+ // In a Foundry hosted container: zero config — endpoint injected, identity ambient.
99
+ const llm = foundry();
100
+ // Locally: az login, then name the project.
101
+ const local = foundry({
102
+ projectEndpoint: 'https://acct.services.ai.azure.com/api/projects/my-project',
103
+ deployment: 'gpt-4.1-mini',
104
+ });
105
+ ```
106
+
107
+ **`foundryLocal()` — the on-device door.** Fetch-only, zero dependencies, the
108
+ `ollama()` discipline applied to Foundry Local's OpenAI-compatible `/v1`
109
+ wire: an alias like `qwen2.5-0.5b` resolves to a concrete variant through the
110
+ service's own catalog (`GET /foundry/list`, priority order, first wins;
111
+ cached per provider), a full variant id skips the catalog entirely, and the
112
+ typed `FoundryLocalUnavailableError` tells the truth a local runtime needs
113
+ telling — `foundry server start` to start it, `foundry server status` to find
114
+ the dynamic port, `foundry model run <alias>` when the model is the missing
115
+ piece, with the machine's actual model list attached when the service could
116
+ answer. No API key is sent because none exists. Streamed usage is read off
117
+ the final empty-choices frame — the exact bug class 9.73.0 fixed, pinned here
118
+ from day one.
119
+
120
+ **`entraIdentity()` — Azure credentials for tools**
121
+ (`agentfootprint/security`). The `googleIdentity` anatomy, law for law:
122
+ vends the deployment's identity as a bearer via any `TokenCredential`
123
+ (default: `DefaultAzureCredential`'s chain — env service principal, workload
124
+ identity, managed identity, VS Code, az CLI), scopes the request's way or
125
+ `AZURE_AI_SCOPE` by default, refuses `mode: 'user'` and per-request user
126
+ tokens BY NAME until an OBO surface exists, refuses services outside the
127
+ allowlist, and never lets an SDK's failure text — which echoes request
128
+ detail — reach a thrown message. `AZURE_AI_SCOPE` and
129
+ `AZURE_MANAGEMENT_SCOPE` are both exported because the audiences are not
130
+ interchangeable, and pretending there is one "azure scope" would be a lie
131
+ that 401s at runtime.
132
+
133
+ **`azureOpenai({ credential })` — the existing Azure door goes keyless.** An
134
+ Entra credential now rides the SDK's `azureADTokenProvider`; the static
135
+ api-key path is byte-identical to before; both given at once is refused by
136
+ name as the config bug it is. The docs' old caveat — "azureOpenai is the
137
+ wrong door for bearer auth" — is retired. Each keyless door defaults to the
138
+ audience ITS route documents: this one asks for
139
+ `https://cognitiveservices.azure.com/.default` (the classic deployment-scoped
140
+ route's own documented audience, `AZURE_COGNITIVE_SERVICES_SCOPE`), while
141
+ `foundry()` asks for `https://ai.azure.com/.default` (`AZURE_AI_SCOPE`, the
142
+ v1/project route's) — both overridable via `scope`, both pinned on the wire
143
+ by tests that record what the credential was actually asked for.
144
+
145
+ **`openai({ legacyEndpoint })` — the dialect dial goes public.** `baseURL`
146
+ has always implied the legacy dialect (`max_tokens`, no `stream_options`)
147
+ because most OpenAI-compatible servers are behind; `legacyEndpoint: false`
148
+ now declares "this baseURL speaks the current dialect" — which is exactly
149
+ what the Azure v1 route is. Default unchanged: `!!baseURL`.
150
+
151
+ **`providerFromEnv` learns both doors — upgrade-safely.** `FOUNDRY_LOCAL_MODEL`
152
+ slots directly after `OLLAMA_MODEL` (a model name you typed for a local
153
+ runtime — the same name-beats-leftover-credential law), and
154
+ `FOUNDRY_PROJECT_ENDPOINT` + `AZURE_AI_MODEL_DEPLOYMENT_NAME` — a pair of
155
+ product-specific spellings nobody exports by accident — outranks the
156
+ lingering-credential arms. Two guards keep an upgrade from breaking a
157
+ working environment, because the hosted platform auto-injects the endpoint
158
+ into every container, Foundry-bound or not: an endpoint with **no**
159
+ deployment named HOLDS its refusal (every arm below answers exactly as it
160
+ did before this arm existed, and the held refusal is raised only when
161
+ nothing else resolves), and the generic `MODEL_NAME` alone never carries the
162
+ endpoint past a bootable Azure config — choosing Foundry over working Azure
163
+ takes the deliberate spelling. Every existing precedence is pinned
164
+ untouched; each guard has its own test.
165
+
166
+ New from `agentfootprint/providers`: `foundry`, `foundryInferenceUrl`,
167
+ `FoundryProviderOptions`, `foundryLocal`, `FoundryLocalProvider`,
168
+ `FoundryLocalUnavailableError`, `FoundryLocalProviderOptions`,
169
+ `TokenCredentialLike`, `AccessTokenLike`. New from `agentfootprint/security`:
170
+ `entraIdentity`, `AZURE_AI_SCOPE`, `AZURE_MANAGEMENT_SCOPE`,
171
+ `AZURE_COGNITIVE_SERVICES_SCOPE`, `EntraIdentityOptions`,
172
+ `TokenCredentialLike`, `AccessTokenLike`, `AzureIdentitySdkModule`. New
173
+ optional peer: `@azure/identity`. Pinned by 178 new test cases (counted in
174
+ the diff, not the runner) across
175
+ `test/adapters/identity/entra-identity.test.ts`,
176
+ `test/adapters/unit/FoundryProvider.test.ts`,
177
+ `test/adapters/unit/FoundryLocalProvider.test.ts`,
178
+ `test/adapters/integration/foundry-wire.test.ts` (a real `openai` SDK against
179
+ a local fake of the `/openai/v1` wire — Bearer header, `max_completion_tokens`,
180
+ token rotation, and the audience each door asks its credential for, all
181
+ asserted on the wire, not assumed), the extended Azure env/wire suites, and
182
+ the fix pins from the adversarial review below. Deliberately NOT in this train: the App Insights sink,
183
+ Azure AI Search, the Toolbox MCP transport (next trains), and every
184
+ preview-only surface (A2A door, browser/computer-use, managed memory) —
185
+ refusing to ship against previews we cannot verify is a feature.
186
+
187
+ The whole train was adversarially reviewed before release (five lenses, every
188
+ serious finding independently re-verified with a reproduction): 1 blocker and
189
+ 9 should-fix findings were confirmed and every one is fixed and pinned in
190
+ this release — including a mid-stream failure frame that `foundryLocal()`
191
+ would have reported as a clean stop, an abort signal that never reached a
192
+ streaming body, and an env-detection arm that would have broken working
193
+ deployments on upgrade.
194
+
195
+ ### Fixed
196
+
197
+ - **`ollama()` inherited four stream/abort defects — found by reviewing the new
198
+ Foundry Local adapter, fixed at the root.** The adversarial review of
199
+ `foundryLocal()` proved its four streaming defects were byte-twin shapes
200
+ copied from `OllamaProvider`, which had shipped them for months: a caller's
201
+ already-aborted `AbortSignal` still sent the request; an abort after headers
202
+ never reached the streaming body (generation ran on, un-stoppable); an early
203
+ `break` leaked the response body (the reader was never cancelled); and a
204
+ mid-stream `{"error": …}` frame was silently dropped, reporting a failed
205
+ generation as a clean stop. All four now match the fixed Foundry Local
206
+ shapes — same helper names, Ollama's NDJSON wire. 10 of the 12 new pinning
207
+ tests fail against the previous source (proven by restoring it); no public
208
+ API change; `OllamaUnavailableError` is byte-identical.
209
+
210
+
10
211
  ## [9.73.0] - 2026-08-27
11
212
 
12
213
  ### Fixed
package/CLAUDE.md CHANGED
@@ -47,6 +47,7 @@ not in this table, search `src/index.ts` for the nearest noun before writing cod
47
47
  | a value that must say how it knows itself — an unknown count that can never render as zero | `Claim<T>` (`known`/`unknown`/`notApplicable`) | `src/lib/claim/claim.ts` | 9.58.0 |
48
48
  | asking a debugging model "what did this run contradict itself about, and why?" — the Context Integrity findings, joined to the step that filed them | `find_context_errors` | `src/lib/trace-toolpack/traceToolpack.ts` | 9.61.0 |
49
49
  | counting a HUMAN-VERIFIED value as ground for the choice-seam check — the person clicked a row, the app verified the cells against the artifact, and the id the model takes from that selection is not fabricated; the source label travels onto the record | `externalGrounds` + `ExternalGround` + `external_ground_used` | `src/integrity/unsupported-argument/check.ts` | 9.72.0 |
50
+ | a model head-mathing a total from tool-result numbers while a compute tool sits unused on the wire — a LATE line each iteration naming the staged refs and the `wants` tool that spends them (recency working FOR the instruction), plus the revise correction naming the same route | `nudge: true` + `stagedRefsNudgeLine` + `findStagedRefs` + `grounding_nudged` | `src/core/agent/stagedRefs.ts` | 9.75.0 |
50
51
  | a tool answering "I looked and found nothing", routably | `absent` | `src/core/agent/coverage/absent.ts` | 9.43.0 |
51
52
  | stating what a clean answer does NOT rule out | `coverage` | `src/core/agent/coverage/ledger.ts` | 9.43.0 |
52
53
  | minting one of those shapes from a tool that is NOT JavaScript — the canonical note sentences and reserved marker keys as DATA, so a Python/Go/Rust sidecar reads a file instead of regex-scraping `dist/esm` (which a consumer really did) | `canonical-notes.json` at the package root + the `./canonical-notes.json` exports entry, GENERATED from the built barrel by `scripts/gen-canonical-notes.mjs` | `scripts/gen-canonical-notes.mjs` | 9.70.0 |
@@ -133,7 +134,7 @@ Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `d
133
134
  - **Recordings as artifacts (9.26.0)**: `artifacts: { store, recordings: true | { label } }`. `Agent.startRunRecording()` calls the SAME `recordRun` (before `createExecutor` — `attach()` collects for the executor not yet built); `fileRunRecording` mints AFTER `finalizeResult`, awaited, every failure contained to `artifacts.refused`. Pure half in artifacts/recordingArtifact.ts; payload is the recording's JSON TEXT (a live snapshot handed to an in-process store would be a live view into a finished run). No new wire op — `artifact-get` serves it.
134
135
  - **Code staging-in (9.26.0)**: `CodeSession.stageInputs?(inputs) → StagedCodeInput[]` — OPTIONAL, feature-detected via `canStageCodeInputs`, and its contract is TWO promises: the payloads are readable at the returned paths, AND every later `execute` exposes the manifest as `STAGED_INPUTS_ENV` (`AF_STAGED_INPUTS`, `name → path`). `CodeInput.name` is the MANIFEST KEY (the wants arg name, so a static description can name it) and `fileName` is the on-disk name — separate fields so the two cannot drift. `codeRunnerTool({ wants })` composes the schema properties + the description clause and refuses BY NAME on a non-staging runner. Implemented by localCodeRunner only.
135
136
  - **Repeated-call nudge (9.26.0)**: `core/agent/repeatedCall.ts` (pure `noteRepeatedCall` + `repeatedCallLedgers()`) + the ONE batch-loop hook in toolCalls. Fingerprints (FNV-1a) of stable-stringified args and the tool's OWN delivered result — never values. **The counters are NOT tracked state**: they live in a bounded run-keyed map held by `buildToolCallsHandler` and keyed by `deps.currentRun().runId`, so a turn that repeats nothing is byte-identical in state, commit log, narrative and recordings (a scope key would have changed all four for every agent that merely upgraded); the repeat itself rides `agentfootprint.tools.repeated_call`, the emit channel per-attempt facts belong on. A resume mints a new runId ⇒ counting restarts. Fires at the SECOND identical landing, once. `AgentOptions.repeatedCallNudge: false` disables (threaded value-conditionally; ON is the default). Deliberately NOT applied on the pause-resume dispatch paths.
136
- - **Evidence gate (9.35.0)** — `.namesAndNumbersFromEvidence({ posture, shapes, exempt, minDigits })`: every name/number in the final answer must appear in a `role:'tool'` result. Postures reuse the routing VOCABULARY (`assist`|`guard`|`rails`) as a SEPARATE option — routing authority ≠ evidence discipline, and overloading `skillGraphCascade.strictness` would deny "strict routing, loose evidence". Wiring is the stepNudge blast radius verbatim: `ResolvedEvidenceGate` (builder-resolved, refusals at the CALL SITE) → Agent ctor trailing param → `buildRouteDeciderStage`'s 4th arg (`judgeEvidence` runs LAST of the three judges — schema > steps > evidence — and NOT on a denied or schema-exhausted answer) → `evidenceRecheckStage` branch (`{loopTo}`, mounted only for a revising posture) → `STAGE_IDS.EVIDENCE_RECHECK` + BOUNDARY_LOCAL_IDS + milestoneFor. TWO deps flags, and the second is the one a reader misses: `evidenceRecheckStage` (branch) AND `hasEvidenceGate` (bubbles `systemPromptInjections` out of sf-llm-call in the GROUPED chart — without it the gate flags the app's own prompt). Per-check facts ride `agentfootprint.agent.evidence_checked` (emit channel); only the terminal verdict is committed (`unsupportedValues`), because the boundary raises off it. `UnsupportedValuesError` joins the TERMINAL-typed-error list in `run()`'s catch (a verdict is not a crash — no retry handle for a wall).
137
+ - **Evidence gate (9.35.0)** — `.namesAndNumbersFromEvidence({ posture, shapes, exempt, minDigits })`: every name/number in the final answer must appear in a `role:'tool'` result. Postures reuse the routing VOCABULARY (`assist`|`guard`|`rails`) as a SEPARATE option — routing authority ≠ evidence discipline, and overloading `skillGraphCascade.strictness` would deny "strict routing, loose evidence". Wiring is the stepNudge blast radius verbatim: `ResolvedEvidenceGate` (builder-resolved, refusals at the CALL SITE) → Agent ctor trailing param → `buildRouteDeciderStage`'s 4th arg (`judgeEvidence` runs LAST of the three judges — schema > steps > evidence — and NOT on a denied or schema-exhausted answer) → `evidenceRecheckStage` branch (`{loopTo}`, mounted only for a revising posture) → `STAGE_IDS.EVIDENCE_RECHECK` + BOUNDARY_LOCAL_IDS + milestoneFor. TWO deps flags, and the second is the one a reader misses: `evidenceRecheckStage` (branch) AND `hasEvidenceGate` (bubbles `systemPromptInjections` out of sf-llm-call in the GROUPED chart — without it the gate flags the app's own prompt). Per-check facts ride `agentfootprint.agent.evidence_checked` (emit channel); only the terminal verdict is committed (`unsupportedValues`), because the boundary raises off it. `UnsupportedValuesError` joins the TERMINAL-typed-error list in `run()`'s catch (a verdict is not a crash — no retry handle for a wall). **Grounded numbers (9.75.0)** rides the SAME dial: `nudge: true` arms the staged-refs nudge — `toolWantsOf` harvests `Tool.wants` beside the `toolGrounding` harvest in `Agent.buildChart` (same ToolProvider blindness), callLLM appends ONE request-only late line when a placed ticket's kind matches a SERVED `wants` tool (judged on `registeredToolSchemas`, so wrap-up's withheld surface arms nothing; history untouched ⇒ never in the exempt corpus), recorded as `agent.grounding_nudged`; and the recheck correction names the same refs+spender via `buildEvidenceCorrection`'s third arg — threaded whenever a `wants` tool exists, NOT gated on `nudge`, clause INSIDE the authored frame so `isLibraryAuthoredTurn`'s prefix match and values-last both hold.
137
138
  - **Coverage primitives (this release)** — `absent()` / `coverage()`, both copied from FIELD USE. THE argument is the direction of the error: a *nothing-found* misread as an *outage* costs an investigation, an *outage* misread as *nothing-found* declares a system healthy that was never checked — so the two must not share a shape. Blast radius, and it is the `resultCeiling` radius verbatim: `readCoverageResult` called by `declareCoverage` at BOTH execute boundaries in toolCalls.ts (batch loop + `resolveCredentialAndExecute`), on the UNWRAPPED content and BEFORE the ceiling. FOUR downstream changes and no more: (1) `ToolResultStatus` gained a SEVENTH word `'absent'` — routable by `onToolStatus`, because folding it into `'failure'` is the confusion itself and into `'success'` leaves nothing to route on; (2) two events (`tools.absent`, `tools.coverage_declared`); (3) tracked `AgentState.coverageDeclared` — a limit is a fact about the ANSWER, not about an attempt, which is why it is state and the repeated-call counters are not; (4) the evidence corpus indexes an absence's COVERAGE ONLY (`coverage/evidence.ts` — a failed lookup is the cheapest laundering machine, and `absent()` would have made it cheaper: this is frames.ts's argument on the tool side). Deliberately UNCHANGED: no `error: true`, no retry, no refusal, no gate flag. Survival into the answer is `.limitsTravelWithTheAnswer()` → `attachCoverageLimits` dep → BOTH builders swap the final branch's first stage for `prepareFinalWithLimitsStage` (same id, same position). It APPENDS rather than judges: a check for "did the model state its limits?" needs a second model to decide what counts, which is what evidence/README.md forbids.
138
139
  - **Out-of-budget wrap-up (9.56.0)** — `wrapUpAtMaxIterations` (AgentOptions, default ON, `repeatedCallNudge`'s opt-out grammar): the FOURTH Route branch, and the SchemaRetry mechanism verbatim — `STAGE_IDS.WRAP_UP` + same `{loopTo}`, so the last call is one ordinary turn with its own `iteration_start`/`llm_start`/`cost.tick`. Two things are its own: (1) the tools are WITHHELD at REQUEST ASSEMBLY in callLLM (`scope.wrapUpAsked`, the `schemaTool` seam's mirror — the schema tool still rides, so an output contract survives), which is what makes the call terminal BY CONSTRUCTION rather than by a rule, and is why it is exempt from `maxIterations`; (2) the CONDITIONAL MOUNT is on the agent having a TOOL SURFACE (`registryByName.size > 0 || externalToolProvider`) — a limit only cuts a turn short when tool calls were pending, so a toolless agent's chart must not grow a box that can never run. `decideBranch` treats a spent wrap-up as cut-short for every downstream judge (`toolCalls.length > 0 || scope.wrapUpAsked`), or a step nudge / evidence revision would loop past the limit that fired. `wrapUpAsked` is deliberately NOT seeded — a turn that finishes inside its budget commits the exact key set it always did. The record is FOUR channels: `stoppedEarly` (now with `wrappedUp`, corrected on the pass after so `answerWasEmpty` describes the answer the caller GOT), `cost.limit_hit` (unchanged), the new `agent.budget_exhausted {action: 'wrapped-up'|'cut-short'}`, and an optional `turn_end.stoppedEarly` projection.
139
140
  - **Closed seams**: Agent chart internals (AgentChartDeps not exported — extend via injections/tools/memory/thinking, never by adding a ReAct stage); ContextSlot (3 slots fixed); ProviderKind factory; dormant ports with no consumer (ContextSourceAdapter, EmbeddingProvider, RiskDetector — adapters/types.ts only); reserved tool names under selfExplain — 8.16.0 made the list DERIVED: `TRACE_TOOL_NAMES` (traceToolpack.ts, 11 names since 9.61.0: run_overview/find_context_errors/find_in_trace/trace_node/trace_slice/backtrack/who_wrote/get_value/inspect_tool_call/inspect_tool_run/read_narrative) is what AgentBuilder.ts:~1560 reserves in inline mode (`explain_run` in delegate mode), so a NEW toolpack tool joins ONE list and the reservation follows — but it must ALSO join the lazy template's mounted set (lazyToolpack builds over `{narrative: [], events: []}` so the catalog shape is fixed at build time) and the count assertion in test/lib/trace-toolpack/selfExplainAgent.test.ts. **8.17.0 `inspect_tool_run`** is the descent THROUGH the tool boundary: `flowchartAsTool({ keepRecord: true })` files each invocation's record in a bounded LRU store (lib/trace-toolpack/innerRunRecords.ts) keyed by the executing `ctx.toolCallId`, riding the `Tool` under the `INNER_RUN_RECORDS` registry symbol; `AgentBuilder.build()` → `collectInnerRuns(registry, injections)` → `SelfExplainSource.getInnerRuns` → `TraceToolpackArtifacts.innerRuns`. The inner views are the pack ITSELF re-run over `openRecording(record.recording)` — do not add a second query implementation. Provider-delivered tools are NOT collected (no build-time list); inner runtimeStageIds are a SEPARATE namespace and only `inspect_tool_run` accepts them. **9.61.0 `find_context_errors`** is the Context Integrity read-out: it reads `agentfootprint.integrity.context_error` + `…disposition` off the artifacts' EVENT TAIL (never re-running a check), joins each finding to the step it was filed at, and mounts UNCONDITIONALLY — a tool that vanished with the tail could not say the evidence channel is ABSENT, which is the one sentence this family forbids collapsing into "no errors found".
@@ -145,7 +146,7 @@ Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `d
145
146
  - **Embedder fingerprint** (8.9.0) → `Embedder.id` (optional; every shipped embedder sets one, and NONE include dims — the store appends `@<dims>` itself, so an id carrying its own size double-stamps) + `indexDocuments` defaulting `embedderId` to it + `SqliteVectorStore.reconcileFingerprint` (the only comparison site). Rule: dimensions ALWAYS decide, model ids decide only when BOTH sides named themselves — refusing on an absent name would block the majority of callers who never pass `embedderId`.
146
147
  - **Retrieval record** (8.8.0) → FOUR stages write one object in sequence: `loadRelevant` (candidates+scores+threshold verdicts) → `pickByBudget` (re-marks admitted→over-budget/over-max-entries) → `formatDefault` (`promptFragment` + `promptPosition`) → the read mount's outputMapper lifts it to root as `retrievalEvidence_<id>`. `memoryRecallInjections` then splits ONE recall into one ActiveInjection PER CHUNK — guarded by a byte-equality check (`fragments.join('\n\n') === systemContent`) that falls back to the single injection rather than change the prompt. `rank` (score order) and `promptPosition` (picker order) are DIFFERENT and both load-bearing: joining fragments in rank order reproduces the right bytes in a sequence the model never saw.
147
148
  - **AgentState** → all 8 stages/ files, both builders' mappers, memory-wire STRING-TYPED keys ('runIdentity'/'turnNumber'/… buildAgentChart.ts:177-180 — not refactor-safe), finalizeResult's `reliabilityFail*`/`policyHalt*` reads (rename silently kills the typed errors).
148
- - **events/** → 108 typed events across 24 domains (counts anti-drift-tested against this file — update BOTH when adding events): ALL_EVENT_TYPES exhaustiveness tests, DomainWildcard hand-list, ~42 importers (recorders, strategies, stream, commentary).
149
+ - **events/** → 109 typed events across 24 domains (counts anti-drift-tested against this file — update BOTH when adding events): ALL_EVENT_TYPES exhaustiveness tests, DomainWildcard hand-list, ~42 importers (recorders, strategies, stream, commentary).
149
150
  - **Run-configuration manifest (9.41.0)** → `agentfootprint.agent.run_configured`, the JOIN KEY that turns N runs into N labelled ARMS: one event naming the adapters/strategies in play (provider+model, reactMode, each memory's declared strategy/retrieval/embedder, window, graph posture+classifier, evidence posture, artifacts-present). Composed by the PURE `core/agent/runManifest.ts`, dispatched from `Agent.emitRunManifest()` at the END of `createExecutor` — the ONE funnel `run()` AND `resume()` share, both of which mint a fresh runId. Direct `dispatcher.dispatch` with a STATED pseudo-stage (`run-configured#0`), the `emitToolSessionReport` precedent — there is no stage yet, and `minimalMeta()` would make the one joinable-by-design event unjoinable. TWO laws, both tested: NAMES ONLY (a store is reported PRESENT and unnamed rather than identified by a directory/endpoint — `MemoryStore` and `ArtifactStore` declare no id), and ABSENT means "not configured", never a guessed `'default'`. Graph presence is read off `skillGraphNextSkill`, NOT `skillGraphCascade` (a 9.16-style mount sets no cascade and would read as "no graph"). `MemoryDefinition` gained `strategy`/`retrieval`/`embedderId` for it — declared names the compiled pipeline had closed over, the `store`-in-the-open precedent.
150
151
  - **adapters/types.ts LLMMessage/LLMRequest** → 62 importers: tool_use round-trip (toolCalls.ts:115-135), wire assembly (callLLM.ts:150-160), providers, cache strategies, security/extractSequence, reliability loop.
151
152
  - **Cache** → strategy registration is a MODULE SIDE EFFECT (src/index.ts:15-17); an entry point skipping that import silently falls back to NoOp. Resolved once per Agent at construction (Agent.ts:347).
@@ -252,7 +252,7 @@ const agent = Agent.create({ provider, model })
252
252
  agent.on('agentfootprint.context.evaluated', (e) => console.log(e.payload.activeIds));
253
253
  ```
254
254
 
255
- **108 typed events across 24 domains.** Two subscription shapes and no third:
255
+ **109 typed events across 24 domains.** Two subscription shapes and no third:
256
256
  `'*'` (every event) and `'agentfootprint.<domain>.*'` (one domain). **`'agentfootprint.*'`
257
257
  is not a pattern** — TypeScript rejects it, and at runtime it would match nothing.
258
258
 
@@ -0,0 +1,306 @@
1
+ "use strict";
2
+ /**
3
+ * entraIdentity — the {@link CredentialProvider} port over Microsoft Entra ID
4
+ * (peer-dep `@azure/identity`).
5
+ *
6
+ * import { entraIdentity } from 'agentfootprint/security';
7
+ * const credentials = entraIdentity();
8
+ *
9
+ * ── What it is, and what it deliberately is not ─────────────────────────────
10
+ * This is the **narrow** adapter: it vends *Entra* access tokens for *Azure*
11
+ * APIs, from whatever credential the environment already has — the
12
+ * DefaultAzureCredential chain walks environment service principal, workload
13
+ * identity, managed identity, VS Code, Azure CLI, Azure PowerShell and the
14
+ * Azure Developer CLI, in that order. That is one job and it is done
15
+ * completely.
16
+ *
17
+ * It is **not** a user-delegation surface. Entra's on-behalf-of flow (and any
18
+ * 3-legged consent dance) needs a confidential client app registration that
19
+ * this adapter does not hold, so `mode: 'user'` is **refused by name** rather
20
+ * than quietly served with a machine token. A machine token returned where a
21
+ * user token was asked for is the exact silent downgrade the port exists to
22
+ * prevent: the call succeeds, the data comes back, and it was the agent's
23
+ * access rather than the person's. OBO is a later train; when it lands it will
24
+ * be its own provider, not a flag here.
25
+ *
26
+ * ── The audience split, and where it bites ──────────────────────────────────
27
+ * Azure tokens are minted for ONE audience. {@link AZURE_AI_SCOPE}
28
+ * (`https://ai.azure.com/.default`) is the data plane — every Foundry and
29
+ * Azure OpenAI inference call takes it. {@link AZURE_MANAGEMENT_SCOPE}
30
+ * (`https://management.azure.com/.default`) is the ARM control plane —
31
+ * listing deployments, creating resources. A token for one audience is a 401
32
+ * on the other, which is why BOTH are exported by name instead of leaving the
33
+ * caller to guess a string. The default here is the data-plane scope, because
34
+ * vending inference credentials is what an agent runtime does all day.
35
+ *
36
+ * ── Caching: the credential, never a token ──────────────────────────────────
37
+ * One `DefaultAzureCredential` is constructed for the life of the provider and
38
+ * every `getToken` call goes through it. MSAL — the machinery underneath
39
+ * `@azure/identity` — caches and proactively refreshes tokens internally, so
40
+ * caching a token HERE would mean owning an expiry this adapter did not
41
+ * compute and cannot see revoked. Unlike the Google adapter (where scopes are
42
+ * fixed at client construction and a different scope set needs its own
43
+ * client), Azure scopes travel per `getToken` call, so the ONE cached
44
+ * credential serves every scope set.
45
+ *
46
+ * ── Secrets ─────────────────────────────────────────────────────────────────
47
+ * The `sdkFailure` law, same as every other credential-touching adapter here:
48
+ * the library's own message never comes through, because auth libraries echo
49
+ * request detail into 401/403 text and a message thrown from a
50
+ * `CredentialProvider` reaches the LLM as a tool result AND rides
51
+ * `agentfootprint.credential.failed` to every sink. What comes through is the
52
+ * operation that failed and the error's NAME. The original is not attached as
53
+ * `cause` — a cause travels into every serializer that walks own properties,
54
+ * which would undo all of it in one `JSON.stringify`.
55
+ *
56
+ * Pattern: Adapter (GoF) + lazy peer-dep load — `@azure/identity` is required
57
+ * the first time `getCredential` runs, or never if you inject a credential.
58
+ */
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.entraIdentity = exports.AZURE_COGNITIVE_SERVICES_SCOPE = exports.AZURE_MANAGEMENT_SCOPE = exports.AZURE_AI_SCOPE = void 0;
61
+ const lazyRequire_js_1 = require("../../lib/lazyRequire.js");
62
+ const kinds_js_1 = require("../../identity/kinds.js");
63
+ const ADAPTER = 'entraIdentity';
64
+ /**
65
+ * The data-plane scope for ALL Foundry / Azure OpenAI inference
66
+ * (`https://ai.azure.com/.default`). This is the default scope this provider
67
+ * requests.
68
+ *
69
+ * The audience split matters: a token minted for this scope does NOT work on
70
+ * the ARM control plane, and a {@link AZURE_MANAGEMENT_SCOPE} token does not
71
+ * work here — Azure validates the audience on every call. Both are exported by
72
+ * name so nobody has to remember which string is which.
73
+ */
74
+ exports.AZURE_AI_SCOPE = 'https://ai.azure.com/.default';
75
+ /**
76
+ * The ARM control-plane scope (`https://management.azure.com/.default`) —
77
+ * listing deployments, managing resources. A DIFFERENT audience from
78
+ * {@link AZURE_AI_SCOPE}: a token for one is a 401 on the other, which is why
79
+ * both are named rather than leaving the caller to guess.
80
+ */
81
+ exports.AZURE_MANAGEMENT_SCOPE = 'https://management.azure.com/.default';
82
+ /**
83
+ * The CLASSIC Azure OpenAI data-plane scope
84
+ * (`https://cognitiveservices.azure.com/.default`) — the audience Microsoft's
85
+ * own keyless guidance names for the older deployment-scoped route
86
+ * (`{endpoint}/openai/deployments/{d}/…`), which is the route `azureOpenai()`
87
+ * builds and therefore its default. Current resources widely accept
88
+ * {@link AZURE_AI_SCOPE} too, but an older `*.openai.azure.com` resource may
89
+ * not — and a door should default to the audience ITS route documents, not the
90
+ * one its sibling uses. (Azure Government spells this
91
+ * `https://cognitiveservices.azure.us/.default`.)
92
+ */
93
+ exports.AZURE_COGNITIVE_SERVICES_SCOPE = 'https://cognitiveservices.azure.com/.default';
94
+ /**
95
+ * Vend Entra access tokens from whatever credential this environment has —
96
+ * the DefaultAzureCredential chain: environment service principal, workload
97
+ * identity, managed identity, VS Code, Azure CLI, Azure PowerShell, Azure
98
+ * Developer CLI. (`AZURE_TOKEN_CREDENTIALS` can restrict the chain; that is
99
+ * the SDK's own dial and this adapter does not second-guess it.)
100
+ *
101
+ * @throws when `mode: 'user'` is requested — no user-delegation surface is
102
+ * wired for Entra yet (on-behalf-of is a later train), and a machine token
103
+ * returned in its place would be a silent downgrade.
104
+ * @throws when `services` is configured and the request names another one.
105
+ *
106
+ * @example A tool that calls an Azure API with the deployment's own identity
107
+ * const agent = Agent.create({ provider, credentials: entraIdentity() })
108
+ * .tool(defineTool({
109
+ * name: 'ask_foundry',
110
+ * needs: [{ credential: 'azure-ai' }],
111
+ * execute: async (args, ctx) =>
112
+ * fetch(url, { headers: ctx.credential!.toHeaders() }).then((r) => r.text()),
113
+ * }))
114
+ * .build();
115
+ *
116
+ * @example A control-plane token, without touching the data-plane default
117
+ * entraIdentity({ scopes: [AZURE_MANAGEMENT_SCOPE] });
118
+ */
119
+ function entraIdentity(options = {}) {
120
+ const defaultScopes = options.scopes ?? [exports.AZURE_AI_SCOPE];
121
+ const allowed = options.services === undefined ? undefined : new Set(options.services);
122
+ const cache = {};
123
+ const resolveCredential = () => {
124
+ if (options._credential)
125
+ return options._credential;
126
+ // ONE credential for the life of the provider — scopes ride on getToken
127
+ // per call (unlike Google, where they are fixed at client construction),
128
+ // so no per-scope keying is needed. MSAL underneath caches and refreshes
129
+ // tokens on its own; we cache the CREDENTIAL, never a token.
130
+ if (cache.credential)
131
+ return cache.credential;
132
+ const mod = loadIdentitySdk(options._sdk);
133
+ if (typeof mod.DefaultAzureCredential !== 'function') {
134
+ throw new Error(`${ADAPTER}: \`@azure/identity\` is installed but exports no ` +
135
+ `\`DefaultAzureCredential\`. This adapter is built against the 4.x package — ` +
136
+ `update it, or pass \`_credential\`.`);
137
+ }
138
+ cache.credential = new mod.DefaultAzureCredential();
139
+ return cache.credential;
140
+ };
141
+ return {
142
+ id: options.id ?? 'entra-identity',
143
+ async getCredential(req) {
144
+ if (req.mode === 'user') {
145
+ throw new Error(`${ADAPTER}: a \`mode: 'user'\` request arrived for '${req.service}', and this ` +
146
+ `provider cannot serve one.\n` +
147
+ ` It vends the DEPLOYMENT's Entra credential (the DefaultAzureCredential ` +
148
+ `chain — environment service principal, workload identity, managed identity, ` +
149
+ `or a developer's \`az login\`). No user-delegation surface is wired for Entra ` +
150
+ `yet — the on-behalf-of flow is a later train.\n` +
151
+ ` Returning a machine token here would succeed and be wrong: the call would ` +
152
+ `run with the AGENT's access rather than the person's, and nothing downstream ` +
153
+ `could tell.\n` +
154
+ ` Fix: declare \`mode: 'machine'\` if the deployment's own identity is really ` +
155
+ `what you want, or vend the user's token from a provider that holds one.`);
156
+ }
157
+ if (req.userToken !== undefined) {
158
+ // A user's signed token handed to a provider that cannot exchange it.
159
+ // Named rather than ignored: without a confidential client app
160
+ // registration there is no on-behalf-of exchange to perform, and
161
+ // silently dropping somebody's proof and vending machine access is
162
+ // the same downgrade in a quieter costume.
163
+ throw new Error(`${ADAPTER}: a \`userToken\` arrived for '${req.service}', but this provider has ` +
164
+ `nothing to exchange it against — the on-behalf-of flow needs a confidential ` +
165
+ `client app registration, and this provider vends the deployment's own Entra ` +
166
+ `credential.\n` +
167
+ ` Ignoring it would hand back agent-scoped access while holding the user's ` +
168
+ `proof. Drop the token, or use a provider that can exchange one.`);
169
+ }
170
+ if (allowed !== undefined && !allowed.has(req.service)) {
171
+ throw new Error(`${ADAPTER}: this provider is configured for [${[...allowed].join(', ')}] and was ` +
172
+ `asked for '${req.service}'.\n` +
173
+ ` It vends ENTRA access tokens; handing one to a tool that wanted a different ` +
174
+ `service's credential would fail downstream as a puzzling 401 instead of here ` +
175
+ `as a wiring error.\n` +
176
+ ` Fix: add '${req.service}' to 'services', or attach a provider that serves it.`);
177
+ }
178
+ let credential;
179
+ try {
180
+ credential = resolveCredential();
181
+ }
182
+ catch (err) {
183
+ // A refusal this adapter authored (a missing peer dependency, a
184
+ // too-old SDK) is already the right diagnosis; rewriting it through
185
+ // sdkFailure would send the reader chasing Entra sign-in logs for a
186
+ // problem `npm install` fixes. Wrong diagnoses are their own kind of
187
+ // silently-wrong.
188
+ if (isOwnRefusal(err))
189
+ throw err;
190
+ throw sdkFailure('new DefaultAzureCredential', err);
191
+ }
192
+ // A request's own non-empty scopes win over the provider's default.
193
+ // The array goes to getToken as-is — Azure scopes are per-call, and
194
+ // `.default` scopes are single-element by convention anyway.
195
+ const scopes = req.scopes !== undefined && req.scopes.length > 0 ? req.scopes : defaultScopes;
196
+ let answer;
197
+ try {
198
+ answer = await credential.getToken(scopes);
199
+ }
200
+ catch (err) {
201
+ throw sdkFailure('getToken', err);
202
+ }
203
+ const token = answer?.token;
204
+ if (typeof token !== 'string' || token.trim() === '') {
205
+ // The SCOPES are quoted; nothing else is. An audience URI is public,
206
+ // and it is the datum most often wrong here — "could not mint for the
207
+ // requested scope" is useless when the reader cannot see WHICH scope
208
+ // was requested (a tool that declares `needs` without `scopes` gets
209
+ // this provider's default, which it never typed anywhere). The token
210
+ // response's own fields stay withheld: every one of them is a secret.
211
+ // Same conclusion `entraBearerToken` reached out loud in
212
+ // src/adapters/llm/OpenAIProvider.ts — the two siblings now agree.
213
+ throw new Error(`${ADAPTER}: the credential resolved but vended no access token for ` +
214
+ `'${req.service}' at scope${scopes.length === 1 ? '' : 's'} ` +
215
+ `[${scopes.join(', ')}] — ${answer === null
216
+ ? '`getToken` returned null, the SDK\'s spelling of "no token available"'
217
+ : "the response's `token` field was empty"}.\n` +
218
+ ` No token value is quoted here on purpose — every field of a token response is ` +
219
+ `a secret. This usually means the chain found a credential source that could not ` +
220
+ `actually mint for THAT audience.\n` +
221
+ ` Fix: check the audience first — inference is ${exports.AZURE_AI_SCOPE} and the ARM ` +
222
+ `control plane is ${exports.AZURE_MANAGEMENT_SCOPE}; name the right one in the request's ` +
223
+ `\`scopes\` or this provider's \`scopes\` option. If it is already right, sign in as ` +
224
+ `an identity that can mint for it (\`az login\`, or a managed identity with the role).`);
225
+ }
226
+ // The SDK records expiry in unix MILLISECONDS; the port reports unix
227
+ // SECONDS. Reported when known and omitted when not — an invented
228
+ // expiry is worse than none, because a caller would cache against it.
229
+ // (`answer` cannot be null past the token guard; the optional chain is
230
+ // for the compiler, which does not carry the narrowing across fields.)
231
+ const expiryMs = answer?.expiresOnTimestamp;
232
+ const expiresAt = typeof expiryMs === 'number' && Number.isFinite(expiryMs) && expiryMs > 0
233
+ ? Math.floor(expiryMs / 1000)
234
+ : undefined;
235
+ return {
236
+ status: 'issued',
237
+ credential: (0, kinds_js_1.bearer)(token),
238
+ ...(expiresAt !== undefined && { expiresAt }),
239
+ };
240
+ },
241
+ };
242
+ }
243
+ exports.entraIdentity = entraIdentity;
244
+ // ─── Internals ───────────────────────────────────────────────────────
245
+ function loadIdentitySdk(injected) {
246
+ if (injected)
247
+ return injected;
248
+ try {
249
+ return (0, lazyRequire_js_1.lazyRequire)('@azure/identity');
250
+ }
251
+ catch {
252
+ throw new Error(`${ADAPTER} requires the \`@azure/identity\` peer dependency.\n` +
253
+ ` Install: npm install @azure/identity\n` +
254
+ ` It is optional and loaded only when this provider first vends, so nothing else ` +
255
+ `in this library pays for it.`);
256
+ }
257
+ }
258
+ /**
259
+ * "There is no usable credential here" — the most common real failure, given
260
+ * the fix instead of the library's own text. `@azure/identity` names it
261
+ * `CredentialUnavailableError` (one rung) or `AggregateAuthenticationError`
262
+ * (the whole chain came up empty); both mean the same thing to the reader.
263
+ */
264
+ function credentialsUnavailable(name) {
265
+ const failure = new Error(`${ADAPTER}: could not acquire an Entra token in this environment — ${name}.\n` +
266
+ ` The underlying message is withheld: auth libraries echo request detail into ` +
267
+ `failure text, and this message reaches the model as a tool result.\n` +
268
+ ` Every rung of the DefaultAzureCredential chain was tried: environment service ` +
269
+ `principal, workload identity, managed identity, VS Code, Azure CLI, Azure ` +
270
+ `PowerShell, Azure Developer CLI.\n` +
271
+ ` Fix: run \`az login\`, or set AZURE_CLIENT_ID / AZURE_TENANT_ID / ` +
272
+ `AZURE_CLIENT_SECRET, or run where a managed identity exists, or pass \`_credential\`.`);
273
+ failure.name = 'AzureCredentialsUnavailableError';
274
+ return failure;
275
+ }
276
+ /** Re-raise without the library's text. See the module header for why. */
277
+ function sdkFailure(operation, err) {
278
+ const name = errorName(err);
279
+ // The no-credential names get the fix, not just the diagnosis — but ONLY
280
+ // those names. Everything else is reported as what it is.
281
+ if (name === 'CredentialUnavailableError' || name === 'AggregateAuthenticationError') {
282
+ return credentialsUnavailable(name);
283
+ }
284
+ const failure = new Error(`${ADAPTER}: ${operation} failed — ${name}.\n` +
285
+ ` The underlying message is withheld: this call handles an access token, and auth ` +
286
+ `libraries echo request detail into failure text. Check the Entra sign-in logs for ` +
287
+ `the full error.`);
288
+ failure.name = 'AzureCredentialError';
289
+ return failure;
290
+ }
291
+ /**
292
+ * Did THIS adapter write this error?
293
+ *
294
+ * Every refusal authored here opens with the adapter's own name — both as the
295
+ * marker and because it is what makes the message readable ("entraIdentity:
296
+ * …", "entraIdentity requires …"). A library's own failure never does, so the
297
+ * prefix is a reliable discriminator without an error subclass per refusal.
298
+ */
299
+ function isOwnRefusal(err) {
300
+ return err instanceof Error && err.message.startsWith(ADAPTER);
301
+ }
302
+ function errorName(err) {
303
+ const name = err?.name;
304
+ return typeof name === 'string' && name.length > 0 ? name : 'an unnamed failure';
305
+ }
306
+ //# sourceMappingURL=azure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"azure.js","sourceRoot":"","sources":["../../../src/adapters/identity/azure.ts"],"names":[],"mappings":"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"}