Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Security and automation boundaries require GoodDollar maintainer review.
/.github/workflows/ @GoodDollar/goodbuilders-maintainers
/.github/agents/ @GoodDollar/goodbuilders-maintainers
/scripts/widget-release/ @GoodDollar/goodbuilders-maintainers
/src/widgets/ @GoodDollar/goodbuilders-maintainers
/src/login/ @GoodDollar/goodbuilders-maintainers
/config.ts @GoodDollar/goodbuilders-maintainers
/configServerless.ts @GoodDollar/goodbuilders-maintainers
/next.config.mjs @GoodDollar/goodbuilders-maintainers
44 changes: 44 additions & 0 deletions .github/agents/widget-integrator.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
name: widget-integrator
description: Integrates a verified stable GoodWidget release into GoodWallet as a human-reviewed draft PR.
tools: ["read", "edit", "search", "shell"]
---

Integrate only the package and version in the validated task prompt.

The widget package executes on the GoodWallet origin, so the generated pull
request is an implementation proposal—not an authorization or merge decision.

The release descriptor supplies only immutable widget identity and supported
package entry points. It does not authorize provider methods, choose Wallet
presentation, or describe widget-specific UI states.

For a new widget, add the exact dependency, lockfile entry, reviewed
Wallet-owned registry metadata, static loaders for both declared entries,
authenticated route, existing restricted EIP-1193 provider boundary,
translations, and host-level tests. Omit `integrationMode` to use the
Web Component default unless a maintainer explicitly requests React.

For an existing widget, update the exact dependency, lockfile, and registry
`packageVersion`. Preserve its reviewed integration mode, route, icon,
presentation, and `providerPolicy`; change host integration/tests only when the
release requires it. Select an existing GoodWallet icon or add one local SVG
and identify the choice as a human-review item.

Do not copy widget loading, success, error, selector, fixture, callback, or
event contracts into GoodWallet. Those behaviors remain inside the package.

You must not:

- modify authentication/session internals;
- modify deployment configuration or any file under `.github/workflows`;
- modify `src/widgets/provider/policy.ts` or broaden provider permissions;
- treat descriptor fields or package behavior as permission to broaden a
widget's Wallet-owned `providerPolicy`;
- expose a signer, private key, master seed, or unrestricted RPC provider;
- use a dependency range, approve a PR, or merge.

Run dependency validation, build, unit tests, and relevant Playwright tests.
Record deterministic desktop/mobile evidence. Open a draft pull request, label
it `automated-widget-integration`, include the package/version idempotency key
in its title/body, and request CODEOWNERS review.
4 changes: 4 additions & 0 deletions .github/workflows/goodwallet-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ jobs:
- name: Run install
run: yarn install --immutable

- name: Validate exact GoodWidget dependencies
# Widgets execute in-process, so PRs must pin the bytes reviewers audit.
run: yarn widget-deps:check

- name: Run Biome Check
uses: borales/actions-yarn@v5
with:
Expand Down
86 changes: 86 additions & 0 deletions .github/workflows/goodwidget-released.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Intake GoodWidget release

on:
repository_dispatch:
types: [goodwidget-released]
workflow_dispatch:
inputs:
payload:
description: Validated GoodWidget release envelope JSON
required: true
type: string
action:
description: Replay action
required: true
type: choice
default: validate
options:
- validate
- issue
- agent-task
confirm_agent_task:
description: I confirm this may create a Copilot task and draft PR
required: false
type: boolean
default: false

permissions:
contents: read

# Serialize the validation/task handoff so two deliveries cannot race past the
# idempotency search before its durable tracking issue exists.
concurrency:
group: goodwidget-integration
cancel-in-progress: false

jobs:
validate:
name: Validate sender, descriptor, and npm release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache: yarn
- run: corepack enable && yarn install --immutable
- name: Validate exact GoodWidget dependencies
run: yarn widget-deps:check
- name: Validate release envelope and npm integrity
env:
GOODWIDGET_DISPATCH_ACTOR: ${{ vars.GOODWIDGET_DISPATCH_ACTOR }}
run: yarn node scripts/widget-release/process-event.mjs validate

integrate:
name: Create reviewed integration task
needs: validate
# Repository dispatch is inert until maintainers explicitly enable the
# feature flag; manual agent-task replay also requires confirmation.
if: >-
(github.event_name == 'repository_dispatch' &&
vars.ENABLE_GOODWIDGET_AGENT_TASKS == 'true') ||
(github.event_name == 'workflow_dispatch' &&
(inputs.action == 'issue' ||
(inputs.action == 'agent-task' && inputs.confirm_agent_task)))
runs-on: ubuntu-latest
# This protected environment gates the PAT used by the preview Agent Tasks
# API and keeps it out of pull-request workflows.
environment: widget-integration
permissions:
contents: read
issues: write
pull-requests: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache: yarn
- run: corepack enable && yarn install --immutable
- name: Process release
env:
ACTION_MODE: ${{ github.event_name == 'repository_dispatch' && 'agent-task' || inputs.action }}
GOODWIDGET_DISPATCH_ACTOR: ${{ vars.GOODWIDGET_DISPATCH_ACTOR }}
GITHUB_TOKEN: ${{ github.token }}
COPILOT_AGENT_TASK_PAT: ${{ secrets.COPILOT_AGENT_TASK_PAT }}
run: yarn node scripts/widget-release/process-event.mjs "$ACTION_MODE"
944 changes: 944 additions & 0 deletions .yarn/releases/yarn-4.17.1.cjs

Large diffs are not rendered by default.

135 changes: 135 additions & 0 deletions docs/widget-integration-automation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# GoodWidget integration automation

GoodWallet accepts the strict GoodWidget package integration descriptor `2.0.0`.
The executable consumer schema is `scripts/widget-release/contract.mjs`; the
canonical producer schema is
`GoodWidget/schemas/widget-integration-manifest.schema.json`.

## Shared contract

The descriptor deliberately advertises only immutable identity and importable
package entry points:

```json
{
"schemaVersion": "2.0.0",
"hostContractVersion": "1.0.0",
"widgetId": "goodwidget.goodreserve",
"packageName": "@goodwidget/goodreserve-widget",
"entries": {
"react": { "export": "GoodReserveWidget" },
"webComponent": {
"registerPath": "./register",
"tagName": "gw-goodreserve-widget"
}
}
}
```

The release envelope contains the values that exist only after npm publication:

```json
{
"descriptor": "<the package integration descriptor>",
"version": "1.2.3",
"integrity": "sha512-...",
"sourceSha": "<40-character commit SHA>",
"releaseUrl": "https://github.com/GoodDollar/GoodWidget/tree/<package-tag>",
"idempotencyKey": "@goodwidget/package@1.2.3"
}
```

Wallet routes, labels, icons, integration-mode selection, provider permissions,
and tests are not controlled by this payload. The intake validates the
configured service-user actor, schema and host-contract compatibility, package
scope, npm `latest`, exact npm version/integrity, duplicate issues/tasks, and
the current registry. It contains no release secret in pull-request jobs.

## Integration-mode selection

GoodWallet supports both declared package entries through generic hosts. Web
Components are the default; a reviewed registry entry may override one widget
to React:

```ts
{
widgetId: "goodwidget.goodreserve",
packageName: "@goodwidget/goodreserve-widget",
packageVersion: "1.2.3",
integrationMode: "react", // omit to use the Web Component default
entries: {
react: {
exportName: "GoodReserveWidget",
load: () => import("@goodwidget/goodreserve-widget"),
},
webComponent: {
tagName: "gw-goodreserve-widget",
load: () => import("@goodwidget/goodreserve-widget/register"),
},
},
// Wallet-owned route, presentation and providerPolicy follow.
}
```

Both loaders are added for a new widget so changing mode later is a Wallet-only
reviewed change. Both hosts receive the same restricted provider, theme
overrides, and public config. The Web Component host registers only in the
browser and assigns objects as properties rather than HTML attributes.

The intake explicitly classifies work before starting Copilot:

- absent registry entry → `new`, requiring a full integration pass;
- registered package with an older exact dependency → `update`, normally only
version, lockfile, and registry-version changes;
- registered package at the exact released version → no task.

## Security ownership

GoodWallet exclusively owns:

- the restricted EIP-1193 method and chain allowlists;
- any narrower per-widget provider policy;
- authenticated routing and session access;
- exact installed versions and lockfile integrity;
- dashboard presentation and integration-mode selection;
- CODEOWNERS review and the merge decision.

The descriptor cannot request or broaden provider permissions. Widget-specific
loading, success, error, callbacks, events, fixtures, and selectors stay inside
GoodWidget and are not reproduced in the Wallet registry.

## Activation gate

Automatic task creation is intentionally disabled until all external
prerequisites are complete. Repository dispatch always validates, but reaches
the protected `widget-integration` environment only when
`ENABLE_GOODWIDGET_AGENT_TASKS` is exactly `true`. Before setting it:

1. Reconcile and merge dashboard PR #24 and mobile-overflow PR #39. PR #22 is
unrelated (`frame-ancestors` only); #39's old assertion predates #24's
grid/More drawer.
2. Protect `widget-integration` with required maintainer approval.
3. Configure `GOODWIDGET_DISPATCH_ACTOR` to the dedicated dispatch service user.
4. Confirm Copilot Business/Enterprise and configure its fine-grained
`COPILOT_AGENT_TASK_PAT` with only `Agent tasks: write` for GoodWallet.
5. Publish and npm-verify GoodReserve, then use manual `workflow_dispatch` with
`action=agent-task` and the explicit confirmation. Confirm it creates a
reviewable draft PR.

The Agent Tasks API is public preview. If it or the entitlement/token is
unavailable, processing creates a structured issue. Manual replay supports
validation, structured-issue fallback, and an explicitly confirmed agent task.
All modes deduplicate on `<package>@<version>`. Intake runs are serialized and
create a durable tracking issue before calling Agent Tasks, preventing a replay
from starting a second task during the gap before the first task opens its PR.

## Review boundary

The custom agent may edit a widget integration, route, icon, exact dependency,
lockfile, Wallet-owned registry entry, and host-level tests. It cannot edit
authentication/session internals, deployment/workflow configuration, or the
provider allowlist. Generated code runs on the Wallet origin, so CODEOWNERS
review and manual merge are mandatory.

No widget is pre-registered before a verified stable npm release. The initial
GoodReserve integration remains reserved for the end-to-end rehearsal.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"lint": "next lint",
"test": "vitest run",
"test:e2e": "playwright test",
"test:e2e:screenshots": "playwright test --update-snapshots",
"test:e2e:screenshots": "playwright test",
"widget-deps:check": "node scripts/widget-release/check-exact-dependencies.mjs",
"biome:verify:all": "biome check",
"biome:fix:staged": "biome check --write --no-errors-on-unmatched --staged",
"biome:fix:all": "biome check --write --no-errors-on-unmatched",
Expand Down
28 changes: 28 additions & 0 deletions scripts/widget-release/check-exact-dependencies.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import fs from "node:fs/promises"

const packageJson = JSON.parse(
await fs.readFile(new URL("../../package.json", import.meta.url), "utf8"),
)
const sections = ["dependencies", "devDependencies", "optionalDependencies"]
const invalid = []

// In-process widgets run with the wallet's origin and session capabilities.
// Exact versions make the reviewed package bytes reproducible via the lockfile.
for (const section of sections) {
for (const [name, version] of Object.entries(packageJson[section] ?? {})) {
if (
name.startsWith("@goodwidget/") &&
!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(version)
) {
invalid.push(`${section}.${name}=${version}`)
}
}
}

if (invalid.length > 0) {
throw new Error(
`GoodWidget packages must use exact stable versions:\n${invalid.join("\n")}`,
)
}

console.log("GoodWidget dependency versions are exact.")
23 changes: 23 additions & 0 deletions scripts/widget-release/classify-task.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Convert repository state into one explicit Copilot task type.
*
* package.json is authoritative for the installed version; registry.ts proves
* that the package is actually exposed as a Wallet widget. Checking both
* avoids matching an unrelated widget that happens to use the same version.
*/
export function classifyIntegrationTask({
registrySource,
packageName,
installedVersion,
targetVersion,
}) {
const packageAlreadyRegistered = registrySource.includes(
`packageName: "${packageName}"`,
)

return {
alreadyIntegrated:
packageAlreadyRegistered && installedVersion === targetVersion,
integrationTaskType: packageAlreadyRegistered ? "update" : "new",
}
}
Loading
Loading