feat: platform.yaml — declarative config + multi-account strategies - #17
Merged
Conversation
…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.
|
Commit: Security Scan Results
|
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.
…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).
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Workshop users describe their whole deployment in one
platform.yaml, including the multi-account strategy (centralized | distributed | federated). Three commits, one story:app.py+deploy.shread the fileWithout the file, nothing changes — synth is byte-identical to main (template diff + re-confirmed 6-stack baseline).
The user experience
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)
TraceSegmentDestinationfailed with InvalidRequestException in an account where the destination was already CloudWatchLogs — now idempotent, withcheck_observability.pycheck 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.