This is the canonical reference. Non-negotiable doctrines are guardrails (PRs violating them are rejected). Default doctrines can be overridden with a written rationale. Each entry has three parts: the rule (imperative sentence), the why (failure mode it prevents), and how to apply (where in the codebase it shows up).
The safety floor every cartridge inherits.
Why. Egress allowlist is the only practical defense against data exfiltration from a compromised cartridge. Four systems of record (Salesforce, Monday, GitHub, GitBook) are the only outside destinations a Lambda can reach. Anything else is a foundation PR with explicit rationale.
How to apply. New external destinations require updates to substrate/modules/platform/network/. Cartridges do not add their own egress rules.
Why. The bootstrap permission boundary prevents categories of escalation no matter what a cartridge role asks for. Layering app-level checks on top is redundant; bypassing is impossible.
How to apply. Every aws_iam_role gets permissions_boundary = var.permission_boundary_arn.
Why. AuthType = NONE exposes Lambdas without authentication, breaking the three-layer defense. SCP-denied at the org level so the resource type cannot be created even by an admin.
How to apply. Every Lambda invocation goes through API Gateway HTTP API + X-Origin-Secret + JWT, fronted by CloudFront.
Why. Compromise of an agent role must not leak PII; compromise of an audit role must not allow tampering. Three keys structurally enforce three trust boundaries.
How to apply. Agent CMK, Lockbox CMK, Audit CMK declared in substrate/modules/platform/identity/. Cartridges declare which cell they consume (see Step 4.5.a); they do not pick keys.
Why. Auditors and incident responders need to answer "who did what" without trusting application code. CloudTrail data events + immutable audit bucket + GuardDuty give that answer.
How to apply. CloudTrail data events on for S3, KMS, Secrets Manager. Audit bucket: Object Lock, no delete.
Why. AWS-managed keys are fine for a hobby project. PayCargo needs the audit trail of every decrypt call and the ability to revoke decrypt without redeploying.
How to apply. Every CMK is aws_kms_key with an explicit key policy. No aws/...-prefixed AWS-managed key paths.
Why. Observe what production traffic looks like before declaring what is an anomaly. Skipping the alert phase produces false positives and operator distrust of the firewall.
How to apply. New egress rules ship in alert mode first. After clean alert logs, the rule flips to drop. See Step 5.
Why. AES-256 at rest is quantum-resistant per current consensus. TLS-in-transit is on AWS's hybrid post-quantum rollout. We do not roll our own crypto, so we inherit the migration.
How to apply. Symmetric encryption only for our own choices (AES-256-GCM). Asymmetric stays on AWS surfaces. See Step 4.5.a section 12.
Why. Reachability is not access. A cartridge Lambda in the agent subnets must not be able to touch another tier's tables even if its IAM slips — so the DynamoDB/S3 gateway VPC-endpoint policy denies cross-tier table access at the network layer, before the DDB API is ever consulted. It has bitten the project three times: a missing action (Scan, DeleteItem) or a missing table pattern makes calls fail with no local reproduction (the dev harness has no VPC).
How to apply. substrate/modules/platform/data/gateway-endpoint-policies.tf scopes each subnet tier to its own prefix — agent → <project>-agent-*, lockbox → <project>-lockbox-*, cartridge tier driven by var.cartridge_table_patterns — each with an explicit DDB action allow-list. Adding a cross-tier table (e.g. a cartridge reading console-content) means adding its pattern here AND granting the action in the cartridge's IAM; both layers must agree.
The installation hub. The standard that cartridges plug into.
Why. If each cartridge implements its own access control, retention, or budget logic, drift is inevitable. Centralized enforcement lets us change policy once.
How to apply. Cartridge manifests declare data classification, retention, OAuth providers, owner, budget. Platform applies the constraints. Step 6.i.
Why. Data governance is a substrate concern, not a per-app concern. Without this layer every app invents its own classification.
How to apply. Cartridges declare a governance_cell. Substrate maps cells to keys.
Why. CloudFront OAC + Lambda Function URL silently fails on POST/PUT body-hash signing. We hit this on the obs budget-save bug and lost an afternoon. Never again.
How to apply. Every cartridge backend is API Gateway HTTP API + Lambda. CloudFront origin sends X-Origin-Secret; Lambda rejects without it. Step 6.j.
Why. Three-layer defense: CloudFront fronts, Cognito authenticates, the Cognito group authorizes. Any single layer failing does not collapse the others.
How to apply. Admin routes: read JWT from X-Auth-Token, verify, check cognito:groups contains the required group. Step 4.5.b.
Why. A single SDK at one URL is auditable and updatable in one place. Per-cartridge SDK copies drift apart within weeks.
How to apply. SDK lives at console/sdk/v1/. Cartridges link from the Console origin via the consoleOrigin config returned from /api/config.
Why. External cartridges (and increasingly internal ones) need a reproducible SDK version they can pin. The browser SDK URL implies v1 but is not a real version. Without a manifest declaring what counts as "the SDK", consumers cannot reason about compatibility.
How to apply. Semver in VERSION at the repo root. forge-sdk-manifest.json declares the four bundle paths (shared/modules/, console/sdk/v1/, cartridges/_template/, Doctrine.md + .claude/skills/forge-sdk/). Release tags are sdk-vX.Y.Z. Consumers pull with console/sdk/pull-sdk.sh [version] into ./forge-sdk/. A PR that adds or changes any SDK surface ships its documentation AND the release-sdk.sh bump in the SAME PR — the docs-coverage.test.mjs CI guards (generated docs/sdk-reference.md freshness, API-doc coverage, module-list sync, VERSION/manifest sync) reject the merge otherwise (see 8.10). See Step 6.o.
Why. PayCargo's Azure AD is the identity source of truth. Federation eliminates the two-password problem. Zero default permissions on first sign-in prevents accidental access; permissions are explicitly granted via the in-platform Request Roles cartridge.
How to apply. Cognito User Pool IdP type = SAML. SAML assertion does NOT carry group claims. JIT provisioning creates the Cognito user with no groups; user sees only the Request Roles cartridge. System admins approve requests via AdminAddUserToGroup. Cartridge JWT verify code unchanged. See Step 6.p.
Why. Forge access tracks Forge ownership, not Azure HR conventions. Coupling Cognito groups to Azure groups ties our access model to a shape we don't control and makes the audit story go through HR's group state from N weeks ago.
How to apply. SAML assertion configured to omit group claims. No pre-token-generation Lambda. Group management surfaces: obs Admin tab (ongoing) and Request Roles cartridge (new-user onramp). Each cartridge declares which Cognito groups it requires; the platform enforces. See Step 6.p.
Why. Bulk-download distribution lets consumers drift for months between pulls and gives the platform no visibility into who is on what version. A live MCP server makes every fetch authenticated, audited, and version-specific. Real-time means consumers always build against an answered question, not a stale lockfile. Protected means a leaver loses SDK access automatically when Cognito access is revoked. Agents (Claude Code, custom MCP clients) become first-class consumers alongside humans.
How to apply. A forge-sdk-mcp cartridge serves SDK content as MCP resources at URIs like forge-sdk://shared/modules/jwt-verify.js, forge-sdk://Doctrine.md#2.7. Every request is Cognito-authenticated; every fetch logs {requester, resource, version, timestamp, prompt_context_hash}. The pull-sdk.sh CLI wraps MCP under the hood; agents speak MCP natively. v1 git-pull is the bootstrap layer; v2 MCP ships once Request Roles + SSO are live. See Step 6.r.
Why. Doctrine typo fixes should not require a git PR and a Terraform apply. Versioning of content as DDB time-series gives operators rollback, diff, and per-document publish history that git tag-based versioning cannot match for content workflows. Authoring in the Console UI puts the doctrine update flow inside the same approval surface (Request Roles, role assignment) that already governs the platform.
How to apply. A forge-sdk-cms cartridge owns the markdown content layer: editor UI in the Console, draft / submit / approve / publish workflow, DDB-backed content rows with per-document version history. The MCP server cartridge (Doctrine 2.9) consumes from the CMS. The repo continues to hold cartridge code; the CMS holds the content cartridges and agents read. Initial bootstrap imports the existing Doctrine.md + docs/*.md + SKILL.md as v1.x.x CMS rows; subsequent edits happen via the Console UI. See Step 6.s.
As shipped (2026). A simpler concretion landed: the Content CMS is Console-hosted (not a standalone cartridge, no approval queue) — versioned CLAUDE.md/skills.md keyed by (cartridge, scope, name), read server-side by cartridges to steer Bedrock calls and by the Console MCP grounding tools. See 2.12 and Step 6.s.
Why. English-only text can't be localized, and retrofitting i18n later touches every view. Making localization the default keeps every surface (cartridge SPA, Console, SDK components) translatable from day one.
How to apply. No display string is hard-coded in any frontend. All user-facing text goes through PCSDK.i18n.t() against per-language catalogs under frontend/locales/ (a base language, an optional <lang>.auto.js machine layer, and an optional <lang>.js manual-override layer that wins). Numbers/dates/currency use the Intl-backed fmtNumber/fmtCurrency/fmtCompact/fmtDate helpers. Technical literals (CSS classes, IDs, catalog keys, brand/icon names) are exempt. A heuristic CI lint (scripts/i18n-lint.mjs) plus human review enforce it.
Why. An AI call is only as disciplined as the context it carries. Baking rules into cartridge code lets them drift and needs a deploy to change. A versioned store lets an operator edit the doctrine once; every consumer follows it on its next run.
How to apply. shared/modules/content/store.js keys docs by (cartridge, scope, name) (names claude/skills; scope _ = cartridge-wide) with append-only versions + an active pointer, in <project>-console-content. Cartridge Lambdas read the active docs server-side and fence them into the system prompt (content/safety.js) so guidance steers without smuggling directives. Edited in the Console Content CMS pane (admins) or a cartridge's own console (that cart's group). See Step 6.s.
Why. A scheduled, unattended Claude Code has no human at the keyboard, so interactive OAuth (browser sign-in, refresh tokens) is the fragile part. A recyclable machine token is minted once, stored on the scheduling machine, and rotated or revoked at will — and it can only do what its boundaries allow. This is the shipped machine-to-machine door; distinct from 2.9's SDK-distribution MCP vision.
How to apply. POST /api/mcp is checked before the JWT gate, behind X-Origin-Secret. Auth is HMAC-SHA256 over METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY (replay window ±300s); the secret is encrypted at rest (reversibly, so the server recomputes the HMAC). One endpoint multiplexes many target cartridges (switch_target, membership-enforced); per-token boundaries scope tools (grounding can't publish). Tools are thin adapters: grounding_manifest/grounding_get (Content CMS) and publish_evaluation (Doc Vault, real discipline gate). See Step 6.v.
Why. An iframed cartridge that repaints its own name/description stacks a duplicate header directly under the Console's top bar. The Console already shows the cartridge's identity, so the cartridge must not.
How to apply. The Console top bar paints topbar-title ← cartridge.name and topbar-subtitle ← cartridge.description (from console/lambda/catalog.js). An iframed cartridge renders no title/subtitle header of its own — it begins at the working area (a slim controls bar is fine). Exceptions: external: true cartridges (e.g. obs) render full chrome; a per-screen title inside a multi-screen cartridge is page context, not a cartridge-name duplicate.
How rides are built.
Why. Every npm dependency is a supply-chain surface, a version-pin drift risk, and an audit story we have to defend. JWT verify is ~70 lines of WebCrypto. HMAC is ~10 lines of node:crypto. None justifies npm.
How to apply. Import AWS SDK clients from the Node 20 runtime. Get JWT, HMAC, log, OAuth helpers from shared/modules/ staged into lambda/_shared/ at apply. Any new npm dep requires a written justification.
Why. Department-led, platform-supported. The folder is the home; the department owns the cartridge and its budget.
How to apply. New cartridges start from cartridges/_template/ and land under the owning department. global/ only for cross-department.
Why. Every cartridge backend looks the same: API Gateway HTTP API + Lambda + CloudFront. Every operator who has read one cartridge's backend.tf can read every cartridge's backend.tf.
How to apply. Copy the shape from cartridges/global/mock-email/ (small) or cartridges/administrative/obs/ (full admin). Step 6.j.
Why. Cartridges that skip the manifest are invisible to the platform's cost attribution, retention enforcement, and access policy.
How to apply. Required fields: owner, data classification, retention, OAuth providers, budget. Step 6.i.
*-complete log lines for every Bedrock invocation. Non-negotiableWhy. The obs Cartridges tab attributes cost to users by joining on this schema. A missing line means unattributed cost.
How to apply. logComplete({ msg: '<agent>-complete', user, model, inputTokens, outputTokens, durationMs }) from shared/modules/log.js.
local_file staging. DefaultWhy. Cartridges should not vendor their own copy of shared helpers; drift is the result. One source of truth at shared/modules/.
How to apply. local_file reads from shared/modules/, writes into lambda/_shared/. archive_file picks it up with depends_on. lambda/_shared/ is gitignored. See cartridges/_template/backend.tf.snippet.
Why. A cartridge is mostly business logic. If a cartridge has more plumbing than business logic, the platform has failed to provide the plumbing.
How to apply. When you find yourself writing JWT, refresh, or OAuth flow code inside a cartridge, lift it into shared/modules/. The first cartridge to need a primitive contributes it.
Why. Three capabilities are platform services, not per-cartridge features: the Content CMS (versioned grounding docs), the Console MCP (machine-token access for unattended agents), and the agent layer (the shared tool-using loop). A cartridge that stands up its own CMS table, MCP endpoint, or agent loop gives the platform N incompatible copies of a solved problem — N security surfaces to audit, N drift risks, N places a fix must land.
How to apply. Grounding docs → read the Console Content CMS (shared/modules/content/store.js) server-side and fence it; never a per-cartridge content table (2.12, 6.s). Unattended/scheduled agents → authenticate through the Console MCP with a recyclable machine token; never a second endpoint or bespoke token scheme (2.13, 6.v). Long tool-using runs → use the shared agent layer (shared/modules/agent/* + PCSDK.agent.run); never a hand-rolled loop or jobs table. A capability the platform lacks is lifted into the substrate (3.7), not forked privately.
Why. 3.1 bans npm in cartridge Lambdas, but the platform now has a non-cartridge layer — the AP decision core — where a dependency is genuinely load-bearing (you do not hand-roll a PostgreSQL wire protocol). “No npm, ever” would force us to reinvent security-critical code worse than the ecosystem does. The risk was never npm; it was unaudited, unpinned, sprawling npm. So the rule is not “never” — it is “never without the process below,” and the bar is deliberately high.
How to apply. Cartridges stay zero-npm (3.1 unchanged) — this exception is for non-cartridge components only. Acceptance (every module, every version bump) records in the registry: exact pinned version + integrity hash; a justification it is load-bearing and not reasonably hand-rollable; permissive license; maintainer/provenance + adoption; the full transitive dependency list (prefer near-zero-dep); an install-script review (reject a surprise postinstall); a clean CVE scan (OSV / npm audit); the reviewing human + date. The registry is the single source of truth (security/npm-allowlist.json); CI rejects any imported package absent from it — the same deterministic enforcement as arch-lint. The scheduled foundation-audit re-scans for new advisories and stale reviews (a finding, not a warning). Pins never float. First entry: pg (node-postgres) for the decision core's relational read-model (4.6).
How information moves through the platform.
Why. Without a centralized classification ENUM, every cartridge invents its own.
How to apply. Mutable ENUM today: public, internal, confidential, pii. Default internal. Classification happens at platform ingress boundaries. Step 4.5.a.
Why. Encryption boundary aligns with trust boundary. Compromise of one zone does not leak the others. See P1.5 Locks.
How to apply. Agent, Lockbox, Audit. Cartridges declare cells; substrate maps cell to CMK.
Why. Agents are smart enough to do useful work on tokens. They are also smart enough to leak PII if given the plaintext. The lockbox pattern makes the leak structurally impossible.
How to apply. Agents work on tokens. Reveal requires an explicit authorized call to the lockbox service which logs the reveal.
Why. The lockbox is specced but not built. Until it is, the platform structurally prevents PII handling rather than trusting cartridge code not to leak.
How to apply. Any cartridge declaring pii is gated until lockbox lands.
Why. An auditable trail that operators can modify is not auditable. Object Lock + Audit CMK + separate IAM zone is the design that survives an insider threat.
How to apply. Audit bucket: aws_s3_bucket_object_lock_configuration with COMPLIANCE mode. Audit CMK key policy denies kms:Decrypt to anything except the audit-reader role.
Why. The platform is DynamoDB-first and cartridges stay that way — DDB fits tenant-partitioned operational stores (documents, runs, keys, the TMS simulator's seeded accruals). But invoice↔accrual matching is relational — indexed candidate retrieval, multi-column joins, tolerance range scans, transactional idempotent posting — which DynamoDB serves poorly. Forcing matching into DDB is the anti-pattern; a relational store is the right tool for this one job.
How to apply. Exactly one sanctioned relational store: an Aurora Serverless v2 (PostgreSQL) instance holding the decision core's accrual read-model — a synchronized projection, not the source of truth (the TMS is; the simulator today, CargoWise later). Tenant isolation is enforced in the database via Row-Level Security keyed on a per-connection tenant id, on top of the app-layer tenant_id filter — defense the single-table DDB model cannot give. Terraform-provisioned in the existing VPC, reached through RDS Proxy (Lambda connection pooling), scaled to zero when idle. Reached only from the decision core, never a cartridge; the pg client is whitelisted per 3.9.
How cartridges talk to outside systems.
Why. If every cartridge implements its own OAuth, the auth surface multiplies. The Console owns the flow; cartridges consume a clean adapter. Step 6.n.
How to apply. OAuth flow lives in shared/modules/oauth/ and the Console Lambda. Cartridges call getOAuthClient(providerId, username).
Why. Mixing the three is the root cause of every OAuth abstraction that ages badly. Distinct on purpose.
How to apply. Provider = SSM manifest. Connection = DDB + Secrets Manager, owned by one user. Consumer = cartridge calling adapter verbs.
Why. Per-cartridge connections multiply the auth surface. Platform-shared connections defeat the provider's audit log. Per-user keeps identity end-to-end aligned.
How to apply. Connection ID = <provider_id>:<username>. Each user manages their own in the Setup tab.
Why. Adding a new provider should be one SSM parameter plus one adapter file. The OAuth flow code never changes.
How to apply. Provider manifest at /forge/oauth/providers/<id>.
Why. A token alone is not an abstraction. Cartridges need typed verbs. The scope gate prevents calling a verb that needs a scope the connection did not get.
How to apply. Per-provider adapter at shared/modules/oauth/adapters/<name>.js. buildAdapter attaches only methods whose scope_required is granted; ungranted methods throw a clear "reconnect from Setup" error.
Why. Lazy refresh hits user-facing latency and races on the expiry boundary. The scheduler isolates refresh from the request path.
How to apply. OAuth refresh sweeper runs every 10 minutes via EventBridge Scheduler. Cartridges read the current valid token; never refresh themselves.
Why. Different attack surfaces, different mechanisms.
How to apply. verifyGithubHmac for webhooks. checkOriginSecret for CloudFront → API Gateway. Do not reuse one for the other.
How AI work is decomposed and routed.
Why. Every AI task has cheap routine steps and expensive high-stakes steps. Paying frontier prices for routine steps is the most common cost mistake.
How to apply. Scout passes use Haiku or local LLM. Writer passes use Sonnet or Opus. The model router substrate makes the choice swappable. Step 6.l, 6.m.
Why. Hard-coding vendor model IDs locks the platform into one vendor's pricing trajectory. Routing as data lets us slide compute from cloud to local without touching cartridge code.
How to apply. Logical route names (scout-local, scout-cloud, writer-opus) map to backend endpoints in substrate/modules/platform/model-router/.
Why. Frontier models matter today; open-source will matter as it catches up. The router absorbs every quality improvement at zero refactor cost.
How to apply. When an open-source model crosses a quality threshold for a route, change the SSM manifest entry. Cartridges do not change.
Why. Claude 4.5 cannot be invoked on-demand against the bare foundation-model ID. The us.anthropic.claude-*-v1:0 inference profile is required.
How to apply. expandModelId() maps short IDs to the inference profile prefix. New cartridges use the same map.
Why. Single-turn AI work either pays for context the writer does not need or skips context the writer does need.
How to apply. Scout narrows context. Fetcher loads it. Writer produces. Optional reviewer adversarially verifies. Step 6.l.
Why. Per-user, per-model, per-cartridge cost visibility is the difference between "this is working" and "this is too expensive to keep."
How to apply. Every Bedrock call emits a *-complete log line via logComplete().
Why. Bedrock guardrails enforce policy at the inference boundary: no PII output, no off-policy topics, content moderation. Cheaper than per-cartridge prompt engineering.
How to apply. guardrailIdentifier and guardrailVersion set on every InvokeModel call. KMS Decrypt grant on the caller role for the guardrail CMK. Step 6.j.
How changes get from PR to production.
Why. Auto-apply on merge is how shops accidentally deploy unreviewed Terraform on a Friday night.
How to apply. GitHub Actions OIDC role; workflow_dispatch for Terraform Apply; required input confirm = "APPLY". Step 3a.
In AI-Automation-Shared (account 753974371195). The same human gate, held locally: terraform apply of a saved plan from substrate/environments/ai-sandbox with AWS_PROFILE=ai-shared, only after an explicit go-ahead on that exact plan. Nothing is pushed or applied automatically, and every AWS command checks the account first.
Why. The boundary is the trap door. Changes cross trust zones.
How to apply. Permission boundary policy in substrate/bootstrap/. Changes route to platform-team CODEOWNERS.
Why. Cron jobs that silently miss runs are how operational visibility erodes.
How to apply. Every scheduled Lambda is wrapped with wrapScheduledHandler. Watchdog runs every 30 minutes against the SSM manifest of expected jobs. Step 6.k.
Why. Cost Explorer charges per query and is slow.
How to apply. 48h hard TTL for daily-rollup data (narrative); per-query freshness for the Cartridges tab "Calculate now" button.
Why. Mixing "declare the cert" with "wait for DNS to validate it" with "attach the cert" produces an apply that hangs forever.
How to apply. PR 1 declares (apply succeeds, DNS records emitted). DNS admin adds records. PR 2 attaches the cert and wires SES into Cognito.
Why. If a multi-agent workflow bounds coverage with top-N, sampling, or no-retry, the operator must know. Otherwise a "complete" review reads as "we covered everything" when it did not.
How to apply. log() what was dropped. Reviewers see the cap explicitly.
Why. External cartridges depend on stable references. A clear release event lets consumers upgrade deliberately rather than chase main. A clean tag is also what pull-sdk.sh resolves against.
How to apply. ./console/sdk/release-sdk.sh {major|minor|patch|X.Y.Z} validates clean working tree, bumps VERSION and forge-sdk-manifest.json, commits, tags sdk-vX.Y.Z. Does NOT auto-push. Operator pushes main and the tag separately so the release goes through review. See Step 6.o.
How decisions get made.
Why. Cartridges built without a design sheet end up costing three times what was budgeted, touching data classifications they did not declare, and integrating with systems whose OAuth nobody has set up.
How to apply. Before the Forge runs, an Anvil sheet exists with: purpose, audience, external systems, OAuth providers, data classification, capabilities, cost estimate, risk callouts. P4 Anvil.
Why. AI produces code fast; that does not make it correct.
How to apply. Branch protection requires CODEOWNERS review. Apply requires literal APPLY typed by a human. AI scales the writing; humans gate the landing.
Why. Chasing functional outcomes with shippers and branches is what hardens the substrate. Building the Enclave first wastes time on theater.
How to apply. Cartridges target real users and real workflows. Substrate hardening follows from production use.
Why. Competitors who match feature velocity inherit one vendor's pricing power as a permanent dependency. Substrate-first looks slower for two quarters and then compounds. In financial compliance the damage is irreversible.
How to apply. When tempted to skip substrate to chase a feature, ask whether the cartridge ships without it. If yes, ship; substrate work compounds in parallel. If no, substrate was load-bearing all along.
Why. A bug fix does not need surrounding cleanup. Three similar lines is better than a premature abstraction. Half-finished features ship as full features and rot.
How to apply. Scope to the task. File follow-ups for cleanup.
Why. Defensive error handling for impossible scenarios is code that will never run and will distract a reader.
How to apply. Validate at user input, external APIs, and provider responses. Inside the platform, trust the framework.
Why. Strategic narrative is a different discipline from engineering.
How to apply. Memos to the CEO or board go through Mike Lewis first. Engineering-side docs go through CODEOWNERS.
Why. SSO failure modes (Azure outage, expired SAML signing certificate, IdP misconfiguration, federation incident) must not lock the platform team out. A Cognito-native admin track that exists outside the federation is the recovery path. System admins are not break-glass-only; they are the platform's administrative track that also covers break-glass.
How to apply. At least two Cognito-native accounts in the admins Cognito group. TOTP MFA enforced. Used routinely (approve Request Roles, debug cartridges, incident response). Verified quarterly. SSO bypass is the design feature, not a workaround. See Step 6.p.
Why. Once doctrine + skill + docs live in the CMS, the CMS becomes critical-path infrastructure. A CMS outage cannot leave system admins without doctrine to reference, agents without skills to load, or consumers without docs to read. The git copy stays canonical-equivalent at every published version and serves as both the disaster-recovery cache and the cold-start bootstrap for the CMS itself.
How to apply. Every CMS publish triggers a git mirror update (system admin reviews and merges). System admins maintain at least one current local checkout. During a CMS incident, the MCP server falls back to the git mirror for read paths. Quarterly verification: diff CMS published content against the git mirror; divergence triggers investigation. Same parallel-track shape as Doctrine 8.8: when the primary path is down, the recovery path is alive. See Step 6.s.
Why. Documentation that lags the code is worse than none — it misleads. The fix is to make truth mechanical: the repo is self-documenting (aggregated surfaces are generated from source, never hand-copied) and self-healing (drift can't merge). This is the very mechanism that would have caught this doctrine page trailing its own source.
How to apply. Any PR that changes a shared surface (SDK module, PCSDK.* API, shared Lambda helper, cartridge registration, repo convention) ships its documentation in the SAME PR. Aggregated surfaces are generated (scripts/gen-sdk-reference.mjs → docs/sdk-reference.md); a new aggregated surface means a new generator registered in scripts/heal-docs.mjs. The docs-coverage.test.mjs CI guards fail on a stale generated page, an undocumented API, an out-of-sync module list, or a VERSION/manifest mismatch. When a guard fails, run node scripts/heal-docs.mjs — never hand-edit a generated file.
The shape of things that get rejected. Each one has a one-line failure mode and the doctrine that replaces it.
Why it fails. SigV4 body-hash mismatch silently 403s POST/PUT. Lost an afternoon on the obs budget-save bug.
Replace with. API Gateway HTTP API + X-Origin-Secret. Console 2.3.
Why it fails. Multiplies the auth surface. Operators connect Salesforce N times. The IAM permission boundary already enforces scope; app-level repeat is theater.
Replace with. Shared OAuth in the Console, per-user connections. OAuth 5.1, 5.3.
Why it fails. Supply chain surface; version drift; audit story we have to defend.
Replace with. Lift the primitive into shared/modules/. Cartridge 3.1.
aws/...).Why it fails. Cannot answer "who decrypted what." Cannot revoke decrypt without redeploying.
Replace with. Customer-managed CMK. Foundation 1.6.
Why it fails. Compromise of any role leaks everything. Audit story fails.
Replace with. Three CMKs per trust zone. Foundation 1.4, P1.5.
Why it fails. Vendor lock-in; cannot swap to a cheaper or local model without changing every cartridge.
Replace with. Model router substrate; logical route names. AI 6.2.
Why it fails. Operators read a "complete" report and assume coverage. The cap hides the gap.
Replace with. Log the cap. Operational 7.6.
Why it fails. Department ownership is structural. A sales cartridge under engineering/ signals confusion about whose budget it lives in.
Replace with. Cartridge under the owning department. Cartridge 3.2.
Why it fails. English-only text can't be localized, and retrofitting i18n later touches every view.
Replace with. PCSDK.i18n.t() + per-language catalogs under frontend/locales/. Console 2.11.
Why it fails. The Console top bar already shows the cartridge name/description, so a cartridge-painted header stacks a duplicate directly beneath it.
Replace with. No title header in an iframed cartridge; begin at the working area. Console 2.14.
Why it fails. These are platform services. A private copy multiplies the security surface to audit, drifts from the canonical one, and forces every fix to land in N places.
Replace with. Consume the Content CMS (shared/modules/content), the Console MCP (machine tokens), and the agent layer (shared/modules/agent + PCSDK.agent.run). Cartridge 3.8.
Why it fails. A module imported outside the 3.9 registry is an unaudited supply-chain surface — no owner, no pin discipline, no CVE watch — the exact risk 3.1 exists to prevent, reintroduced through the back door.
Replace with. In a cartridge: hand-roll into shared/modules/ (3.1). In the decision core: run it through the 3.9 acceptance + registry, or do not import it. Cartridge 3.9.
Pre-PR by Claude Code via the agent skill at .claude/skills/forge-sdk/SKILL.md. At PR time via CODEOWNERS review. At apply time via the literal APPLY confirm. At runtime via the Foundation guardrails (IAM permission boundary, KMS key policies, Bedrock guardrails, Network Firewall, GuardDuty).
Markdown source: Doctrine.md at the repo root.