PayCargo · AI-Sandbox · Step 6.i

6.iCartridge extended metadata — doctrine

The v2 manifest shape. Captures what each cartridge declares so the platform can enforce data governance, retention, AI budget, SoR access, egress, and lifecycle. Not built yet; doctrine ready for post-60d work.
Status: Doctrine — not deployed · Builds on: Step.6.g (Console v0 build) + Step.6.h (implementation reference) · Implements after: 60-day platform consolidation
Doctrine
This doc is forward-looking. The v0 manifest deployed early on is 8 fields (id, name, description, icon, color, url, loadMode, requiredGroups). The v2 shape below extends that into 9 named sections that map cleanly to existing doctrinal layers.

Operator surface now live: the obs app's Cartridges tab is the operator-facing implementation of this metadata. Per-cartridge rows let an admin set monthly budget, warning threshold, warning recipients, SSO roles, retention tiers, and notes — all stored in DDB under agent_session_id="cartridge-config". Today the platform stores the config; the enforcement pathways (Console launcher tile-hiding, retention applied to log groups, SES warning send) are wired one at a time. Each field carries an "Enforcement: declarative" badge so the operator knows what's enforced today vs. stored-for-later.

1. Why extended metadata

Every cartridge interacts with five things beyond its own UI: data (what sensitivity tier, what KMS key, what PII handling), retention (how long it keeps what), AI spend (which models, how much), systems of record (which SoRs it can read/write, at what rate), and egress (which destinations its network calls can reach). Each one of these has a doctrinal layer that already exists at the platform level. Without metadata, every cartridge author redecides each question, and the platform can't audit or enforce centrally.

The metadata model says: the cartridge declares what it needs; the platform enforces what it declared. That's the iOS Info.plist + iOS entitlements pattern, applied to vibe apps.

§ Declaration vs Discretion

Cartridges declare, platform enforces.

Anything a cartridge author could otherwise quietly do (write to a more sensitive CMK than they need, call an SoR they shouldn't, burn through AI budget without limit) gets moved into the manifest. The cartridge can't deploy without declaring it. The platform can't lie about what it allowed because the manifest is the contract.

2. v2 manifest shape — full sketch

Nine sections. Sections marked (today) already exist in v0 and stay structurally where they are (just reorganized into the namespaced form below). Sections marked (extended) are the post-60d additions.

{
  // ── Manifest envelope ────────────────────────────────────────
  manifestVersion: 2,
  id:              'chatbot',
  version:         '1.0.0',

  // ── Display (today) ──────────────────────────────────────────
  display: {
    name:        'Chatbot',
    description: 'Claude via Bedrock',
    icon:        'fa-comments',
    color:       'purple',
  },

  // ── Loading (today) ──────────────────────────────────────────
  load: {
    mode:    'iframe',
    url:     'https://chatbot.ai-sandbox.paycargo.com/',
    sandbox: ['allow-scripts','allow-same-origin','allow-forms'],
  },

  // ── Access control (today + extended) ────────────────────────
  access: {
    requiredGroups:   ['chatbot-users'],
    departmentOwner:  'engineering',     // which dept's roster owns this
    audience:         'platform',        // platform / cxo / ciso / dept
  },

  // ── Data governance (extended) ───────────────────────────────
  governance: {
    sensitivityTier: 'internal',         // public / internal / confidential / restricted
    kmsTier:         'eng-low-risk',     // which CMK from the data-governance grid
    piiMode:         'none',             // none / tokenized / lockbox-mediated
    dataResidency:   ['us-east-1'],
  },

  // ── Retention (extended) ─────────────────────────────────────
  retention: {
    sessionTtlDays:  90,
    auditYears:      7,
    archivePolicy:   'glacier-after-1y',
    deletionMode:    'soft-then-purge',
  },

  // ── AI budget (extended) ─────────────────────────────────────
  budget: {
    monthlyCeilingUSD:  500,
    softWarnPct:        0.75,
    hardCapBehavior:    'degrade',       // degrade / disable
    allowedModels: [
      'anthropic.claude-haiku-4-5-*',
      'anthropic.claude-sonnet-4-5-*',
    ],
    defaultGuardrailId: 'gr-xxxxx',
  },

  // ── Systems of Record (extended) ─────────────────────────────
  sor: {
    salesforce: { scope: 'read-only',  rateLimit: '60/min' },
    monday:     { scope: 'read-write', rateLimit: '120/min' },
    github:     { scope: 'read-only',  rateLimit: '5000/hr' },
    gitbook:    { scope: 'read-only',  rateLimit: 'unlimited' },
  },

  // ── Egress allowlist (extended; derived from sor + extras) ───
  egress: {
    destinations: [
      'api.salesforce.com',
      'api.monday.com',
      'api.github.com',
      'api.gitbook.com',
    ],
  },

  // ── Lifecycle + ops (extended) ───────────────────────────────
  lifecycle: {
    status:             'beta',          // alpha / beta / ga / deprecated / sunset
    createdAt:          '2026-06-01',
    lastSecurityReview: '2026-06-01',
    owningTeam:         'eng-platform',
    onCallRotation:     'eng-platform',
  },
}

3. Per-section deep dive

3.1 access — who can see and use this cartridge

FieldTypeEnforced byNotes
requiredGroupsstring[]Console /api/cartridges filter + cartridge backend group checkSubset check against JWT cognito:groups. Today: empty array = visible-to-all. v2: every cartridge declares at least one group.
departmentOwnerenumConsole launcher (badge color, audit attribution)One of: global / sales / marketing / engineering / hr / finance / labs. Matches Step 6.d department segmentation.
audienceenumConsole launcher (sidebar grouping)platform / cxo / ciso / dept. Drives which sidebar section the tile appears under, if multiple.

Today's requiredGroups is the only access mechanism. v2 keeps that AND adds department ownership (for audit + accounting) and audience (for UI organization). Department determines which CMK tier the cartridge's data is encrypted with; audience just affects which Console section the tile renders in.

3.2 governance — data classification + encryption

FieldValuesEnforced by
sensitivityTierpublic · internal · confidential · restrictedDrives CMK assignment + audit log routing
kmsTierOne of the CMK keys from the dept × tier grid (Step 4.5.a)Cartridge's S3 bucket + Lambda env vars + DDB encrypted with this CMK
piiModenone · tokenized · lockbox-mediatedCartridge backend's reveal-PII helper; pre-deploy lint
dataResidencystring[]Region-restricted resources at deploy time

piiMode contract

3.3 retention — how long data lives

FieldDefaultEnforced by
sessionTtlDays90DynamoDB TTL attribute on session-state items
auditYears7S3 Object Lock COMPLIANCE on the audit bucket. Matches Step 4 default.
archivePolicyglacier-after-1yS3 Lifecycle Rule on the cartridge's data bucket
deletionModesoft-then-purgeCartridge backend delete helper; per-record tombstones before hard-delete

Retention is not optional

Every cartridge MUST declare retention. A cartridge that doesn't know how long it keeps data has not thought about regulatory compliance. The platform's lint fails deploys where retention is missing.

3.4 budget — AI spend ceiling

FieldTypeEnforced by
monthlyCeilingUSDnumberCartridge backend Bedrock wrapper + CloudWatch billing alarm
softWarnPct0..1Same wrapper — surface a banner in the cartridge UI when crossed
hardCapBehaviordegrade · disableBedrock wrapper at 100% of ceiling
allowedModelsstring[]Bedrock invoke wrapper rejects model IDs not in this list
defaultGuardrailIdstringAuto-attached to every Bedrock InvokeModel call

degrade means fall back to a cheaper model (Haiku) once the ceiling is hit; disable means return a "monthly budget exhausted" error to the user. Cartridge declares which behavior it wants based on its UX.

3.5 sor — Systems of Record access scope

Per-SoR access is two dimensions: scope (read-only / read-write / none) and rate limit. The cartridge backend's SoR client library reads this manifest at startup and refuses calls that violate either.

sor: {
  salesforce: { scope: 'read-only',  rateLimit: '60/min' },
  monday:     { scope: 'read-write', rateLimit: '120/min' },
  github:     { scope: 'read-only',  rateLimit: '5000/hr' },
  gitbook:    { scope: 'read-only',  rateLimit: 'unlimited' },
}

Pilot allowlist is exactly four SoRs (Salesforce, Monday, GitHub, GitBook) per the egress doctrine. Adding a fifth requires a platform-level change, not a per-cartridge flag — that's intentional.

TODO: decide whether sor declarations require human review (vs self-declared and lint-checked). For read-write scope on any SoR, almost certainly review-required. For read-only, possibly self-declare.

3.6 egress — Network Firewall allowlist

Derived from sor for most cartridges (each SoR has known API hostnames). But some cartridges may need additional egress destinations (e.g. a public RSS feed, a static CDN). Listing them here lets the NFW rule generator produce the right allowlist without manual touch.

Default egress posture: deny-all. Each entry in egress.destinations opens one hostname. Sub-pattern matching (*.example.com) allowed but discouraged — list exact hostnames where possible.

3.7 lifecycle — cartridge status + ownership

FieldPurpose
statusControls Console launcher behavior: alpha tiles get an "experimental" badge; beta tiles get a "beta" badge; ga tiles render plain; deprecated tiles get a warning + sunset date; sunset tiles are hidden from the launcher (still accessible to admins).
createdAtISO date — used for "added X days ago" UX.
lastSecurityReviewISO date — Console flashes a warning if this is older than 365 days.
owningTeamThe team responsible for the cartridge — surfaced in audit logs, billing reports, and "who do I page" UI.
onCallRotationPagerDuty rotation ID (or equivalent) for production incidents involving the cartridge.

4. Where each section is enforced

Enforcement is split between the Console (launcher-side) and the cartridge's own backend (runtime-side). Roughly: Console gates discoverability + UI; cartridge backend gates runtime behavior.

SectionConsole-sideCartridge-sidePlatform-side
displayrenders tile
loadiframe / external / disabled dispatch
accessrequiredGroups filter on tile listbackend re-checks group claim on each request
governancecode lint pre-deploy (PII regex on piiMode: none)Terraform: CMK assignment, region restrictions
retentionbackend writes TTL attribute on DDB items, tombstone on deleteTerraform: S3 lifecycle rule, Object Lock policy
budgetshow usage % in tile (optional)Bedrock invoke wrapper enforces ceiling + model allowlist + guardrailCloudWatch billing alarm at the soft/hard caps
sorSoR client library enforces scope + rate limitPlatform reviews read-write scope on PR
egressNFW rule generator produces allowlist; CloudFormation drift detects out-of-band changes
lifecyclebadges + deprecation warnings on tilesAudit log attribution

5. Versioning

The top-level manifestVersion field exists so the Console can support v1 cartridges (today's 8-field shape) and v2 cartridges (this doc) side-by-side during the migration. The handler reads manifestVersion first and dispatches to the right parser.

Manifest version is per-cartridge, NOT global. A v1 cartridge and a v2 cartridge can live in the same CARTRIDGES array; the launcher renders both correctly.

6. Migration from v1 to v2

  1. Add the manifest dispatcher to handler.js. Read manifestVersion; default to 1 for entries that don't set it.
  2. Define platform defaults. Each v2 section has a default value (e.g. governance.sensitivityTier: 'internal', retention.sessionTtlDays: 90). A v1 cartridge gets ALL defaults applied silently.
  3. Migrate existing cartridges one at a time. Obs (when migrated to cartridge form) becomes v2 with explicit declarations. Chatbot ships as v2 from day one.
  4. Lint deploys. CI rejects new cartridges that don't declare every required v2 section.
  5. Set a sunset date for v1. Six months after v2 ships, drop the v1 dispatcher. Any cartridge still on v1 must migrate or be archived.

7. Where this metadata lives

Today: hardcoded in handler.js's CARTRIDGES array. Fine for v0 with three entries; not fine when there are 20 cartridges and an Admin tab edits them.

Future iterations (the natural progression):

  1. v1.1: move CARTRIDGES to cartridges.json in the same Lambda zip. Easier diff in PR, no logic change.
  2. v1.2: move to S3 (s3://ai-sandbox-console-frontend/cartridges.json). Lambda reads at cold start. Admin tab can write without redeploy.
  3. v2: move to DynamoDB (one item per cartridge). Admin tab CRUD via API. Each item has the full v2 schema above.
  4. v2.1: Bedrock guardrail + KMS key + NFW rules auto-generated from manifest at deploy time. Manifest becomes the single source of truth.

8. Open questions / TODOs

TODO 1: piiMode: 'tokenized' assumes the lockbox exists (per "Content lockbox / tokenization pattern" doctrine). Spec out the lockbox API surface before any cartridge declares this mode. Currently scoped post-60d.
TODO 2: Decide sor review gate threshold. Self-declare for read-only, peer review for read-write? Or always peer review on first declaration of any SoR scope?
TODO 3: budget.hardCapBehavior: 'degrade' needs a fallback model declaration. Does every cartridge need to nominate a degrade-target model, or does the platform pick one (always Haiku)?
TODO 4: Should egress.destinations be enforced by NFW (network layer) OR by the cartridge's HTTP client (app layer) OR both? Layered defense argues both. Cost argues just NFW.
TODO 5: Cartridge versioning. version: '1.0.0' field is in the manifest envelope, but how does the Console decide which version to launch when multiple are registered? "Latest wins" by default, but maybe an admin pin?
TODO 6: lifecycle.status: 'sunset' hides tile from launcher but admin still sees. Define the admin override mechanism (probably a URL query param ?show=sunset with admin-group check).

9. Doctrine pins reinforced

What this doc enables

When post-60d work begins, the platform team starts from this spec instead of a blank page. Each TODO marker above is the entry point for a focused decision. The migration path is already mapped. The enforcement split is already defined. The first cartridge (chatbot) ships as v2 from day one because the manifest version is future-aware. The substrate has shape before the build starts.