Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ab7b419
Update LICENSE
w1kke Feb 19, 2026
d3525d6
feat: implement ERC-8004 atlas districts and storefront opt-out flow
w1kke Feb 21, 2026
530e26a
feat(scripts): add ERC-8004 refresh, prompt, ingest, and Nano Banana …
w1kke Feb 21, 2026
c7bffed
chore: checkpoint existing portal changes
w1kke Feb 21, 2026
1d1eb41
feat: implement experience trainer milestones and stabilize onboardin…
w1kke Feb 21, 2026
371b960
feat: open experience trainer in town modal
w1kke Feb 22, 2026
8ea0747
fix: simplify ceremony modal embed and clarify canvas skill guidance
w1kke Feb 22, 2026
763d72f
Add trainer trace delete controls and clear-all
w1kke Feb 22, 2026
9b48dde
Add trainer tool lab and transcript integrity diagnostics
w1kke Feb 22, 2026
63277dc
docs(spec): add dynamic skill action dictionary TDD roadmap
w1kke Feb 22, 2026
c5492a2
Redesign Atlas into map-first modal marketplace
w1kke Feb 22, 2026
39f2013
chore: commit all pending workspace changes
w1kke Feb 22, 2026
f6d3b4e
feat(trainer): add plugin-based dynamic skill actions and diagnostics
w1kke Feb 22, 2026
4b8a1a3
Merge pull request #20 from Agent-Town/codex/erc8004_atlas
w1kke Feb 22, 2026
113dff2
feat(trainer): implement trainer namespace plugin milestones
w1kke Feb 22, 2026
9a13c6a
ignore data folder
w1kke Feb 22, 2026
3f5e5ef
fix(atlas): recover districts without chains table and clarify missin…
w1kke Feb 23, 2026
1b1c555
Merge atlas storefronts into experience trainer and add Next prototype
w1kke Feb 23, 2026
491d4cf
Add Auto Whisk prompt export and download remap scripts
w1kke Feb 23, 2026
0c98f89
Update atlas UI layout
w1kke Feb 24, 2026
dfd90c1
Investigate slow paint calls
w1kke Feb 25, 2026
baf467b
Review town-ui-openclawlite branch
w1kke Feb 25, 2026
e013618
Use Agent-Town forked SDK bundle and remove CDN fallback
w1kke Feb 26, 2026
1a620cf
Merge pull request #22 from Agent-Town/codex/agent0-fork-local-bundle
w1kke Feb 26, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store

data/
data/store.e2e.json
data/store.json
data/store*.sqlite
Expand Down
8 changes: 8 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[submodule "vendors/openclaw-lite-main/vendor/openclaw-main"]
path = vendors/openclaw-lite-main/vendor/openclaw-main
url = https://github.com/openclaw/openclaw.git
branch = main

[submodule "vendors/agent0-ts"]
path = vendors/agent0-ts
url = https://github.com/Agent-Town/agent0-ts.git
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This repo is a **minimal** Agent Town landing page.
2. **Human + agent co-op** — the unlock flow requires both participants.
3. **Session-token identity** — do not add external identity providers.
4. **Deterministic testability** — every milestone must be verifiable with Playwright.
5. **Wallet-first identity** — each user is represented by their connected wallet; wallet continuity drives session continuity.

## Non-goals / constraints

Expand Down Expand Up @@ -110,6 +111,7 @@ When changing worker behavior required by skill files (`skill.md` / `SKILL.md`):

### 6) Session and identity guardrails

- The user identity is the connected wallet (or wallets), not a transient browser credential.
- Team Code is a session token/routing token and should stay hidden from cluttered UX surfaces.
- Team/session identity should be stable across polling/refresh for a live session; avoid regressions that rotate it unexpectedly.

Expand Down
875 changes: 674 additions & 201 deletions LICENSE

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,31 @@ npm run setup:sepolia-wallet -- --no-faucet
- Co-op actions: `/api/agent/canvas/paint`, `/api/agent/house/*`.
- House API auth and ceremony details are documented in `specs/02_api_contract.md`.

## Trainer namespace toggle
You can enable/disable `trainer.*` tools with either URL params or localStorage.

- URL query param:
- `?trainerNamespace=1` enables
- `?trainerNamespace=0` disables
- aliases also supported: `trainer_namespace`, `trainer-tools`, `trainerTools`
- localStorage override key: `agentTown:feature:trainerNamespace`
- set `"true"` to enable
- set `"false"` to disable
- remove the key to clear the override

Browser console examples:

```js
localStorage.setItem('agentTown:feature:trainerNamespace', 'true'); // enable
localStorage.setItem('agentTown:feature:trainerNamespace', 'false'); // disable
localStorage.removeItem('agentTown:feature:trainerNamespace'); // clear override
```

Resolution precedence:
- localStorage override
- URL query param
- default behavior

## Key routes
- `/` — onboarding, Team Code, token check, reconnect.
- `/start` — start page (logo/video/welcome + Enter -> Privy login).
Expand Down Expand Up @@ -148,3 +173,4 @@ Unlocking a house in the UI is gated by a Privy-backed Solana wallet signature.
- API contract: `specs/02_api_contract.md`
- Experience flow: `specs/01_experience_flow.md`
- TDD milestones: `specs/04_tdd_milestones.md`
- District map + storefront: `specs/11_district_map_storefront_spec.md`
17 changes: 17 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ Mind settings are included in agent state backup/restore in the house flow:
- If you switched providers, verify `Model ID` updated to a model supported by that provider.
- `test-local` is for deterministic tests and not real model inference.

### Trainer namespace toggle

Use either query params or localStorage:

- URL: `?trainerNamespace=1` (enable) or `?trainerNamespace=0` (disable)
- localStorage key: `agentTown:feature:trainerNamespace` with value `"true"` or `"false"`

Console helper:

```js
localStorage.setItem('agentTown:feature:trainerNamespace', 'true');
localStorage.setItem('agentTown:feature:trainerNamespace', 'false');
localStorage.removeItem('agentTown:feature:trainerNamespace');
```

Precedence: localStorage override, then URL param, then default.

## Next

- Decision guide: [Which Provider Should I Pick?](/docs/which-provider.md)
Expand Down
21 changes: 20 additions & 1 deletion docs/internal-skill-testline.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Status: Active
Audience: Engineering only
Last updated: 2026-02-18
Last updated: 2026-02-22

## Goal

Expand Down Expand Up @@ -77,9 +77,28 @@ Keep `skill.md` evolution testable as we:
| Session restore path works when cookie is missing but `x-team-code-hint` is available | `public/app.js`, `public/house.js`, `server/index.js` | client auto-sends team-code hint header and server rebinds same team session identity | `e2e/57_phase3_onboarding_wallet_llm_persist.spec.js` (`state endpoint restores session via team code hint when cookie is missing`) |
| Truthful local “agent active” readiness | `public/app.js` + worker `skillState` bridge | OpenClaw Lite suppresses “ready” when skill import state is failed; `/skill.md` auto-import is attempted after local connect and failures are surfaced | `e2e/57_phase3_onboarding_wallet_llm_persist.spec.js` (`agent readiness status tracks skill import failure and recovery`) |
| House-page OpenClaw export compatibility | `public/house.js` | download delegates to local worker `gateway.command.exportZip` (`openclaw-lite-export.zip`); upload accepts OpenClaw export and legacy house backup zip formats | `e2e/54_agent_state_backup_restore.spec.js` (`house backup stores encrypted state and supports ZIP download/upload restore`) |
| Trainer namespace discovery + feature flag gating | `public/trainer_namespace_plugin.js`, `public/trainer.js`, `public/app.js`, `server/index.js` | plugin-constrained `trainer.*` registry is visible only when `featureFlags.trainerNamespace` resolves true | `e2e/98_trainer_namespace_contract_harness.spec.js` (`trainer namespace tools are discoverable when enabled and hidden when disabled`) |
| Trainer namespace read-only introspection tools | `public/trainer_namespace_plugin.js`, `public/trainer.js` | `trainer.list_runs`, `trainer.get_run`, `trainer.get_event`, `trainer.get_session_context` return deterministic payloads | `e2e/99_trainer_namespace_read_tools.spec.js` (`trainer namespace read tools return deterministic run, event, and session context payloads`) |
| Trainer namespace dynamic action catalog bridge | `public/trainer_namespace_plugin.js`, `public/trainer.js`, `public/skill_actions_plugin.js` | `trainer.list_actions` reflects active skill action extraction and switches atomically across skill changes | `e2e/100_trainer_namespace_action_catalog.spec.js` (`trainer.list_actions reflects active skill and updates after skill switch`) |
| Trainer namespace action invocation bridge | `public/trainer_namespace_plugin.js`, `public/trainer.js`, `public/skill_actions_plugin.js` | `trainer.invoke_action` executes skill action requests with provided params and deterministic failure codes | `e2e/101_trainer_namespace_invoke_action.spec.js` (`trainer.invoke_action validates inputs and executes action requests with provided params`) |
| Trainer namespace evidence freshness + expiry loop | `public/trainer_namespace_plugin.js`, `public/trainer.js` | `trainer.list_evidence` supports deterministic freshness filtering and expiry-window evaluation | `e2e/102_trainer_namespace_evidence_loop.spec.js` (`trainer.list_evidence supports deterministic freshness and expiry windows after invoke_action`) |
| Trainer namespace transcript integrity + not-used diagnostics | `public/trainer_namespace_plugin.js`, `public/trainer.js`, `public/app.js` | `trainer.get_transcript_integrity` and `trainer.explain_not_used` surface reason codes and align with Session Context diagnostics | `e2e/103_trainer_namespace_diagnostics.spec.js` (`trainer diagnostics tools expose transcript integrity + not-used reasons and align with Session Context diagnostics`) |
| Trainer namespace approval-gated destructive tools | `public/trainer_namespace_plugin.js`, `public/trainer.js` | `trainer.delete_trace` / `trainer.clear_traces` require one-time approval tokens with TTL and deterministic failure paths | `e2e/104_trainer_namespace_approval_gate.spec.js` (`trainer destructive tools require approval token, allow one operation, and expire deterministically`) |
| Trainer namespace budgets + recursion guards | `public/trainer_namespace_plugin.js` | per-turn/per-window budgets and recursion blocking enforce deterministic `TRAINER_RATE_LIMITED` and `TRAINER_RECURSION_BLOCKED` outcomes | `e2e/105_trainer_namespace_rate_limit_recursion.spec.js` (`trainer namespace enforces rate limits and blocks recursive dispatch attempts deterministically`) |
| Trainer namespace redaction in diagnostics and debug panes | `public/trainer_namespace_plugin.js`, `public/app.js` | secret-like values are masked in trainer namespace outputs/audit snapshots and agent debug surfaces | `e2e/106_trainer_namespace_redaction.spec.js` (`trainer namespace redacts secret-like values from diagnostics and avoids leaking raw secrets into debug panes`) |
| Trainer namespace human-agent coop verification loop | `public/trainer_namespace_plugin.js`, `public/trainer.js` | builder demonstration + repeat invocation + evidence-backed verification flow remains deterministic in trainer tooling | `e2e/107_trainer_namespace_coop_canvas.spec.js` (`trainer namespace supports a deterministic human-agent coop loop for canvas verification`) |

## Progress Log

### 2026-02-22

- Added plugin-constrained `trainer.*` namespace coverage for discovery, read tools, dynamic action catalogs, and action invocation (`e2e/98` to `e2e/101`).
- Added deterministic trainer namespace evidence lifecycle coverage (`freshOnly` and expiry windows) via `e2e/102_trainer_namespace_evidence_loop.spec.js`.
- Added transcript-integrity and not-used diagnostics parity coverage between trainer tools and Session Context tab (`e2e/103_trainer_namespace_diagnostics.spec.js`).
- Added approval-gated destructive trainer tool coverage with one-time token + TTL semantics (`e2e/104_trainer_namespace_approval_gate.spec.js`).
- Added trainer namespace policy guard coverage for per-turn/window rate limits and recursion blocks (`e2e/105_trainer_namespace_rate_limit_recursion.spec.js`).
- Added redaction coverage for trainer diagnostics/debug panes and cooperative canvas verification loop coverage (`e2e/106_trainer_namespace_redaction.spec.js`, `e2e/107_trainer_namespace_coop_canvas.spec.js`).

### 2026-02-18

- Added deterministic PKCE OAuth contract coverage for OpenAI Codex (`start`/`status`/`exchange`) and state-rebind recovery when `attemptId` is stale.
Expand Down
87 changes: 87 additions & 0 deletions docs/rate-limits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Rate Limits (Current State)

## Summary

- `/api/state` is **not** rate-limited by middleware.
- `/api/state` no longer performs `readStore()` and no longer returns signup/public-team stats.
- Most explicit request throttling is in `server/index.js` middleware and Pony inbox flow.
- There is also a worker-side tool limiter for `http_request` fallback proxy calls.

## `/api/state` hot path (current)

Route:
- `/Users/robin/.codex/worktrees/d83e/Portal/server/index.js` (`app.get('/api/state', ...)`)

Current behavior:
- Uses only in-memory session state (`ensureHumanSession`, lite/onboarding/ceremony/experience snapshots).
- Does not read global store tables per request.
- Does not compute `stats.signups` or `stats.publicTeams`.

Implication:
- `/api/state` latency should now reflect normal request handling, not full-store JSON deserialization.

Note:
- `/api/session` and `/api/session/reset` still read store stats today and are separate from `/api/state`.

## Active server rate limits

Middleware implementation:
- `/Users/robin/.codex/worktrees/d83e/Portal/server/index.js` (`rateLimit({ windowMs, max, keyFn })`)

Headers when active:
- `X-RateLimit-Limit`
- `X-RateLimit-Remaining`
- `X-RateLimit-Reset`
- On reject: `Retry-After`, response `429 { ok: false, error: 'RATE_LIMITED' }`

Configured middleware limits:

| Scope | Window | Max | Key |
|---|---:|---:|---|
| `/api/agent` | 60s | 1200 | `agent:${req.ip}` |
| `/api/house` | 60s | 180 | `house:${req.ip}` |
| `/api/share/create` | 60s | 60 | `share:${req.ip}` |
| `/api/token` | 60s | 30 | `token:${req.ip}` |
| `/api/wallet` | 60s | 30 | `wallet:${req.ip}` |
| `/api/house/init` | 60s | 20 | `house-init:${req.ip}` |

## Pony message rate limit

Implementation:
- `/Users/robin/.codex/worktrees/d83e/Portal/server/index.js`
- `PONY_RATE_WINDOW_MS = 60_000`
- `PONY_RATE_MAX_PER_PAIR = 20`
- `checkPonyRateLimit({ senderKey, toHouseId })`

Applied in:
- `/api/pony/send`

Behavior:
- Keyed by sender-target pair: `${senderKey}->${toHouseId}`
- On reject: `429 { ok: false, error: 'RATE_LIMITED_PONY', retryAfter }`

## Worker/runtime-side rate limit (OpenClaw)

Implementation:
- `/Users/robin/.codex/worktrees/d83e/Portal/vendors/openclaw-lite-main/src/openclaw-lite/worker.js`
- `HTTP_RATE_LIMIT_WINDOW_MS = 1000`
- `HTTP_RATE_LIMIT_MAX = 50`
- `consumeHttpRateLimit(url)`

Behavior:
- Applies to `http_request`/`web_fetch` proxy fallback path by origin.
- Effective threshold: up to 50 fallback requests per origin per 1-second window.
- Same-origin requests that succeed directly do not hit this limiter.
- Returns tool error code `RATE_LIMIT` with `retryAfterMs`.

Synced runtime artifact:
- `/Users/robin/.codex/worktrees/d83e/Portal/public/openclaw-lite/worker.js`

## Endpoints not currently middleware-rate-limited

Examples:
- `/api/state`
- `/api/session`
- `/api/atlas/*`

These may still be slow due to CPU/IO hot paths (not throttling).
5 changes: 2 additions & 3 deletions e2e/01_home.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { test, expect } = require('@playwright/test');
const { enterHatch } = require('./helpers/phase2');

const resetToken = process.env.TEST_RESET_TOKEN || 'test-reset';

Expand All @@ -7,9 +8,7 @@ test.beforeEach(async ({ request }) => {
});

test('home loads, shows team code and skill link', async ({ page, request }) => {
await page.goto('/');
await page.getByTestId('auth-signin').click();
await expect(page.getByTestId('hatch-panel')).toBeVisible();
await enterHatch(page, 'signin');

await expect(page.getByTestId('team-code')).toHaveText(/TEAM-[A-Z0-9]{4}-[A-Z0-9]{4}/);
await expect(page.getByTestId('skill-link')).toBeVisible();
Expand Down
17 changes: 0 additions & 17 deletions e2e/03_create_share_leaderboard.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,6 @@ function ssePayload(chunks) {
return chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join('') + 'data: [DONE]\n\n';
}

test('co-op open -> co-create -> generate house -> unlock with wallet signature', async ({ page, request }) => {
// Mock a Solana wallet (Phantom-style) for Playwright.
await page.addInitScript(() => {
// Minimal mock matching usage in create.js/house.js
const sig = new Uint8Array(64);
// Deterministic but non-zero
for (let i = 0; i < sig.length; i++) sig[i] = (i * 13) & 0xff;
window.__PRIVY_WALLET_BRIDGE__ = {
connectSolana: async () => ({ address: 'So1anaMock111111111111111111111111111111111' }),
disconnectSolana: async () => {},
signSolanaMessage: async () => ({ signature: sig, publicKey: { toString: () => 'So1anaMock111111111111111111111111111111111' } })
};
});

await page.goto('/');
const teamCode = (await page.getByTestId('team-code').innerText()).trim();

function makeToolChunks({ id, model, toolName, args = {}, callId }) {
const created = Math.floor(Date.now() / 1000);
return [
Expand Down
24 changes: 1 addition & 23 deletions e2e/05_erc8004_mint.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test.beforeEach(async ({ request }) => {
await request.post('/__test__/reset', { headers: { 'x-test-reset': resetToken } });
});

test('ERC-8004 UI stays hidden on house page', async ({ page, request }) => {
test('ERC-8004 UI stays hidden on house page', async ({ page }) => {
// Mock Solana wallet + EVM wallet + ag0 SDK
await page.addInitScript(() => {
// Solana mock
Expand Down Expand Up @@ -47,28 +47,6 @@ test('ERC-8004 UI stays hidden on house page', async ({ page, request }) => {
});

await page.goto('/');
const teamCode = (await page.getByTestId('team-code').innerText()).trim();

// Connect agent
await request.post('/api/agent/connect', { data: { teamCode, agentName: 'ClawTest' } });

// Match
await page.getByTestId('sigil-key').click();
await request.post('/api/agent/select', { data: { teamCode, elementId: 'key' } });

// Press open
await page.getByTestId('open-btn').click();
await request.post('/api/agent/open/press', { data: { teamCode } });
await page.waitForURL('**/create');

// Agent ceremony
// Use randomness to avoid deterministic houseId collisions when tests run in parallel workers.
const ra = crypto.randomBytes(32);
const agentRevealPair = makeCeremonyRevealPair();
const raCommit = crypto.createHash('sha256').update(ra).digest('base64');
const commitResp = await request.post('/api/agent/house/commit', {
data: { teamCode, commit: raCommit, revealPub: agentRevealPair.publicKeyB64 }
});
await reachCreateViaLite(page);

await page.getByTestId('px-0-0').click();
Expand Down
51 changes: 1 addition & 50 deletions e2e/06_agent_house_append.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,7 @@ test.beforeEach(async ({ request }) => {
await request.post('/__test__/reset', { headers: { 'x-test-reset': resetToken } });
});

function sha256(buf) {
return crypto.createHash('sha256').update(buf).digest();
}

function hkdf(ikm, info, len = 32) {
// Node >= 15
return crypto.hkdfSync('sha256', ikm, Buffer.alloc(0), Buffer.from(info, 'utf8'), len);
}

function houseAuthHeaders(houseId, method, path, body, key) {
const ts = String(Date.now());
const bodyHash = crypto.createHash('sha256').update(body || '').digest('base64');
const msg = `${houseId}.${ts}.${method}.${path}.${bodyHash}`;
const auth = crypto.createHmac('sha256', key).update(msg).digest('base64');
return { 'x-house-ts': ts, 'x-house-auth': auth };
}

function aesGcmEncrypt(key32, plaintext, aad) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key32, iv);
if (aad) cipher.setAAD(aad);
const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const tag = cipher.getAuthTag();
// WebCrypto AES-GCM returns ciphertext||tag as one buffer; mirror that.
return { iv, ct: Buffer.concat([ct, tag]) };
}

test('agent derives ceremony key and appends; human can decrypt in house UI', async ({ page, request }) => {
test('agent derives ceremony key and appends; human can decrypt in house UI', async ({ page }) => {
// Mock Solana wallet for unlock UX.
await page.addInitScript(() => {
const sig = new Uint8Array(64);
Expand All @@ -48,28 +21,6 @@ test('agent derives ceremony key and appends; human can decrypt in house UI', as
});

await page.goto('/');
const teamCode = (await page.getByTestId('team-code').innerText()).trim();

// Connect agent
await request.post('/api/agent/connect', { data: { teamCode, agentName: 'ClawTest' } });

// Match
await page.getByTestId('sigil-key').click();
await request.post('/api/agent/select', { data: { teamCode, elementId: 'key' } });

// Press open
await page.getByTestId('open-btn').click();
await request.post('/api/agent/open/press', { data: { teamCode } });
await page.waitForURL('**/create');

// Agent ceremony
// Use randomness to avoid deterministic houseId collisions when tests run in parallel workers.
const ra = crypto.randomBytes(32);
const agentRevealPair = makeCeremonyRevealPair();
const raCommit = sha256(ra).toString('base64');
const commitResp = await request.post('/api/agent/house/commit', {
data: { teamCode, commit: raCommit, revealPub: agentRevealPair.publicKeyB64 }
});
await reachCreateViaLite(page);

await page.getByTestId('px-0-0').click();
Expand Down
Loading