Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 5 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ VITE_STRAVA_CLIENT_ID=
GMP_SERVER_API_KEY=
VITE_GMP_API_KEY=
VITE_ISOCHRONES_GMP_API_KEY=
# Optional dedicated server key for Real World Reasoning's Maps Grounding Lite.
# Falls back to GMP_SERVER_API_KEY when empty.
GMP_MCP_KEY=
RWR_GROUNDING_LITE_ENABLED=false

# Contact form + email list (optional locally)
RESEND_API_KEY=
Expand All @@ -31,7 +35,7 @@ GITHUB_REVIEW_TOKEN=
# Analytics (optional public configuration for the portfolio and hosted demos)
ANALYTICS_MEASUREMENT_ID=

# Gemini (optional; server-only contact classification)
# Gemini (server-only Hairstyle AI shared tier and optional contact classification)
# Never prefix this with VITE_. Configure the production value through
# Secret Manager as GEMINI_API_KEY=gemini-api-key:latest on Cloud Run.
GEMINI_API_KEY=
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ updates:
schedule:
interval: "weekly"

- package-ecosystem: "npm"
directory: "/demos/real-world-reasoning-agent"
schedule:
interval: "weekly"

- package-ecosystem: "github-actions"
directory: "/"
schedule:
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ jobs:
echo "- RESEND_API_KEY (optional)"
echo "- RESEND_SEGMENT_ID (optional, email-list subscriptions)"
echo "- RESEND_TOPIC_ID (optional, email-list preferences)"
echo "- GEMINI_API_KEY (optional)"
echo "- GEMINI_API_KEY (required for the Hairstyle AI shared tier)"
echo "- CONTACT_TO_EMAIL (optional)"
echo "- GOOGLE_OAUTH_CLIENT_SECRET (required for /writer/)"
echo "- GOOGLE_OAUTH_SESSION_SECRET (required for /writer/)"
Expand Down Expand Up @@ -209,6 +209,7 @@ jobs:
# --project "$GCP_PROJECT_ID" --region "$GCP_REGION" \
# --update-secrets=STRAVA_CLIENT_SECRET=strava-client-secret:latest \
# --update-secrets=GMP_SERVER_API_KEY=gmp-server-api-key:latest \
# --update-secrets=GMP_MCP_KEY=gmp-mcp-key:latest \
# --update-secrets=RESEND_API_KEY=resend-api-key:latest \
# --update-secrets=RESEND_SEGMENT_ID=resend-segment-id:latest \
# --update-secrets=RESEND_TOPIC_ID=resend-topic-id:latest \
Expand All @@ -219,7 +220,7 @@ jobs:
# --update-secrets=GITHUB_CONTENT_TOKEN=github-content-token:latest \
# --update-secrets=GITHUB_REVIEW_TOKEN=github-review-token:latest \
# --update-secrets=BUFFER_API_KEY=buffer-api-key:latest \
# --set-env-vars=STRAVA_CLIENT_ID=<your Strava app's client id>,CONTACT_FROM_EMAIL=<verified sender>,GOOGLE_OAUTH_CLIENT_ID=<oauth-client-id>,WRITER_PUBLIC_ORIGIN=https://ryanbaumann-dashboardfolio.admin.com,BUFFER_ORGANIZATION_ID=<buffer-org-id>,BUFFER_LINKEDIN_CHANNEL_ID=<buffer-linkedin-id>,BUFFER_X_CHANNEL_ID=<buffer-x-id>
# --update-env-vars=STRAVA_CLIENT_ID=<your Strava app's client id>,CONTACT_FROM_EMAIL=<verified sender>,GOOGLE_OAUTH_CLIENT_ID=<oauth-client-id>,WRITER_PUBLIC_ORIGIN=https://ryanbaumann-dashboardfolio.admin.com,BUFFER_ORGANIZATION_ID=<buffer-org-id>,BUFFER_LINKEDIN_CHANNEL_ID=<buffer-linkedin-id>,BUFFER_X_CHANNEL_ID=<buffer-x-id>,RWR_GROUNDING_LITE_ENABLED=true
#
# STRAVA_CLIENT_ID isn't secret on its own (it's public in the
# OAuth authorize URL), so plain --set-env-vars is fine for it.
Expand All @@ -246,6 +247,7 @@ jobs:
'STRAVA_CLIENT_ID',
'STRAVA_CLIENT_SECRET',
'GMP_SERVER_API_KEY',
'GEMINI_API_KEY',
'GITHUB_CONTENT_TOKEN',
]);
for (const app of apps) {
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ picture.
- `demos/strava-explorer/`: Vite app for exploring Strava activities on Google Maps Platform 3D Maps.
- `demos/aqi-map/`: Vite app rendering Air Quality API heatmap tiles and point conditions on a 2D Google map.
- `demos/isochrones/`: Vite + Node app for reachability analysis using Google Maps Platform Isochrones.
- `demos/hairstyle-ai-studio/`: React + Vite app for Gemini-powered hairstyle generation, with a gateway-hosted daily allowance and optional tab-memory personal key.
- `demos/real-world-reasoning-agent/`: React + Vite Google Maps Platform and Gemini agent demo with first-party gateway proxies for live Maps and AI missions.
- `gateway/`: zero-npm-dependency Node server that serves the portfolio at
the root path, every workspace demo app's static build (both routed via the root
`apps.json` manifest, most-specific path first), and same-origin `/api/*`
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
## [Unreleased]

### Security
- Documented and enforced the Lab gateway's key boundaries, CSP assumptions,
conservative per-IP and process-wide daily Maps/Gemini ceilings, bounded
limiter memory, privacy-limited analytics, and production Grounding Lite
configuration.
- Added a same-origin, BYO-key Gemini gateway for Hairstyle AI Studio. Visitor
keys stay in React memory, pass transiently in a request header, and are
never stored or included in analytics. The gateway validates image data and
Expand All @@ -16,6 +20,7 @@ All notable changes to this project will be documented in this file.
- Pinned the Cloud Run service to `--max-instances 1`. The gateway's in-memory per-IP rate limiters (private-demo auth brute-force, and the spend limits in front of Isochrones, Gemini, and Resend) are only correct on a single instance, but the deploy passed no instance cap and Cloud Run's default is 100, so under load every limit silently became per-instance. Also pinned `--concurrency`, `--memory`, and `--cpu` at their current defaults so a platform default change cannot raise cost or dilute the limits again.

### Added
- Released Real World Reasoning Agent as a first-party open-source Fieldwork Lab from the explicitly authorized private snapshot at `68e8c34547066a984ccb97f5b587caeb97561ec1`. The reviewed source, tests, eval fixtures, guarded Maps/Gemini proxy logic, and provenance now live under `demos/real-world-reasoning-agent/`; the old repository's visibility and settings were not changed.
- Imported Hairstyle AI Studio from its public upstream repository at
`9ea2c0f31e5e1d252220ede6731b655bf2fb8fba`, hosted it at
`/hairstyle-ai-studio/`, and placed it third in the homepage Labs order.
Expand All @@ -28,6 +33,8 @@ All notable changes to this project will be documented in this file.
mobile selection flow. The existing merge-time Buffer workflow now accepts
validated one-off release drafts and stages them for approval without
publishing.
- Added a follow-up Buffer draft for the five-free-generations tier and the
corrected personal-key fallback. It remains editable and unpublished.
- Added frozen development/selection eval suites for responsive design,
portfolio content/design/review, Google Maps Platform, and the skill
improvement workflow. The deterministic gate now validates eval ownership,
Expand Down Expand Up @@ -55,6 +62,7 @@ All notable changes to this project will be documented in this file.
- Added gateway test coverage verifying that all external image, font, and avatar hosts loaded by demo apps are permitted by their respective CSP policies.

### Changed
- Reworked Hairstyle AI Studio's Gemini access to match the proven hosted-plus-BYOK pattern: each client IP receives five successful image generations per UTC day, recommendation analysis stays outside that spend cap, and a validated memory-only personal key bypasses the shared allowance while retaining a separate abuse guard. The UI now opens directly into the studio, shows remaining free generations, and distinguishes shared exhaustion from personal-key provider quota.
- Updated Hairstyle AI Studio to current compatible dependencies with a clean
audit, routed optional recommendations to `gemini-3.5-flash-lite`, retained
`gemini-3.1-flash-lite-image` for image-capable generation, and replaced
Expand Down
50 changes: 50 additions & 0 deletions LEARNINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,56 @@

This log captures durable lessons discovered while building and maintaining the portfolio and demo lab, keeping the root instructions lean.

## 2026-07-27 - Per-user free tiers still need a global spend ceiling

Context: IP allowances limited ordinary use but an IP-rotating caller could
still spend the hosted provider key, and imported Maps proxy limits were sized
for load testing rather than a public portfolio.
Learning: Separate per-user fairness, process-wide daily spend ceilings, and
provider-side quotas. Pin exact upstream routes and methods; same-origin checks
alone are not caller authentication.
Evidence: Hairstyle now has per-IP and global hosted-generation caps; Real
World Reasoning has separate request classes, daily Maps/Gemini ceilings,
bounded limiter memory, exact route/method allowlists, and focused regressions.
Use next time: Define the maximum affordable provider usage before exposing a
public proxy, then make cloud quotas the hard backstop for restart and botnet
scenarios.

## 2026-07-27 - A private-to-open-source migration needs explicit release provenance

Context: Real World Reasoning Agent was still private when its owner requested
that Fieldwork become the first-party open-source home. The public-import
command correctly refused to accept a false public-source confirmation.
Learning: Explicit owner authorization can release a reviewed snapshot without
changing the predecessor repository's visibility, but the boundary must remain
auditable: pin the exact commit, exclude Git history, environment and deployment
files, scan for credentials, record the source visibility and release authority,
and make the new repository the canonical public link.
Evidence: `demos/real-world-reasoning-agent/PROVENANCE.md` records private source
commit `68e8c34547066a984ccb97f5b587caeb97561ec1`; the pre-import scan found no
credentials; and the public tree excludes history, environment files, and
private deployment configuration.
Use next time: Never pass `--confirm-source-public` for a private source. Require
explicit release authority, a clean snapshot scan, and provenance that explains
the exception before public integration.

## 2026-07-27 - Hosted allowance and personal-key abuse limits are different controls

Context: Hairstyle AI Studio put every image request behind one five-per-hour
IP limiter. Entering a personal Gemini key therefore did not help: the gateway
rejected the request before it inspected which credential would pay for it.
Learning: Select and validate the credential before applying spend controls.
Hosted calls consume the shared daily allowance; valid personal-key calls
bypass that spend cap but retain a separate, generous site-abuse limiter.
Malformed personal keys fail closed instead of silently falling back. Provider
quota errors need a different code from local allowance exhaustion so the UI
can offer the right recovery.
Evidence: Gateway regressions cover five successful hosted image generations,
UTC reset metadata, failed-request refunds, personal-key bypass, malformed-key
denial, non-generating key validation, and distinct upstream quota responses.
Use next time: Model shared budget, provider quota, and gateway abuse as
separate policies, then test routing order across every credential source.

## 2026-07-27 - Buffer staging inputs need an explicit merge trigger

Context: The existing Buffer workflow could stage social copy only when a new
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The narrative is intentionally grounded: solution architecture, developer experi
* **[Air Quality Map](demos/aqi-map/README.md)**: inspect live Air Quality API heatmap tiles and point conditions on a 2D Google map.
* **[Isochrones](demos/isochrones/README.md)**: analyze delivery, commute, and response reachability with live-regenerating isochrone bands.
* **Infographic Agent**: an external portable-skill experiment linked from Ryan’s Lab.
* **Atlas, Real World Reasoning Agent**: an external map-agent experiment linked from Ryan’s Lab.
* **Atlas, Real World Reasoning Agent**: a first-party open-source map agent hosted with Fieldwork.

## Local development

Expand Down
18 changes: 12 additions & 6 deletions apps.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
"providers": [
"gemini"
],
"source_url": "https://github.com/ryanbaumann/hairstyle-ai-studio",
"source_url": "https://github.com/ryanbaumann/fieldwork/tree/main/demos/hairstyle-ai-studio",
"source_ref": "9ea2c0f31e5e1d252220ede6731b655bf2fb8fba",
"tags": [
"gemini-api",
Expand All @@ -116,20 +116,26 @@
"name": "real-world-reasoning-agent",
"title": "Atlas - Real World Reasoning Agent",
"description": "An interactive map agent that handles real-world geo reasoning problems using Maps APIs.",
"path": "https://atlas-demo-561209038703.us-central1.run.app/",
"path": "/real-world-reasoning-agent/",
"dev_build_dir": "demos/real-world-reasoning-agent/dist",
"source": {
"type": "external"
"type": "workspace",
"package": "demos/real-world-reasoning-agent",
"output": "dist"
},
"api": {
"type": "none"
"type": "gateway",
"prefix": "/api/real-world-reasoning-agent/"
},
"csp": "maps",
"tags": [
"gemini-api",
"agents",
"google-maps-platform"
],
"preview": "/previews/real-world-reasoning-agent.webp",
"hideOnHome": true
"source_url": "https://github.com/ryanbaumann/fieldwork/tree/main/demos/real-world-reasoning-agent",
"source_ref": "68e8c34547066a984ccb97f5b587caeb97561ec1",
"preview": "/previews/real-world-reasoning-agent.webp"
},
{
"name": "aqi-map",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ Use this skill when changing this repository's Gemini model integration, UX flow
## Secret checklist
- Never put Gemini keys in `VITE_*` variables, source, local storage, IndexedDB,
logs, analytics, URLs, or committed environment files.
- Visitors bring their own key at runtime. Keep it only in React memory and
pass it transiently in `X-Gemini-API-Key` to the same-origin
`/api/hairstyle-ai-studio/*` gateway routes.
- The gateway forwards the caller's key only to Gemini. It must not store,
return, log, or replace it with a shared deployed key.
- Use the server-side `GEMINI_API_KEY` for the shared allowance of five
successful image generations per client IP per UTC day. Do not charge the
allowance for recommendation analysis or failed generation requests.
- Let visitors override the shared key at runtime. Validate a personal key
through the non-generating proxy endpoint before activation, keep it only in
React memory, and pass it transiently in `X-Gemini-API-Key`.
- A valid personal key bypasses the shared spend cap but not the separate
gateway abuse limiter. A supplied malformed or rejected key must fail closed;
never silently replace it with the shared deployed key.
- The gateway forwards the selected key only to Gemini. It must not store,
return, log, or send it to analytics.
- Update the app README, privacy copy, gateway tests, and rate-limit tests when
environment or request behavior changes.

Expand Down
6 changes: 4 additions & 2 deletions demos/hairstyle-ai-studio/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ Model IDs are server-owned in the repository root's `gateway/lib/hairstyleAi.js`

## Secret handling
- Never add `VITE_GEMINI_API_KEY`; Vite would publish it in the browser bundle.
- Users provide their own key at runtime. Keep it only in React memory and pass it transiently in `X-Gemini-API-Key` to the same-origin `/api/hairstyle-ai-studio/*` gateway routes.
- The gateway must never log, store, return, or send the key to analytics.
- The server-side `GEMINI_API_KEY` owns the five-successful-image-generations-per-IP daily allowance. Recommendation analysis does not consume it.
- A user may override the shared key at runtime. Validate the personal key through the non-generating proxy endpoint before activation, keep it only in React memory, and pass it transiently in `X-Gemini-API-Key`.
- A valid personal key bypasses the shared daily spend cap but remains subject to the gateway's separate abuse limiter. Never silently fall back to the shared key when a supplied personal key is malformed or rejected.
- The gateway must never log, store, return, or send either key to analytics.
- Never commit real API keys or screenshots containing keys.

## UX rules
Expand Down
8 changes: 5 additions & 3 deletions demos/hairstyle-ai-studio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ commit `9ea2c0f31e5e1d252220ede6731b655bf2fb8fba`.

- The React, Vite, Tailwind, local-history, result, refinement, and salon-brief flows remain app-local.
- The standalone Express server and Docker image were removed. Fieldwork's zero-dependency gateway owns `/api/hairstyle-ai-studio/*`.
- Visitors bring their own Gemini API key. The browser keeps it only in React memory and sends it in a transient same-origin request header. The gateway does not store, log, return, or analyze it.
- The gateway validates image data, caps request size and prompt length, requires same-origin browser mutations, and rate-limits text and image requests by client IP.
- The shared server key provides five successful image generations per client IP per UTC day. Recommendation analysis does not consume that allowance.
- Visitors can connect a personal Gemini API key after the shared allowance is exhausted or at any time. The proxy validates it without generating content; the browser keeps it only in React memory and sends it in a transient same-origin request header.
- Personal-key calls bypass the shared spend cap but retain a generous per-IP abuse guard. The gateway never stores, logs, returns, or analyzes a visitor key.
- The gateway validates image data, caps request size and prompt length, requires same-origin browser mutations, and separates shared allowance, visitor-key quota, and site-abuse errors.
- Google Analytics uses Fieldwork's privacy-limited shared loader. Events contain only enumerated funnel states, never keys, photos, prompts, style URLs, filenames, IDs, or raw errors.

## Models
Expand Down Expand Up @@ -53,7 +55,7 @@ Gateway behavior and rate limits are tested from `gateway/test/hairstyleAi.test.

## Privacy

Photos are sent to Google Gemini only after the visitor explicitly requests a recommendation, generation, or refinement. Fieldwork does not store those photos on the server. Generated history is stored in the browser's IndexedDB and can be deleted from the app.
Photos are sent to Google Gemini only after the visitor explicitly requests a recommendation, generation, or refinement. Fieldwork does not store those photos on the server. The shared allowance keeps an in-memory daily generation count keyed by client IP; a service restart can reset it early. Generated history is stored in the browser's IndexedDB and can be deleted from the app.

## License

Expand Down
Loading
Loading