# PayCargo Forge — cartridge repo

This repository contains a single Forge cartridge (or a small set of related cartridges). Its purpose is to be shipped, reviewed, and merged into the PayCargo Forge platform running in the AI-Sandbox AWS account.

This file is auto-loaded by Claude Code at the start of every session. Read it in full before writing code.

---

## What kind of code lives here

- **`cartridge.tf`, `backend.tf`, `frontend.tf`** — Terraform for one cartridge (API Gateway HTTP API + Lambda + CloudFront + S3, wired to the shared Cognito pool and the shared DDB agent-state table).
- **`lambda/handler.js`** — the cartridge's backend (Node 20, zero npm dependencies allowed — see the Forge SDK rules).
- **`lambda/_shared/`** — gitignored. Populated at `terraform apply` time by `local_file` resources that stage helpers from `./forge-sdk/shared/modules/` into the Lambda zip.
- **`frontend/`** — static SPA. Loads `PCSDK` (auth, i18n, UI components) from the Console origin. No build step, no bundler.
- **`ANVIL.md`** — the design sheet locked before code was written (purpose, audience, external systems, OAuth providers, data classification, capabilities used, cost estimate, risk callouts).

Repository does NOT contain:
- Substrate Terraform (identity, network, guardrails, KMS keys, permission boundary, Cognito pool). Those come from the main `ai-sandbox` repo and are already deployed.
- The Console. Cartridges consume the Console's SDK from its origin at runtime; they never fork or vendor it.
- Other cartridges. If this repo grows to multiple cartridges, split them.

---

## Working with the Forge SDK

The SDK bundle lives at `./forge-sdk/` in this repo (pulled via `./pull-sdk.sh`, pinned in `forge-sdk.lock`). It contains:

- `./forge-sdk/shared/modules/` — hand-rolled zero-npm Node helpers the cartridge Lambda imports at runtime via `local_file` staging. Never edit files here; they get overwritten on the next `pull-sdk.sh`.
- `./forge-sdk/console/sdk/v1/` — the browser SDK for IDE autocomplete and local testing. At runtime the frontend always loads from the Console origin, not from disk.
- `./forge-sdk/docs/` — seven per-topic reference markdowns. Start with `getting-started.md`, then `cartridge-walkthrough.md`.
- `./forge-sdk/Doctrine.md` — the canonical rules pinned to this SDK version. If a doctrinal question comes up, read this file.
- `./forge-sdk-single-file.md` (optional) — the seven docs consolidated into one file, useful for search.

Upgrade ceremony when a new SDK version is needed:

```bash
./pull-sdk.sh v<new-version>       # blows away ./forge-sdk/, writes new lock
git add forge-sdk.lock
git commit -m "chore(sdk): upgrade forge-sdk <old> -> <new>"
```

### Tenants (SDK 1.18+)

The platform is multi-tenant: each customer is a tenant (e.g. `t_aries`), and there is no default tenant. The Console's top bar picks one; your frontend reads it with `PCSDK.auth.tenant()`, reacts to a switch with `PCSDK.auth.onTenantChange(cb)` (re-load data, or reload the page), and `PCSDK.api` sends it as the `x-tenant` header (or spread `PCSDK.auth.tenantHeaders()` into your own fetch). The header is a hint only: your backend must call `requireTenant` from `shared/modules/tenant.js` against the verified token and return its 403/400 when it refuses, and every row you store carries `tenant_id`. See `docs/cartridge-walkthrough.md` ("Tenant-scoped data") and the obs doc "How tenants keep customer data apart".

Never modify files under `./forge-sdk/` directly. If you need a helper the SDK does not have, either lift the primitive upstream (`shared/modules/` in the main repo) or use the escape hatch documented in the relevant `oauth-pattern.md` or `bedrock-pattern.md`.

---

## Non-negotiable rules

Skim these before every task. The full list, with rationale and anti-patterns, is in `.claude/skills/forge-sdk/SKILL.md`. That skill loads on demand when you touch cartridge code.

1. **Zero npm in cartridge Lambdas.** AWS SDK from Node 20 runtime. JWT/HMAC/log/OAuth from `forge-sdk/shared/modules/`. New npm dep = stop and lift the primitive into the SDK instead.
2. **API Gateway HTTP API + `X-Origin-Secret`.** Never OAC + Lambda Function URL for POST/PUT/DELETE.
3. **Cognito JWT + group check on every admin endpoint.** Read `X-Auth-Token`, verify via `createJwtVerifier`, check `cognito:groups`. Three layers (CloudFront, JWT, group) must all pass.
4. **Permission boundary on every IAM role.** `permissions_boundary = var.permission_boundary_arn`. No exceptions.
5. **No public Lambda Function URLs.** `AuthType = NONE` is SCP-denied at the account level; a `terraform apply` that tries will fail.
6. **Customer-managed CMKs only.** Never `aws/...`-prefixed AWS-managed keys.
7. **Emit `*-complete` log lines on every Bedrock call.** `logComplete({msg: '<cartridge>-complete', user, model, inputTokens, outputTokens, durationMs})`. The obs Cartridges tab joins on this schema for cost attribution.
8. **Bedrock guardrails on every InvokeModel.** `guardrailIdentifier` + `guardrailVersion` set, KMS Decrypt on the caller role for the guardrail CMK.
9. **Claude 4.5 uses inference profiles.** `us.anthropic.claude-haiku-4-5-20251001-v1:0` or `us.anthropic.claude-sonnet-4-5-20250929-v1:0`. The bare foundation-model ID will 404.
10. **OAuth via the Console, not per-cartridge.** Cartridges call `getOAuthClient(providerId, jwtPayload.username)`; they never authorize, refresh, or store tokens.
11. **Per-user OAuth connections.** Connection ID is `<provider_id>:<username>`. No platform-shared or per-cartridge connections.
12. **Cartridge manifest declares metadata.** Owner, data classification, retention, OAuth providers, budget. Missing fields fail review.
13. **No hard-coded user-facing strings in frontend.** All display text via `PCSDK.i18n.t()` + per-language catalogs in `frontend/locales/<lang>.js`. Numbers/dates/currency via the `Intl`-backed `fmt*` helpers.

---

## Design system (frontend)

The cartridge's frontend loads PCSDK from the Console origin, which brings the PayCargo design tokens (`--pc-*`), Font Awesome kit, Inter font, and prebuilt components (Table, Pagination, Modal). Do not vendor any of these; do not hardcode brand colors or fonts.

**Full reference: [`DESIGN.md`](./DESIGN.md).** Token catalog, component patterns (buttons, form inputs, tables, badges, callouts, sidebar), icon conventions, layout patterns, and the pre-ship checklist all live there. Read it in full before writing any frontend code.

Minimum load-order for a cartridge `index.html`:

```html
<link rel="stylesheet" href="https://kit.fontawesome.com/d28826e619.css" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" data-pcsdk href="/sdk/v1/css/tokens.css">
<link rel="stylesheet" data-pcsdk href="/sdk/v1/css/components.css">
<script src="/sdk/v1/js/cartridge-auth.js" defer></script>
<script src="/sdk/v1/js/i18n.js" defer></script>
```

The `data-pcsdk` marker is what `PCSDK.auth.ready()` uses to rewrite the URLs to the Console origin at boot. Do not change the marker or add cache-busters.

---

## Local development

```bash
./pull-sdk.sh                      # ensure ./forge-sdk/ is populated
terraform init
terraform plan                     # sanity check
```

There is no local Lambda runtime harness. Iterate against the deployed cartridge in account 753974371195, or write a small Node script under `scripts/` that imports the same handler and exercises it.

---

## Deploy

In `PayCargoDevOps/AI-Automation-Shared` (AWS account `753974371195`), deploys are a **local** `terraform apply` from `substrate/environments/ai-sandbox` with `AWS_PROFILE=ai-shared`, and every apply needs an explicit human go-ahead on the exact plan. Nothing is pushed or applied automatically. That is Doctrine 7.1's gate, held locally in this environment.

```bash
export AWS_PROFILE=ai-shared
[ "$(aws sts get-caller-identity --query Account --output text)" = "753974371195" ] || exit 1
cd substrate/environments/ai-sandbox
terraform plan -out=/tmp/my-cartridge.tfplan     # show the plan; wait for approval
terraform apply /tmp/my-cartridge.tfplan          # only after "yes, apply"
```

Always check the account first: the retired account `959228203854` must never be touched. The workflows in `.github/workflows/` exist but are not the deploy path for this environment.

---

## Anvil first, code second

Before writing any code for a new cartridge, complete `ANVIL.md`:

| Question | Answer |
|---|---|
| Purpose | One sentence: what does this cartridge do? |
| Audience | Which Cognito group can open it? |
| External systems | Salesforce? Power BI? None? |
| OAuth providers | Provider IDs, from the Setup tab's inventory. |
| Data classification | non-pii / pii / lockbox-required |
| Retention | Log retention (days), data retention |
| Capabilities used | List of `sf.soql`, `sf.raw`, Bedrock guardrail, scheduled jobs, etc. |
| Cost estimate | Bedrock tokens/day + Lambda invocations/day + storage |
| Risk callouts | What can go wrong; which doctrine mitigates each |

If you cannot answer any row, get the answer from the requester before writing code. Do not stub or guess.

---

## When something is missing

- SDK helper missing → check `./forge-sdk/shared/modules/`. If genuinely missing and this cartridge needs it, open an issue in the main ai-sandbox repo describing the primitive and its use case. Do NOT reach for npm.
- Doctrine gap → open `./forge-sdk/Doctrine.md`. If genuinely uncovered, propose an addition as a comment on the PR.
- SDK version outdated → run `./pull-sdk.sh v<new>`, commit the lock, verify the change works. Read the changelog first for major bumps.
- Anti-pattern that seems necessary → it isn't. Re-read the relevant SDK doc; the pattern is already there. If truly not, escalate to the platform team.

---

## Session-specific guidance

The `.claude/skills/forge-sdk/` skill file loads on demand when you touch cartridge code (any change under `cartridges/`, `lambda/`, `frontend/`, `shared/modules/`, `substrate/`, `console/`, or `.claude/`). Trust it over guessing. When the skill contradicts something older in the SDK docs, the skill wins — it is the up-to-date operational summary.

For questions that span the SDK bundle (e.g. "how does OAuth work end-to-end"), the seven docs in `./forge-sdk/docs/` are the reference. `./forge-sdk-single-file.md` (if present) is the same content in one file, easier to grep.
