Skip to content

setup blueprint: agentBlueprintClientSecret persists as null on macOS despite successful credential creation #408

@satscryption

Description

@satscryption

setup blueprint: agentBlueprintClientSecret persists as null on macOS despite successful credential creation

Summary

a365 setup blueprint --agent-name <name> [--m365] mints a client secret on the blueprint Entra app successfully (verified server-side via az ad app credential list), but the local a365.generated.config.json ends up with agentBlueprintClientSecret: null. The CLI logs the secret-write step as a success, so callers depending on the local config see "success → null secret" with no error signal.

This forces every subsequent CLI command that needs the secret (any local-runtime / update-endpoint / agent bridge work) to fail until the operator manually mints a replacement credential and patches the JSON.

Repro

$ a365 --version
1.1.174+25970fb6e6

$ rm -f a365.generated.config.json   # start clean
$ a365 setup blueprint --agent-name "Hermes Inbox Helper R6" --m365
…
    Creating blueprint client secret...
        DPAPI encryption not available on this platform. Secret will be stored in plaintext.
        Client secret created successfully!
          - Secret stored in generated config (encrypted: False)
        IMPORTANT: The client secret has been stored in a365.generated.config.json
…
(setup blueprint completes with exit code 0)

$ python3 -c "import json; print(json.load(open('a365.generated.config.json'))['agentBlueprintClientSecret'])"
None

$ az ad app credential list --id <agentBlueprintId> -o table
KeyId                                 DisplayName              StartDate            EndDate
<id>                                  hermes-bridge-... 2y      2026-05-07T15:35:33Z  2028-05-07T15:35:33Z

So the credential exists on the Entra app — only the local persistence is broken.

Expected

The CLI either:

  • writes the freshly-minted password to agentBlueprintClientSecret in a365.generated.config.json (the success log says it did), or
  • emits a non-zero exit and a clear error if it cannot write (DPAPI-fallback failure, permissions, etc.).

Observed

  • Exit code: 0
  • Log lines (verbatim, in order):
    Creating blueprint client secret...
        DPAPI encryption not available on this platform. Secret will be stored in plaintext.
        Client secret created successfully!
          - Secret stored in generated config (encrypted: False)
        IMPORTANT: The client secret has been stored in a365.generated.config.json
    
  • On disk:
    {
      "agentBlueprintId": "<set>",
      "agentBlueprintClientSecret": null,
      "agentBlueprintClientSecretProtected": false,
      "cliVersion": "1.1.174.9623",
      "completed": false,
      
    }

completed: false is also notable: setup blueprint exits 0 but doesn't seal the file as completed. (May or may not be related — flagging in case it points at the same code path.)

Reliability

Reproduces 100% across four consecutive walkthroughs:

Round Date CLI version Result
3 2026-05-05 1.1.171.4547 secret null on disk; manual recovery
4 2026-05-05 1.1.171.4547 same
5 2026-05-06 1.1.171.4547 same
6 2026-05-07 1.1.174.9623 same — 1.1.174 did not fix this

Environment

  • Host: macOS 26.4.1 (Apple Silicon)
  • .NET runtime: dotnet --version 9.x (Homebrew install, DOTNET_ROOT=/opt/homebrew/opt/dotnet/libexec)
  • a365 CLI version (NuGet identifier): 1.1.174+25970fb6e6
  • a365 CLI version (runtime, cliVersion in generated config): 1.1.174.9623
  • Tenant: M365 with Frontier Preview Program + Microsoft Agent 365 Tier 3 license
  • Az CLI: 2.x signed in to the same tenant, Global Administrator role

Likely cause (from the log line)

DPAPI encryption not available on this platform. Secret will be stored in plaintext. — the CLI correctly identifies that DPAPI is Windows-only and chooses the plaintext-write fallback path. The bug appears to be in that fallback path: it probably writes the generated config before the secret value is bound, or writes the in-memory dataclass without the freshly-minted password, then never overwrites with the populated value.

The contradiction Client secret created successfully! / Secret stored in generated config / agentBlueprintClientSecret: null strongly suggests the success-log fires unconditionally regardless of whether the persistence succeeded.

Recovery (the manual workaround we run on every walkthrough)

az ad app credential reset --id <agentBlueprintId> --append \
    --display-name "hermes-bridge-rN" --years 2 -o json
# patch the resulting `.password` into a365.generated.config.json
# at the agentBlueprintClientSecret key
chmod 600 a365.generated.config.json

We've automated this in our wrapper at satscryption/Hermes-A365#14 as a stop-gap. The wrapper detects the agentBlueprintId set + agentBlueprintClientSecret null post-condition after setup blueprint and either prints a paste-ready recovery hint or, with an opt-in flag, runs the az recovery + patches the generated config + tightens to mode 0600.

Suggested fixes

  1. Persistence: in the non-DPAPI path, ensure the freshly-minted password is bound to the in-memory generated-config object before the file is written (or write again after the credential mint completes).
  2. Logging: don't emit "Client secret created successfully!" / "Secret stored in generated config" / "IMPORTANT: The client secret has been stored…" unless the post-write read-back confirms the value is present. If the write didn't take, exit non-zero with a clear message.
  3. Sealing: investigate whether the same flow that loses the secret also leaves completed: false.

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions