Skip to content

feat: platform.yaml — declarative config + multi-account strategies - #17

Merged
Robobc merged 4 commits into
mainfrom
feat/platform-config
Aug 18, 2026
Merged

feat: platform.yaml — declarative config + multi-account strategies#17
Robobc merged 4 commits into
mainfrom
feat/platform-config

Conversation

@Robobc

@Robobc Robobc commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

What

Workshop users describe their whole deployment in one platform.yaml, including the multi-account strategy (centralized | distributed | federated). Three commits, one story:

  1. Schema — pydantic models, five presets, validator CLI (no behavior change)
  2. Consumptionapp.py + deploy.sh read the file
  3. Federated strategy — verified live across two AWS accounts

Without the file, nothing changes — synth is byte-identical to main (template diff + re-confirmed 6-stack baseline).

The user experience

deployment:
  strategy: federated
  platform_account: "111122223333"
  workload_accounts: ["444455556666"]
  federation:        # four platform outputs; the secret NAME, never a value
    gateway_url: https://...
    issuer_url: https://cognito-idp...
    m2m_client_id: ...
    m2m_client_secret_name: agentcore/platform-m2m
agents:
  pattern: strands-agent
  • All validation errors in ONE pass, before any AWS call; typo'd keys are errors.
  • The same file deploys both sides — the account decides the role. Platform: auth/identity/gateway/observability. Workload: identity + memory + runtimes + observability.
  • Changing the workload's agent pattern was a two-line yaml edit + redeploy (done live).

The architectural finding (docs/MULTI_ACCOUNT.md)

Cross-account trust is pure OAuth — zero cross-account IAM on the data plane. The workload account's own token vault exchanges the platform Cognito M2M credentials over HTTPS; the gateway validates the JWT against its own issuer. Memory stays per-workload by design (actor_id is the tenant boundary; account isolation is the strongest wall).

Verified live (two accounts)

  • Workload strands agent → own credential provider → platform Cognito token → platform gateway web search → cited answer.
  • Workload runtime's JWT authorizer accepts the platform-issued token (SigV4 invoke correctly rejected).
  • Workload deploy UPDATE_COMPLETE for all four role stacks; platform-role synth shows exactly the shared-services set.
  • Bonus fix the live deploy flushed out: TraceSegmentDestination failed with InvalidRequestException in an account where the destination was already CloudWatchLogs — now idempotent, with check_observability.py check 1 as the backstop (the ignore matcher only sees error codes).

Verified (local)

pytest 98 passed (28 new across the three slices) · ruff + shellcheck clean · check-deploy-config.sh 13 checks incl. precedence both ways and invalid-file refusal · no-file synth equivalence.

…idation

First slice of the declarative-config work (PR A of three): the schema, five
presets, and a validator CLI. No behavior change — nothing consumes the file
yet (PR B wires app.py + deploy.sh; PR C implements the federated strategy).

- infra_utils/platform_config.py: pydantic models for the whole deployment —
  project/env/region, multi-account strategy (centralized | distributed |
  federated), IdP, agent pattern + memory, gateway tools + web_search auto
  (resolves against the connector's launch regions), all security controls,
  observability. extra="forbid": a typo'd key is an error, not a silent no-op.
- Validation ACCUMULATES: a file with three problems reports three errors in
  one pass, before any AWS call. Cross-field rules encode real deployment
  constraints learned live: federated requires both account fields, a
  non-cognito IdP requires the Secrets-Manager secret NAME (never a value —
  the schema deliberately has no field that could hold one), traceability
  without cloudtrail_alerting is silence (TESTING.md caveat 5).
- presets/*.yaml: the five workshop profiles as real files users copy, and as
  validated fixtures the future snapshot harness will synth against.
- Validate without AWS: python -m infra_utils.platform_config <file>
- requirements.txt: pydantic>=2.7,<3 (the one new dependency; named by the
  board task and pinned below the next major).

Verified:
- pytest 90 passed (20 new): every preset validates, the three-error file
  reports all three, typo'd keys rejected, federated/account/traceability/
  secret-name rules exercised both ways, web_search auto x region matrix,
  empty file = valid greenfield defaults, and a guard that fails if anyone
  adds a schema field that could hold a secret value.
- CLI: greenfield preset prints the effective config; a broken file prints
  all three errors and exits 1.
- ruff check + format clean.
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Commit: 6602af5 | Updated: 2026-08-16 21:14:37 UTC

Security Scan Results

Scanner S C H M L I Time Action Result Thresh
bandit 0 0 0 0 40 0 405ms 0 PASSED MED (g)
cdk-nag 0 0 0 0 0 0 6.4s 0 PASSED MED (g)
cfn-nag 0 0 0 0 0 0 11ms 0 PASSED MED (g)
checkov 0 0 0 0 0 0 4.7s 0 PASSED MED (g)
detect-secrets 0 0 0 0 0 0 999ms 0 PASSED MED (g)
grype 0 0 0 0 0 0 48.5s 0 PASSED MED (g)
npm-audit 0 0 0 0 0 0 239ms 0 PASSED MED (g)
opengrep 0 0 0 0 0 0 14.1s 0 PASSED MED (g)
semgrep 0 0 0 0 0 0 <1ms 0 MISSING MED (g)
syft 0 0 0 0 0 0 1.7s 0 PASSED MED (g)

Second slice (PR B): the config file now drives deployments. No file, no
change — synth output without platform.yaml is byte-identical to main
(verified by diffing templates).

- app.py: one cfg() resolver replaces ~20 hand-rolled lookups.
  Precedence: cdk context > env var > platform.yaml > legacy defaults.
  The file path comes from -c platform_config / $PLATFORM_CONFIG, default
  ./platform.yaml. An invalid file FAILS the synth with every error listed —
  deploying defaults the user didn't write would be worse than stopping.
  Deliberate nuance: with no file, legacy defaults hold (ENABLE_A2A=true);
  with a file, the file's schema defaults decide (a2a: false unless stated) —
  the file is the user's complete intent, not a patch.
  OAuth provider credentials stay env/context-only: secrets never enter the
  file, and the static guard from PR A keeps the schema that way.
- infra_utils/platform_config.py: to_env() maps the schema onto the env-var
  names deploy.sh/app.py already use (empty values omitted so fill-if-unset
  logic can't be clobbered by ""); --export prints them for the shell.
- deploy.sh: apply_platform_config() between explicit env and workshop.env —
  a user-authored file beats remembered wizard answers. Fail-soft before the
  venv exists (app.py hard-validates at synth), but a file that parses as
  INVALID stops the run. `deploy.sh config` now prints the yaml-derived
  values and the wizard answers, labeled with their precedence.

Verified:
- Synth equivalence: gateway template with the change and no platform.yaml
  is byte-identical to main's.
- Preset drives the app: PLATFORM_CONFIG=presets/security-focused.yaml flips
  cdk ls from 6 stacks to 8 (networking + security) with zero env vars.
- Invalid file: cdk ls fails listing both errors; check-deploy-config.sh (j)
  pins the same for deploy.sh.
- check-deploy-config.sh (i): explicit env survives, platform.yaml beats
  workshop.env, still-unset keys fall through — all three asserted.
- pytest 93 passed (3 new: to_env mapping, empty-value omission, and a static
  guard failing on any app.py lookup that bypasses cfg()).
- ruff + shellcheck: no new findings.
@Robobc Robobc changed the title feat: platform.yaml schema — declarative config with accumulating validation feat: platform.yaml — declarative deployment config (schema + consumption) Aug 16, 2026
…unts

Third slice (PR C): deployment.strategy=federated works end to end. One
platform.yaml deploys both sides — the account you deploy into decides the
role. Verified with a real two-account deployment:

  workload agent (045129524125, strands pattern)
    → own credential provider (account-local token vault)
    → platform Cognito M2M token (066523631817)
    → platform gateway tools/call (web search)
    → cited answer.

The architectural finding that shapes everything: cross-account trust is pure
OAuth. No cross-account IAM exists anywhere on the data plane — the workload
vault exchanges platform client credentials over HTTPS, and the gateway
validates the JWT against its own issuer. docs/MULTI_ACCOUNT.md carries the
full reference architecture, the handoff procedure, and the honest table of
what each strategy shares.

- platform_config.py: FederationConfig (the four platform endpoints a workload
  consumes — none secret; the M2M secret goes in the workload account's OWN
  Secrets Manager under m2m_client_secret_name) + federated_role(account),
  which hard-errors when a federated file is deployed from an account named
  in neither list.
- app.py: role-gated stack graph. Platform: auth, identity, gateway,
  observability — no runtimes, no memory. Workload: identity (provider built
  from federation creds via SecretValue.secrets_manager), memory, runtimes,
  observability. Memory is per-workload BY DESIGN: actor_id is the tenant
  boundary and account isolation is the strongest wall available.
  Workload synth fails with a task-list error when the federation block is
  incomplete. Centralized/distributed paths untouched: no-file cdk ls still
  yields exactly the 6 baseline stacks.
- observability_stack.py: TraceSegmentDestination is now idempotent —
  deploying into an account where the destination is already CloudWatchLogs
  failed live with InvalidRequestException ("already set"). Ignoring that
  code is broader than the message (the SDK matcher only sees codes); the
  backstop is check_observability.py check 1 in MODULE_VERIFY[9].
- docs/MULTI_ACCOUNT.md: strategy comparison, trust diagram, deploy handoff.

Verified:
- Live two-account: platform role synth (auth/identity/gateway/observability),
  workload deploy (identity/memory/runtimes/observability, UPDATE_COMPLETE),
  the E2E invoke above, and the runtime authorizer accepting the
  platform-issued JWT (SigV4 invoke correctly rejected with "Authorization
  method mismatch").
- Config-file UX exercised for real: switching the workload agent from the
  tool-less orchestrator to strands-agent was a two-line platform.yaml edit
  plus redeploy.
- pytest 98 passed (5 new: role mapping, wrong-account hard error,
  federation completeness + derived discovery URL, and a static guard that
  fails if the stack graph stops consulting the role).
- ruff clean; no-file synth equivalence re-confirmed (6 stacks).
@Robobc Robobc changed the title feat: platform.yaml — declarative deployment config (schema + consumption) feat: platform.yaml — declarative config + multi-account strategies Aug 16, 2026
ASH's detect-secrets flagged 4 findings on this PR. All are false positives
by design: two are Secrets Manager NAMES (the schema deliberately has no
field that can hold a secret value — a test enforces it), and two are the
check script's own throwaway test inputs, one of which exists precisely to
prove secrets are never persisted to workshop.env. Annotated with
'pragma: allowlist secret' so the scanner records the intent instead of
being suppressed. detect-secrets scan over the PR's changed files: 0
findings after annotation; all checks still pass.
@Robobc
Robobc merged commit 256e526 into main Aug 18, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant