@alumbwe/anvil 1.0.20 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +74 -70
- package/src/agents/bundled-agents.generated.ts +4 -4
- package/src/chat.tsx +1703 -1698
- package/src/cli-args.ts +134 -134
- package/src/components/status-bar.tsx +331 -313
- package/src/hooks/helpers/send-message.ts +663 -654
- package/src/hooks/use-send-message.ts +806 -773
- package/src/index.tsx +473 -473
- package/src/state/chat-store.ts +553 -544
- package/src/utils/create-run-config.ts +121 -121
- package/src/utils/format-compact-number.ts +37 -0
- package/vendor/@anvil/common/package.json +27 -0
- package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +846 -0
- package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +101 -0
- package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +1408 -0
- package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +258 -0
- package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +48 -0
- package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +282 -0
- package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +526 -0
- package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +68 -0
- package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +132 -0
- package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +422 -0
- package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +167 -0
- package/vendor/@anvil/common/src/__tests__/env-process.test.ts +145 -0
- package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +234 -0
- package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +379 -0
- package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +807 -0
- package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +154 -0
- package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +246 -0
- package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +77 -0
- package/vendor/@anvil/common/src/__tests__/model-config.test.ts +89 -0
- package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +298 -0
- package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +116 -0
- package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +167 -0
- package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +132 -0
- package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +72 -0
- package/vendor/@anvil/common/src/__tests__/user-state.test.ts +30 -0
- package/vendor/@anvil/common/src/actions.ts +215 -0
- package/vendor/@anvil/common/src/analytics-core.ts +69 -0
- package/vendor/@anvil/common/src/analytics.ts +93 -0
- package/vendor/@anvil/common/src/api-keys/constants.ts +26 -0
- package/vendor/@anvil/common/src/browser-actions.ts +413 -0
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +63 -0
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +246 -0
- package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +106 -0
- package/vendor/@anvil/common/src/constants/agents.ts +99 -0
- package/vendor/@anvil/common/src/constants/analytics-events.ts +435 -0
- package/vendor/@anvil/common/src/constants/anthropic.ts +73 -0
- package/vendor/@anvil/common/src/constants/anvil-data-use.ts +81 -0
- package/vendor/@anvil/common/src/constants/anvil-errors.ts +7 -0
- package/vendor/@anvil/common/src/constants/anvil-gemini-thinker.ts +21 -0
- package/vendor/@anvil/common/src/constants/anvil-model-ids.ts +11 -0
- package/vendor/@anvil/common/src/constants/anvil-models.ts +2130 -0
- package/vendor/@anvil/common/src/constants/anvil-onboarding-gate.ts +55 -0
- package/vendor/@anvil/common/src/constants/anvil-onboarding.ts +307 -0
- package/vendor/@anvil/common/src/constants/anvil-referral-tiers.ts +87 -0
- package/vendor/@anvil/common/src/constants/anvil-signup-block.ts +68 -0
- package/vendor/@anvil/common/src/constants/anvil-spend-ceilings.ts +450 -0
- package/vendor/@anvil/common/src/constants/anvil-trust.ts +976 -0
- package/vendor/@anvil/common/src/constants/auth.ts +18 -0
- package/vendor/@anvil/common/src/constants/byok.ts +2 -0
- package/vendor/@anvil/common/src/constants/cf-worker-signals.ts +132 -0
- package/vendor/@anvil/common/src/constants/composio.ts +34 -0
- package/vendor/@anvil/common/src/constants/deepseek-direct.ts +55 -0
- package/vendor/@anvil/common/src/constants/feedback.ts +13 -0
- package/vendor/@anvil/common/src/constants/foreign-client-signals.ts +272 -0
- package/vendor/@anvil/common/src/constants/free-agents.ts +777 -0
- package/vendor/@anvil/common/src/constants/gemini.ts +15 -0
- package/vendor/@anvil/common/src/constants/grant-priorities.ts +13 -0
- package/vendor/@anvil/common/src/constants/hosts.ts +6 -0
- package/vendor/@anvil/common/src/constants/images.ts +51 -0
- package/vendor/@anvil/common/src/constants/index.ts +7 -0
- package/vendor/@anvil/common/src/constants/knowledge.ts +35 -0
- package/vendor/@anvil/common/src/constants/limits.ts +23 -0
- package/vendor/@anvil/common/src/constants/model-config.ts +277 -0
- package/vendor/@anvil/common/src/constants/openrouter-attribution.ts +8 -0
- package/vendor/@anvil/common/src/constants/paths.ts +69 -0
- package/vendor/@anvil/common/src/constants/provider-routes.ts +317 -0
- package/vendor/@anvil/common/src/constants/reasoning-effort.ts +79 -0
- package/vendor/@anvil/common/src/constants/skills.ts +60 -0
- package/vendor/@anvil/common/src/constants/subscription-plans.ts +49 -0
- package/vendor/@anvil/common/src/constants/ui.ts +25 -0
- package/vendor/@anvil/common/src/env-ci.ts +36 -0
- package/vendor/@anvil/common/src/env-process.ts +95 -0
- package/vendor/@anvil/common/src/env-schema.ts +95 -0
- package/vendor/@anvil/common/src/env.ts +24 -0
- package/vendor/@anvil/common/src/gravity-capi.ts +207 -0
- package/vendor/@anvil/common/src/mcp/client.ts +233 -0
- package/vendor/@anvil/common/src/old-constants.ts +10 -0
- package/vendor/@anvil/common/src/project-file-tree.ts +355 -0
- package/vendor/@anvil/common/src/reddit-capi.ts +254 -0
- package/vendor/@anvil/common/src/schemas/feedback.ts +52 -0
- package/vendor/@anvil/common/src/schemas/logs.ts +66 -0
- package/vendor/@anvil/common/src/templates/agent-validation.ts +392 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/LICENSE +202 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/README.md +294 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/01-basic-diff-reviewer.ts +17 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/02-intermediate-git-committer.ts +78 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts +73 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/my-custom-agent.ts +40 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/package.json +6 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/README.md +65 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/skills/example-skill/SKILL.md +29 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/types/agent-definition.ts +497 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/types/tools.ts +444 -0
- package/vendor/@anvil/common/src/templates/initial-agents-dir/types/util-types.ts +178 -0
- package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +351 -0
- package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +169 -0
- package/vendor/@anvil/common/src/testing/errors.ts +33 -0
- package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +334 -0
- package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +6 -0
- package/vendor/@anvil/common/src/testing/index.ts +84 -0
- package/vendor/@anvil/common/src/testing/mock-modules.ts +53 -0
- package/vendor/@anvil/common/src/testing/mock-types.ts +123 -0
- package/vendor/@anvil/common/src/testing/mocks/analytics.ts +261 -0
- package/vendor/@anvil/common/src/testing/mocks/child-process.ts +93 -0
- package/vendor/@anvil/common/src/testing/mocks/crypto.ts +218 -0
- package/vendor/@anvil/common/src/testing/mocks/database.ts +337 -0
- package/vendor/@anvil/common/src/testing/mocks/fetch.ts +219 -0
- package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +166 -0
- package/vendor/@anvil/common/src/testing/mocks/index.ts +101 -0
- package/vendor/@anvil/common/src/testing/mocks/logger.ts +135 -0
- package/vendor/@anvil/common/src/testing/mocks/stream.ts +313 -0
- package/vendor/@anvil/common/src/testing/mocks/timers.ts +132 -0
- package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +127 -0
- package/vendor/@anvil/common/src/testing/setup.ts +282 -0
- package/vendor/@anvil/common/src/testing-env-ci.ts +15 -0
- package/vendor/@anvil/common/src/testing-env-process.ts +78 -0
- package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +34 -0
- package/vendor/@anvil/common/src/tools/compile-tool-definitions.ts +157 -0
- package/vendor/@anvil/common/src/tools/constants.ts +117 -0
- package/vendor/@anvil/common/src/tools/list.ts +190 -0
- package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +213 -0
- package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +47 -0
- package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +65 -0
- package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +49 -0
- package/vendor/@anvil/common/src/tools/params/tool/add-message.ts +39 -0
- package/vendor/@anvil/common/src/tools/params/tool/add-subgoal.ts +57 -0
- package/vendor/@anvil/common/src/tools/params/tool/apply-patch.ts +110 -0
- package/vendor/@anvil/common/src/tools/params/tool/ask-user.ts +181 -0
- package/vendor/@anvil/common/src/tools/params/tool/browser-logs.ts +85 -0
- package/vendor/@anvil/common/src/tools/params/tool/cloud-plan-ready.ts +94 -0
- package/vendor/@anvil/common/src/tools/params/tool/code-search.ts +159 -0
- package/vendor/@anvil/common/src/tools/params/tool/composio.ts +131 -0
- package/vendor/@anvil/common/src/tools/params/tool/create-plan.ts +80 -0
- package/vendor/@anvil/common/src/tools/params/tool/end-turn.ts +57 -0
- package/vendor/@anvil/common/src/tools/params/tool/find-files.ts +60 -0
- package/vendor/@anvil/common/src/tools/params/tool/glob.ts +80 -0
- package/vendor/@anvil/common/src/tools/params/tool/gravity-index.ts +93 -0
- package/vendor/@anvil/common/src/tools/params/tool/list-directory.ts +58 -0
- package/vendor/@anvil/common/src/tools/params/tool/lookup-agent-info.ts +37 -0
- package/vendor/@anvil/common/src/tools/params/tool/propose-str-replace.ts +103 -0
- package/vendor/@anvil/common/src/tools/params/tool/propose-write-file.ts +71 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-docs.ts +90 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-files.ts +109 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-subtree.ts +79 -0
- package/vendor/@anvil/common/src/tools/params/tool/read-url.ts +81 -0
- package/vendor/@anvil/common/src/tools/params/tool/render-ui.ts +168 -0
- package/vendor/@anvil/common/src/tools/params/tool/run-file-change-hooks.ts +57 -0
- package/vendor/@anvil/common/src/tools/params/tool/run-terminal-command.ts +211 -0
- package/vendor/@anvil/common/src/tools/params/tool/screenshot.ts +97 -0
- package/vendor/@anvil/common/src/tools/params/tool/set-messages.ts +44 -0
- package/vendor/@anvil/common/src/tools/params/tool/set-output.ts +61 -0
- package/vendor/@anvil/common/src/tools/params/tool/skill.ts +59 -0
- package/vendor/@anvil/common/src/tools/params/tool/spawn-agent-inline.ts +56 -0
- package/vendor/@anvil/common/src/tools/params/tool/spawn-agents.ts +154 -0
- package/vendor/@anvil/common/src/tools/params/tool/str-replace.ts +107 -0
- package/vendor/@anvil/common/src/tools/params/tool/suggest-followups.ts +97 -0
- package/vendor/@anvil/common/src/tools/params/tool/task-completed.ts +61 -0
- package/vendor/@anvil/common/src/tools/params/tool/think-deeply.ts +56 -0
- package/vendor/@anvil/common/src/tools/params/tool/update-subgoal.ts +89 -0
- package/vendor/@anvil/common/src/tools/params/tool/vision-analyze.ts +82 -0
- package/vendor/@anvil/common/src/tools/params/tool/web-search.ts +73 -0
- package/vendor/@anvil/common/src/tools/params/tool/write-file.ts +71 -0
- package/vendor/@anvil/common/src/tools/params/tool/write-todos.ts +67 -0
- package/vendor/@anvil/common/src/tools/params/utils.ts +150 -0
- package/vendor/@anvil/common/src/tools/utils.ts +24 -0
- package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +20 -0
- package/vendor/@anvil/common/src/types/agent-template.ts +215 -0
- package/vendor/@anvil/common/src/types/anvil-session.ts +535 -0
- package/vendor/@anvil/common/src/types/anvil-streak.ts +6 -0
- package/vendor/@anvil/common/src/types/anvil-usage.ts +64 -0
- package/vendor/@anvil/common/src/types/api/agents/publish.ts +61 -0
- package/vendor/@anvil/common/src/types/bun-test.d.ts +5 -0
- package/vendor/@anvil/common/src/types/contracts/agent-runtime.ts +75 -0
- package/vendor/@anvil/common/src/types/contracts/analytics.ts +9 -0
- package/vendor/@anvil/common/src/types/contracts/bigquery.ts +55 -0
- package/vendor/@anvil/common/src/types/contracts/billing.ts +46 -0
- package/vendor/@anvil/common/src/types/contracts/client.ts +53 -0
- package/vendor/@anvil/common/src/types/contracts/database.ts +112 -0
- package/vendor/@anvil/common/src/types/contracts/env.ts +203 -0
- package/vendor/@anvil/common/src/types/contracts/llm.ts +191 -0
- package/vendor/@anvil/common/src/types/contracts/logger.ts +14 -0
- package/vendor/@anvil/common/src/types/contracts/logs.ts +34 -0
- package/vendor/@anvil/common/src/types/contracts/trace.ts +21 -0
- package/vendor/@anvil/common/src/types/dynamic-agent-template.ts +328 -0
- package/vendor/@anvil/common/src/types/filesystem.ts +10 -0
- package/vendor/@anvil/common/src/types/function-params.ts +33 -0
- package/vendor/@anvil/common/src/types/grant.ts +20 -0
- package/vendor/@anvil/common/src/types/gravity-index.ts +170 -0
- package/vendor/@anvil/common/src/types/json.ts +29 -0
- package/vendor/@anvil/common/src/types/mcp.ts +24 -0
- package/vendor/@anvil/common/src/types/messages/anvil-message.ts +58 -0
- package/vendor/@anvil/common/src/types/messages/content-part.ts +59 -0
- package/vendor/@anvil/common/src/types/messages/data-content.ts +14 -0
- package/vendor/@anvil/common/src/types/messages/provider-metadata.ts +13 -0
- package/vendor/@anvil/common/src/types/organization.ts +118 -0
- package/vendor/@anvil/common/src/types/print-mode.ts +121 -0
- package/vendor/@anvil/common/src/types/publisher.ts +67 -0
- package/vendor/@anvil/common/src/types/session-state.ts +149 -0
- package/vendor/@anvil/common/src/types/skill.ts +86 -0
- package/vendor/@anvil/common/src/types/source.ts +11 -0
- package/vendor/@anvil/common/src/types/spawn.ts +13 -0
- package/vendor/@anvil/common/src/types/subscription.ts +67 -0
- package/vendor/@anvil/common/src/types/usage.ts +16 -0
- package/vendor/@anvil/common/src/types/util.ts +3 -0
- package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +26 -0
- package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +122 -0
- package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +102 -0
- package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +143 -0
- package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +202 -0
- package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +193 -0
- package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +140 -0
- package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +148 -0
- package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +115 -0
- package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +57 -0
- package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +774 -0
- package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +206 -0
- package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +173 -0
- package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +60 -0
- package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +31 -0
- package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +1252 -0
- package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +505 -0
- package/vendor/@anvil/common/src/util/__tests__/path.test.ts +22 -0
- package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +90 -0
- package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +325 -0
- package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +28 -0
- package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +130 -0
- package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +1008 -0
- package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +289 -0
- package/vendor/@anvil/common/src/util/__tests__/string.test.ts +239 -0
- package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +70 -0
- package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +108 -0
- package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +180 -0
- package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +24 -0
- package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +88 -0
- package/vendor/@anvil/common/src/util/ad-user-agent.ts +25 -0
- package/vendor/@anvil/common/src/util/agent-file-utils.ts +110 -0
- package/vendor/@anvil/common/src/util/agent-id-parsing.ts +136 -0
- package/vendor/@anvil/common/src/util/agent-name-normalization.ts +38 -0
- package/vendor/@anvil/common/src/util/agent-name-resolver.ts +86 -0
- package/vendor/@anvil/common/src/util/analytics-dispatcher.ts +82 -0
- package/vendor/@anvil/common/src/util/analytics-log.ts +78 -0
- package/vendor/@anvil/common/src/util/analytics-sampling.ts +255 -0
- package/vendor/@anvil/common/src/util/anvil-model-availability.ts +102 -0
- package/vendor/@anvil/common/src/util/anvil-privacy.ts +82 -0
- package/vendor/@anvil/common/src/util/anvil-streak-line.ts +103 -0
- package/vendor/@anvil/common/src/util/anvil-streak.ts +156 -0
- package/vendor/@anvil/common/src/util/anvil-usage-summary.ts +125 -0
- package/vendor/@anvil/common/src/util/array.ts +31 -0
- package/vendor/@anvil/common/src/util/axiom-only-log.ts +124 -0
- package/vendor/@anvil/common/src/util/cache-debug.ts +171 -0
- package/vendor/@anvil/common/src/util/client-user-agent.ts +114 -0
- package/vendor/@anvil/common/src/util/credentials.ts +26 -0
- package/vendor/@anvil/common/src/util/currency.ts +25 -0
- package/vendor/@anvil/common/src/util/dates.ts +81 -0
- package/vendor/@anvil/common/src/util/disposable-email.ts +277 -0
- package/vendor/@anvil/common/src/util/engagement-tracker.ts +129 -0
- package/vendor/@anvil/common/src/util/env-file-path.ts +40 -0
- package/vendor/@anvil/common/src/util/error.ts +562 -0
- package/vendor/@anvil/common/src/util/file-read-limits.ts +236 -0
- package/vendor/@anvil/common/src/util/file.ts +339 -0
- package/vendor/@anvil/common/src/util/format-code-search.ts +115 -0
- package/vendor/@anvil/common/src/util/lazy-response-ads.ts +92 -0
- package/vendor/@anvil/common/src/util/log-data.ts +57 -0
- package/vendor/@anvil/common/src/util/log-ingest.ts +58 -0
- package/vendor/@anvil/common/src/util/log-mirror.ts +34 -0
- package/vendor/@anvil/common/src/util/lru-cache.ts +67 -0
- package/vendor/@anvil/common/src/util/messages.ts +685 -0
- package/vendor/@anvil/common/src/util/min-heap.ts +87 -0
- package/vendor/@anvil/common/src/util/model-utils.ts +4 -0
- package/vendor/@anvil/common/src/util/object.ts +128 -0
- package/vendor/@anvil/common/src/util/partial-json-delta.ts +89 -0
- package/vendor/@anvil/common/src/util/path.ts +15 -0
- package/vendor/@anvil/common/src/util/project-ignore.ts +119 -0
- package/vendor/@anvil/common/src/util/promise.ts +73 -0
- package/vendor/@anvil/common/src/util/random.ts +15 -0
- package/vendor/@anvil/common/src/util/rate-limit.ts +56 -0
- package/vendor/@anvil/common/src/util/reddit-anvil-retention.ts +60 -0
- package/vendor/@anvil/common/src/util/reddit-capi-events.ts +36 -0
- package/vendor/@anvil/common/src/util/response-ad-positions.ts +54 -0
- package/vendor/@anvil/common/src/util/saxy.ts +741 -0
- package/vendor/@anvil/common/src/util/skills.test.ts +44 -0
- package/vendor/@anvil/common/src/util/skills.ts +36 -0
- package/vendor/@anvil/common/src/util/split-data.ts +259 -0
- package/vendor/@anvil/common/src/util/stop-sequence.ts +61 -0
- package/vendor/@anvil/common/src/util/string.ts +426 -0
- package/vendor/@anvil/common/src/util/system-info.ts +53 -0
- package/vendor/@anvil/common/src/util/thread-title.ts +46 -0
- package/vendor/@anvil/common/src/util/ttft-histogram.ts +73 -0
- package/vendor/@anvil/common/src/util/xml-parser.ts +27 -0
- package/vendor/@anvil/common/src/util/xml.ts +17 -0
- package/vendor/@anvil/common/src/util/zod-schema.ts +25 -0
- package/vendor/@anvil/common/src/util/zoned-time.ts +136 -0
- package/vendor/@anvil/common/src/utils/ask-user-bridge.ts +44 -0
- package/vendor/@anvil/sdk/dist/index.cjs +52297 -0
- package/vendor/@anvil/sdk/dist/index.cjs.map +440 -0
- package/vendor/@anvil/sdk/dist/index.d.ts +4277 -0
- package/vendor/@anvil/sdk/dist/index.mjs +52290 -0
- package/vendor/@anvil/sdk/dist/index.mjs.map +440 -0
- package/vendor/@anvil/sdk/dist/vendor/ai.cjs +30886 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/arm64-linux/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-darwin/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-linux/rg +0 -0
- package/vendor/@anvil/sdk/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-go.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-java.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-javascript.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-python.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/vendor/@anvil/sdk/dist/wasm/tree-sitter.wasm +0 -0
- package/vendor/@anvil/sdk/package.json +21 -0
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anvil account standing ("Access Level") — the per-account policy layer
|
|
3
|
+
* that decides how much free capacity an account gets.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* Every free-mode control before this was keyed on the ACCOUNT and applied the
|
|
8
|
+
* same number to every account: the same 5,000 requests/day, the same $50
|
|
9
|
+
* spend budget, the same 6 premium sessions. That is only a bound if accounts
|
|
10
|
+
* are scarce, and `docs/anvil-signup-gate.md` is the record of them not
|
|
11
|
+
* being. A relay pooling 100 minted accounts is entitled, entirely within the
|
|
12
|
+
* rules, to 100x every per-user limit.
|
|
13
|
+
*
|
|
14
|
+
* The signup gate raised the price of minting an account. This raises the
|
|
15
|
+
* price of a FRESH one being worth anything: a brand-new account from an
|
|
16
|
+
* unverifiable network gets a small fraction of the capacity, and an account
|
|
17
|
+
* that has demonstrably existed and done work for months gets considerably
|
|
18
|
+
* MORE than the flat limits ever gave it. Same fleet spend, redistributed
|
|
19
|
+
* toward the people we actually want to serve.
|
|
20
|
+
*
|
|
21
|
+
* ## Two axes, deliberately separate
|
|
22
|
+
*
|
|
23
|
+
* `AnvilAccessTier` (full / limited) is a REGION property, resolved per
|
|
24
|
+
* request from the caller's IP country. `AnvilTrustLevel` is an ACCOUNT
|
|
25
|
+
* property, resolved from durable facts about the account. They multiply:
|
|
26
|
+
* limits are a matrix, not a sum. A limited-region user can climb to `core`
|
|
27
|
+
* and get more than a full-region `new` account — which is the whole point of
|
|
28
|
+
* shipping this alongside the region split rather than instead of it.
|
|
29
|
+
*
|
|
30
|
+
* ## What the level must never be
|
|
31
|
+
*
|
|
32
|
+
* A ban input, or a reason to serve a degraded model. Everything here produces
|
|
33
|
+
* a NUMBER — a limit — and a limit that is reached produces a retryable 429
|
|
34
|
+
* naming the remedy. `docs/anvil-abuse-detection.md` records what it cost
|
|
35
|
+
* the last time a soft signal was allowed to convict (659 wrongly-banned
|
|
36
|
+
* accounts, 2026-08-03); none of the signals below is stronger than the ones
|
|
37
|
+
* that did it.
|
|
38
|
+
*
|
|
39
|
+
* ## Naming
|
|
40
|
+
*
|
|
41
|
+
* User-facing copy says "Access Level" and never "trust", because a user shown
|
|
42
|
+
* a low trust score reads an accusation. The code says `trustLevel` because
|
|
43
|
+
* that is what it is and a euphemism in an identifier costs a reader time.
|
|
44
|
+
* `ANVIL_TRUST_LEVEL_LABELS` is the one bridge between the two.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import type { AnvilAccessTier } from './anvil-models'
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Levels
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Ordered least- to most-established. The order is load-bearing:
|
|
55
|
+
* `ANVIL_TRUST_LEVELS.indexOf` is how "at least X" comparisons are done, so
|
|
56
|
+
* inserting a level in the middle re-ranks every comparison in one edit rather
|
|
57
|
+
* than requiring each call site to be found.
|
|
58
|
+
*/
|
|
59
|
+
export const ANVIL_TRUST_LEVELS = [
|
|
60
|
+
'new',
|
|
61
|
+
'verified',
|
|
62
|
+
'established',
|
|
63
|
+
'core',
|
|
64
|
+
] as const
|
|
65
|
+
|
|
66
|
+
export type AnvilTrustLevel = (typeof ANVIL_TRUST_LEVELS)[number]
|
|
67
|
+
|
|
68
|
+
/** The level an account holds before anything is known about it. Every failure
|
|
69
|
+
* path in the resolver must land somewhere DEFINITE, and this is not it — see
|
|
70
|
+
* `ANVIL_TRUST_FALLBACK_LEVEL`. */
|
|
71
|
+
export const ANVIL_TRUST_MIN_LEVEL: AnvilTrustLevel = 'new'
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The level used when signals cannot be loaded (database error, timeout).
|
|
75
|
+
*
|
|
76
|
+
* `established` and NOT `new`, and this is the single most consequential
|
|
77
|
+
* constant in the file. This resolver runs on the free-mode hot path; if a
|
|
78
|
+
* Postgres hiccup dropped every caller to `new`, one degraded dependency would
|
|
79
|
+
* throttle the entire product to a fifth of its capacity, and it would look
|
|
80
|
+
* exactly like an outage nobody could attribute. Failing to the level that
|
|
81
|
+
* reproduces roughly today's flat limits means a broken resolver costs us the
|
|
82
|
+
* enforcement, never the users. Same reasoning as the signup gate's fail-open.
|
|
83
|
+
*/
|
|
84
|
+
export const ANVIL_TRUST_FALLBACK_LEVEL: AnvilTrustLevel = 'established'
|
|
85
|
+
|
|
86
|
+
export function isAtLeastTrustLevel(
|
|
87
|
+
level: AnvilTrustLevel,
|
|
88
|
+
minimum: AnvilTrustLevel,
|
|
89
|
+
): boolean {
|
|
90
|
+
return (
|
|
91
|
+
ANVIL_TRUST_LEVELS.indexOf(level) >=
|
|
92
|
+
ANVIL_TRUST_LEVELS.indexOf(minimum)
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function lowerOf(
|
|
97
|
+
a: AnvilTrustLevel,
|
|
98
|
+
b: AnvilTrustLevel,
|
|
99
|
+
): AnvilTrustLevel {
|
|
100
|
+
return isAtLeastTrustLevel(a, b) ? b : a
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** User-facing name. Never says "trust", "risk" or "score" — a user reading
|
|
104
|
+
* their own level is reading an explanation of their limits, not a verdict on
|
|
105
|
+
* their character. */
|
|
106
|
+
export const ANVIL_TRUST_LEVEL_LABELS: Record<AnvilTrustLevel, string> = {
|
|
107
|
+
new: 'Getting started',
|
|
108
|
+
verified: 'Verified',
|
|
109
|
+
established: 'Established',
|
|
110
|
+
core: 'Core member',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** One line of user-facing copy per level, shown under the label. */
|
|
114
|
+
export const ANVIL_TRUST_LEVEL_BLURBS: Record<AnvilTrustLevel, string> = {
|
|
115
|
+
new: 'Welcome! Your account is brand new, so limits start small. They open up quickly — the steps below take a few minutes.',
|
|
116
|
+
verified:
|
|
117
|
+
'Your account is verified. You have solid daily limits, and a bit of history unlocks the next level.',
|
|
118
|
+
established:
|
|
119
|
+
'You are an established Anvil user with generous limits on messages, spend and premium sessions.',
|
|
120
|
+
core: 'You are a core member. You get the highest free limits we offer, in every region.',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Limits
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Everything a level controls, in one place.
|
|
129
|
+
*
|
|
130
|
+
* Every field is a per-account ceiling over a time window. None is a global
|
|
131
|
+
* budget and none is a concurrency cap — read each doc comment rather than
|
|
132
|
+
* inferring from the name.
|
|
133
|
+
*/
|
|
134
|
+
export interface AnvilTrustLimits {
|
|
135
|
+
/**
|
|
136
|
+
* User prompts per Pacific day. NOT requests: one prompt is one root agent
|
|
137
|
+
* run, and an agentic turn behind it may make dozens of LLM calls.
|
|
138
|
+
*
|
|
139
|
+
* This is the limit that means what a person thinks "messages" means, and it
|
|
140
|
+
* is the one an honest heavy user should be able to feel without hitting.
|
|
141
|
+
* Counted only when a call OPENS a root run (`isNewPromptWindow`), so a
|
|
142
|
+
* caller that reuses one run id to hide many prompts pays the run-reuse
|
|
143
|
+
* guard's ceiling instead.
|
|
144
|
+
*/
|
|
145
|
+
userMessagesPerDay: number
|
|
146
|
+
/** Total free-mode LLM requests in any 5-hour window: prompts plus every
|
|
147
|
+
* subagent, tool loop and retry underneath them. */
|
|
148
|
+
messagesPer5Hours: number
|
|
149
|
+
/** Total free-mode LLM requests per day. The overall ceiling — a premium
|
|
150
|
+
* request consumes this budget as well as its own. */
|
|
151
|
+
messagesPerDay: number
|
|
152
|
+
/**
|
|
153
|
+
* Settled non-BYOK provider cost, in USD, since midnight Pacific, at or
|
|
154
|
+
* above which no FRESH session is admitted. Live sessions and reclaims are
|
|
155
|
+
* never interrupted, so the honest reading is "how much we will spend
|
|
156
|
+
* starting new work for this account today".
|
|
157
|
+
*/
|
|
158
|
+
dailySpendUsd: number
|
|
159
|
+
/**
|
|
160
|
+
* Premium-model sessions per Pacific day (the shared premium pool's base
|
|
161
|
+
* entitlement). Referral, streak and operator entitlement is ADDED on top of
|
|
162
|
+
* this, so a level never takes away something a user earned.
|
|
163
|
+
*
|
|
164
|
+
* Zero at every limited-region level: the limited tier cannot reach a
|
|
165
|
+
* premium model at all, and the number would be decoration.
|
|
166
|
+
*/
|
|
167
|
+
premiumSessionsPerDay: number
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Two axes were deliberately REMOVED from this interface, and the reasoning is
|
|
172
|
+
* worth keeping so they are not quietly re-added.
|
|
173
|
+
*
|
|
174
|
+
* **Browser sessions per day** (`ANVIL_WEB_STANDARD_SESSION_LIMIT`) and
|
|
175
|
+
* **concurrent Desktop tabs** (`ANVIL_DESKTOP_SESSION_LIMITS.unlimited`) are
|
|
176
|
+
* still enforced — they simply are not level-scaled. Both are session-SHAPE
|
|
177
|
+
* controls rather than cost controls: starting a session costs nothing, and a
|
|
178
|
+
* session that sits idle costs nothing either. What costs money is the traffic
|
|
179
|
+
* inside it, and that is already bounded four different ways by the fields
|
|
180
|
+
* above.
|
|
181
|
+
*
|
|
182
|
+
* Scaling them by level would therefore have taken visible, immediate
|
|
183
|
+
* capability away from exactly the users we most need to keep — a new user
|
|
184
|
+
* discovers "you may only open one tab" instantly, and it reads as a product
|
|
185
|
+
* that is broken rather than as a budget — in exchange for no measurable saving
|
|
186
|
+
* at all. The premium-session pool stays level-scaled because a premium session
|
|
187
|
+
* is the one whose mere existence commits us to expensive inference.
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The matrix. Region tier picks the row, account level picks the column.
|
|
192
|
+
*
|
|
193
|
+
* ## How these numbers were chosen
|
|
194
|
+
*
|
|
195
|
+
* `established` × `full` REPRODUCES the flat limits that shipped before this
|
|
196
|
+
* file existed (5,000/day, 3,000/5h, $50, 6 premium), and `established` ×
|
|
197
|
+
* `limited` reproduces the limited row (3,000/day, 2,000/5h). That is
|
|
198
|
+
* deliberate and is what makes this change safe to ship:
|
|
199
|
+
* the population that already looked like ordinary heavy users keeps exactly
|
|
200
|
+
* what it had, `core` is the raise, and only accounts with no history at all
|
|
201
|
+
* are tightened.
|
|
202
|
+
*
|
|
203
|
+
* Sizing for the two new levels below `established` is anchored on the
|
|
204
|
+
* per-user-per-day distributions in `free-mode-rate-limiter.ts` (full tier p50
|
|
205
|
+
* 131, p90 837, p99 2,351): `verified` at 3,000/day sits above the full tier's
|
|
206
|
+
* p99, and `new` at 1,200/day sits above its p90. So a genuinely new user
|
|
207
|
+
* doing genuinely heavy work still fits, and the accounts that do not fit are
|
|
208
|
+
* the ones doing several times what any measured human does on their first
|
|
209
|
+
* day.
|
|
210
|
+
*
|
|
211
|
+
* `core` is roughly 1.6x `established` rather than unbounded. It is a reward,
|
|
212
|
+
* not an exemption — an account that reaches `core` and is then compromised or
|
|
213
|
+
* sold should still cost a bounded amount, and the fleet-wide spend has to
|
|
214
|
+
* survive every core member using their allowance on the same day.
|
|
215
|
+
*
|
|
216
|
+
* ## The limited row is not merely the full row scaled down
|
|
217
|
+
*
|
|
218
|
+
* Its floor is deliberately harsher (`new` × limited is a third of `new` ×
|
|
219
|
+
* full) because that intersection — brand-new account, unsupported region,
|
|
220
|
+
* often VPN — is the exact shape of the reselling farms. Its ceiling is
|
|
221
|
+
* deliberately generous (`core` × limited beats `verified` × full on every
|
|
222
|
+
* axis it can — premium is region-gated, not level-gated) because the entire
|
|
223
|
+
* promise this makes to a real developer in an unsupported country is that the
|
|
224
|
+
* region is a starting point and not a cage.
|
|
225
|
+
*/
|
|
226
|
+
export const ANVIL_TRUST_LIMITS: Record<
|
|
227
|
+
AnvilAccessTier,
|
|
228
|
+
Record<AnvilTrustLevel, AnvilTrustLimits>
|
|
229
|
+
> = {
|
|
230
|
+
full: {
|
|
231
|
+
new: {
|
|
232
|
+
userMessagesPerDay: 120,
|
|
233
|
+
messagesPer5Hours: 800,
|
|
234
|
+
messagesPerDay: 1_200,
|
|
235
|
+
dailySpendUsd: 8,
|
|
236
|
+
premiumSessionsPerDay: 2,
|
|
237
|
+
},
|
|
238
|
+
verified: {
|
|
239
|
+
userMessagesPerDay: 300,
|
|
240
|
+
messagesPer5Hours: 1_800,
|
|
241
|
+
messagesPerDay: 3_000,
|
|
242
|
+
dailySpendUsd: 20,
|
|
243
|
+
premiumSessionsPerDay: 4,
|
|
244
|
+
},
|
|
245
|
+
established: {
|
|
246
|
+
userMessagesPerDay: 600,
|
|
247
|
+
messagesPer5Hours: 3_000,
|
|
248
|
+
messagesPerDay: 5_000,
|
|
249
|
+
dailySpendUsd: 50,
|
|
250
|
+
premiumSessionsPerDay: 6,
|
|
251
|
+
},
|
|
252
|
+
core: {
|
|
253
|
+
userMessagesPerDay: 1_000,
|
|
254
|
+
messagesPer5Hours: 5_000,
|
|
255
|
+
messagesPerDay: 8_000,
|
|
256
|
+
dailySpendUsd: 90,
|
|
257
|
+
premiumSessionsPerDay: 10,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
limited: {
|
|
261
|
+
new: {
|
|
262
|
+
userMessagesPerDay: 40,
|
|
263
|
+
messagesPer5Hours: 400,
|
|
264
|
+
messagesPerDay: 500,
|
|
265
|
+
dailySpendUsd: 3,
|
|
266
|
+
premiumSessionsPerDay: 0,
|
|
267
|
+
},
|
|
268
|
+
verified: {
|
|
269
|
+
userMessagesPerDay: 120,
|
|
270
|
+
messagesPer5Hours: 1_000,
|
|
271
|
+
messagesPerDay: 1_500,
|
|
272
|
+
dailySpendUsd: 10,
|
|
273
|
+
premiumSessionsPerDay: 0,
|
|
274
|
+
},
|
|
275
|
+
established: {
|
|
276
|
+
userMessagesPerDay: 350,
|
|
277
|
+
messagesPer5Hours: 2_000,
|
|
278
|
+
messagesPerDay: 3_000,
|
|
279
|
+
dailySpendUsd: 25,
|
|
280
|
+
premiumSessionsPerDay: 0,
|
|
281
|
+
},
|
|
282
|
+
core: {
|
|
283
|
+
userMessagesPerDay: 700,
|
|
284
|
+
messagesPer5Hours: 3_500,
|
|
285
|
+
messagesPerDay: 5_500,
|
|
286
|
+
dailySpendUsd: 55,
|
|
287
|
+
premiumSessionsPerDay: 0,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function anvilTrustLimits(
|
|
293
|
+
accessTier: AnvilAccessTier,
|
|
294
|
+
level: AnvilTrustLevel,
|
|
295
|
+
): AnvilTrustLimits {
|
|
296
|
+
return ANVIL_TRUST_LIMITS[accessTier][level]
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Signals
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Everything the score is computed from.
|
|
305
|
+
*
|
|
306
|
+
* **`null` means "unknown", never "clean" and never "suspicious".** Every
|
|
307
|
+
* account created before `docs/anvil-signup-gate.md` shipped has null
|
|
308
|
+
* provenance, and every account that never linked GitHub has null GitHub
|
|
309
|
+
* facts. A scorer that read null as bad would demote most of the existing user
|
|
310
|
+
* base overnight; one that read it as good would hand every fresh signup a
|
|
311
|
+
* clean slate. Unknown earns nothing and costs nothing — which lands those
|
|
312
|
+
* accounts at whatever their other, positive signals justify.
|
|
313
|
+
*/
|
|
314
|
+
export interface AnvilTrustSignals {
|
|
315
|
+
/** `user.created_at`. */
|
|
316
|
+
accountCreatedAt: Date | null
|
|
317
|
+
/** `referral_qualification.github_account_created_at` — set by GitHub
|
|
318
|
+
* server-side and not backdatable, which is what makes it worth points at
|
|
319
|
+
* all. A commit author date, by contrast, forges in one command. */
|
|
320
|
+
githubAccountCreatedAt: Date | null
|
|
321
|
+
/** Oldest public repo's creation date. Same non-forgeability. */
|
|
322
|
+
githubOldestRepoCreatedAt: Date | null
|
|
323
|
+
githubPublicRepos: number | null
|
|
324
|
+
githubFollowers: number | null
|
|
325
|
+
githubTwoFactorEnabled: boolean | null
|
|
326
|
+
/** Distinct Pacific days the account has used free mode
|
|
327
|
+
* (`anvil_daily_usage`). Cheap history that cannot be bought. */
|
|
328
|
+
activeDays: number
|
|
329
|
+
/** Approved bounty submissions. Reviewed proof-of-work — the single
|
|
330
|
+
* strongest earned signal here, and the one a limited-region user can act
|
|
331
|
+
* on today without owning an aged GitHub account. */
|
|
332
|
+
approvedBounties: number
|
|
333
|
+
/** Qualified referrals GIVEN (`referral_v2`, active + qualified). */
|
|
334
|
+
qualifiedReferrals: number
|
|
335
|
+
/** Has ever paid us anything. Wired now and worth points now; payments are
|
|
336
|
+
* planned, and the day they land this needs no scorer change. */
|
|
337
|
+
hasPaid: boolean
|
|
338
|
+
/** Privacy signals recorded at SIGNUP (`user.signup_privacy_signals`), split
|
|
339
|
+
* on comma. Empty array = checked and clean; null = never checked. */
|
|
340
|
+
signupPrivacySignals: readonly string[] | null
|
|
341
|
+
/** `user.signup_ip_source`. Anything other than `edge_secret`/`cloudflare`
|
|
342
|
+
* means the caller had some influence over the address. */
|
|
343
|
+
signupIpSource: string | null
|
|
344
|
+
/** Accounts sharing this account's signup /24 or /48. Includes this account,
|
|
345
|
+
* so 1 is the clean value. */
|
|
346
|
+
signupPrefixAccountCount: number | null
|
|
347
|
+
/** Accounts sharing this account's normalized mailbox. Includes this
|
|
348
|
+
* account. */
|
|
349
|
+
mailboxAccountCount: number | null
|
|
350
|
+
/** A ban event that was NOT reversed. Live bans never reach here (banned
|
|
351
|
+
* accounts are refused before any of this runs), so this is history: an
|
|
352
|
+
* account that was actioned and then unbanned on appeal. */
|
|
353
|
+
hasUnreversedBanEvent: boolean
|
|
354
|
+
/** `user.privacy_flagged_at` — first request ever seen on an ipinfo-flagged
|
|
355
|
+
* anonymizing egress, uncorroborated. Sticky by construction: written once,
|
|
356
|
+
* never cleared by code. The WEAK member of the sticky trio, so it carries
|
|
357
|
+
* the mildest cap below. */
|
|
358
|
+
privacyFlaggedAt: Date | null
|
|
359
|
+
/** `user.privacy_corroborated_at` — first request where a second provider
|
|
360
|
+
* agreed the egress was anonymizing. Sticky. */
|
|
361
|
+
privacyCorroboratedAt: Date | null
|
|
362
|
+
/** `user.third_party_client_at` — first free-mode request carrying a tool
|
|
363
|
+
* schema no Anvil client ships. Sticky, and behavioural rather than
|
|
364
|
+
* network-derived, which is what makes it worth a hard cap. */
|
|
365
|
+
thirdPartyClientAt: Date | null
|
|
366
|
+
/**
|
|
367
|
+
* The privacy verdict on the CURRENT request, from `getFreeModeRiskScore`.
|
|
368
|
+
* The one live signal in an otherwise durable set, and the one a user can
|
|
369
|
+
* change in a second by toggling a VPN — which is why it can only CAP a
|
|
370
|
+
* level, never contribute points.
|
|
371
|
+
*/
|
|
372
|
+
currentRiskScore: number | null
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** A signal that moved the score, in user-facing language. */
|
|
376
|
+
export interface AnvilTrustFactor {
|
|
377
|
+
id: string
|
|
378
|
+
label: string
|
|
379
|
+
points: number
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Something the user can do to move up, with what it is worth. */
|
|
383
|
+
export interface AnvilTrustNextStep {
|
|
384
|
+
id: string
|
|
385
|
+
label: string
|
|
386
|
+
detail: string
|
|
387
|
+
points: number
|
|
388
|
+
/** Where the UI should send them. Relative to the anvil web app. */
|
|
389
|
+
href?: string
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export interface AnvilTrustAssessment {
|
|
393
|
+
level: AnvilTrustLevel
|
|
394
|
+
/** 0-100. Exposed so a user can see movement between levels, and so the
|
|
395
|
+
* thresholds below are auditable from the outside. */
|
|
396
|
+
score: number
|
|
397
|
+
/** The level the score alone earned, before caps. Equal to `level` unless a
|
|
398
|
+
* cap applied — which is how the UI knows to explain the cap rather than
|
|
399
|
+
* telling someone with 80 points to keep earning points. */
|
|
400
|
+
uncappedLevel: AnvilTrustLevel
|
|
401
|
+
/** Which cap bound, if any. */
|
|
402
|
+
cappedBy: string | null
|
|
403
|
+
factors: AnvilTrustFactor[]
|
|
404
|
+
nextSteps: AnvilTrustNextStep[]
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
// Scoring
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The two signals a user can act on TODAY, from any country, with no aged
|
|
413
|
+
* GitHub account and no money.
|
|
414
|
+
*
|
|
415
|
+
* ## Why the caps are where they are
|
|
416
|
+
*
|
|
417
|
+
* They were originally 4 bounties (20) and 5 referrals (15). Together that is
|
|
418
|
+
* 35 points against a `core` threshold of 75, and the whole earned-only route
|
|
419
|
+
* — bounties, referrals, 90 days of account age, 30 active days, a clean
|
|
420
|
+
* residential signup — topped out at **70**. Five points short. `core` was
|
|
421
|
+
* literally unreachable by contribution: it required either an aged GitHub
|
|
422
|
+
* account or a payment.
|
|
423
|
+
*
|
|
424
|
+
* That is backwards for a program whose stated purpose is to give developers
|
|
425
|
+
* in unsupported regions a way to raise their own limits. It also made the
|
|
426
|
+
* incentive flat exactly where it should be steep — a user who completed ten
|
|
427
|
+
* bounties and referred twenty people scored the same as one who did four and
|
|
428
|
+
* five.
|
|
429
|
+
*
|
|
430
|
+
* At 6 and 10 the earned-only route reaches 95, so `core` is attainable by
|
|
431
|
+
* work alone, and each additional bounty or referral keeps paying well past
|
|
432
|
+
* the point where someone has proved they are real.
|
|
433
|
+
*
|
|
434
|
+
* ## Why raising them costs nothing against abuse
|
|
435
|
+
*
|
|
436
|
+
* Neither is cheap to manufacture. A bounty is reviewed proof-of-work, daily
|
|
437
|
+
* capped, and carries an anti-fraud agreement the claimant signs
|
|
438
|
+
* (docs/anvil-bounties.md). A qualified referral requires the REFERRED
|
|
439
|
+
* account to hold a GitHub account four calendar months old and to actually
|
|
440
|
+
* use the product, and referral farming has its own detector and clawback path
|
|
441
|
+
* (docs/anvil-abuse-referral-farming.md). An operator who can produce ten
|
|
442
|
+
* qualified referrals has already cleared a higher bar than this scorer sets.
|
|
443
|
+
*/
|
|
444
|
+
export const ANVIL_TRUST_EARNED = {
|
|
445
|
+
BOUNTY_POINTS: 5,
|
|
446
|
+
BOUNTY_CAP: 6,
|
|
447
|
+
REFERRAL_POINTS: 3,
|
|
448
|
+
REFERRAL_CAP: 10,
|
|
449
|
+
} as const
|
|
450
|
+
|
|
451
|
+
const MAX_BOUNTY_POINTS =
|
|
452
|
+
ANVIL_TRUST_EARNED.BOUNTY_POINTS * ANVIL_TRUST_EARNED.BOUNTY_CAP
|
|
453
|
+
const MAX_REFERRAL_POINTS =
|
|
454
|
+
ANVIL_TRUST_EARNED.REFERRAL_POINTS * ANVIL_TRUST_EARNED.REFERRAL_CAP
|
|
455
|
+
|
|
456
|
+
/** Minimum score for each level. `new` is the floor and needs no entry. */
|
|
457
|
+
export const ANVIL_TRUST_THRESHOLDS: Record<
|
|
458
|
+
Exclude<AnvilTrustLevel, 'new'>,
|
|
459
|
+
number
|
|
460
|
+
> = {
|
|
461
|
+
verified: 25,
|
|
462
|
+
established: 50,
|
|
463
|
+
core: 75,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
467
|
+
const MONTH_MS = 30 * DAY_MS
|
|
468
|
+
const YEAR_MS = 365 * DAY_MS
|
|
469
|
+
|
|
470
|
+
function ageMs(date: Date | null, now: Date): number | null {
|
|
471
|
+
if (!date) return null
|
|
472
|
+
const age = now.getTime() - date.getTime()
|
|
473
|
+
// A future date is a clock skew or a bad backfill, not an old account.
|
|
474
|
+
return age >= 0 ? age : 0
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Highest threshold `score` clears. */
|
|
478
|
+
function levelForScore(score: number): AnvilTrustLevel {
|
|
479
|
+
if (score >= ANVIL_TRUST_THRESHOLDS.core) return 'core'
|
|
480
|
+
if (score >= ANVIL_TRUST_THRESHOLDS.established) return 'established'
|
|
481
|
+
if (score >= ANVIL_TRUST_THRESHOLDS.verified) return 'verified'
|
|
482
|
+
return 'new'
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const PRIVACY_EGRESS_SIGNALS = new Set(['vpn', 'proxy', 'tor', 'hosting'])
|
|
486
|
+
|
|
487
|
+
function hasPrivacyEgressAtSignup(
|
|
488
|
+
signals: readonly string[] | null,
|
|
489
|
+
): boolean | null {
|
|
490
|
+
if (signals === null) return null
|
|
491
|
+
return signals.some((signal) =>
|
|
492
|
+
PRIVACY_EGRESS_SIGNALS.has(signal.trim().toLowerCase()),
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Score an account and resolve its level.
|
|
498
|
+
*
|
|
499
|
+
* Pure, so the policy is unit-testable and so the same function can run on the
|
|
500
|
+
* server (to enforce) and be reasoned about from a test (to check nobody moved
|
|
501
|
+
* a threshold by accident). Every I/O concern lives in the caller.
|
|
502
|
+
*
|
|
503
|
+
* ## Points earn, penalties cap
|
|
504
|
+
*
|
|
505
|
+
* Positive signals add points. Negative signals mostly do NOT subtract — they
|
|
506
|
+
* impose a CEILING on the resulting level. The difference matters: a
|
|
507
|
+
* subtracting penalty is defeated by accumulating enough of anything else,
|
|
508
|
+
* which is precisely what a farm operator with 200 aged GitHub accounts can
|
|
509
|
+
* do. A ceiling is not, and it also degrades honestly — a VPN user who cannot
|
|
510
|
+
* exceed `established` still gets `established`, which is today's limits.
|
|
511
|
+
*/
|
|
512
|
+
export function assessAnvilTrust(
|
|
513
|
+
signals: AnvilTrustSignals,
|
|
514
|
+
now: Date = new Date(),
|
|
515
|
+
): AnvilTrustAssessment {
|
|
516
|
+
const factors: AnvilTrustFactor[] = []
|
|
517
|
+
const nextSteps: AnvilTrustNextStep[] = []
|
|
518
|
+
|
|
519
|
+
const add = (id: string, label: string, points: number) => {
|
|
520
|
+
if (points === 0) return
|
|
521
|
+
factors.push({ id, label, points })
|
|
522
|
+
}
|
|
523
|
+
const step = (s: AnvilTrustNextStep) => nextSteps.push(s)
|
|
524
|
+
|
|
525
|
+
// --- GitHub -------------------------------------------------------------
|
|
526
|
+
// The heaviest block (up to 45) because it is the only one an abuser has to
|
|
527
|
+
// BUY. `docs/referrals.md` sets the economic invariant this inherits: keep
|
|
528
|
+
// the reward worth less than the grey-market price of an aged, qualifying
|
|
529
|
+
// GitHub account, and farming stops penciling out.
|
|
530
|
+
const githubAge = ageMs(signals.githubAccountCreatedAt, now)
|
|
531
|
+
if (githubAge === null) {
|
|
532
|
+
step({
|
|
533
|
+
id: 'connect_github',
|
|
534
|
+
label: 'Connect your GitHub account',
|
|
535
|
+
detail:
|
|
536
|
+
'Linking a GitHub account you have had for a while is the fastest way to raise your limits. We read the account and oldest-repo creation dates, which GitHub sets and nobody can backdate.',
|
|
537
|
+
points: 30,
|
|
538
|
+
href: '/web/settings',
|
|
539
|
+
})
|
|
540
|
+
} else {
|
|
541
|
+
add('github_linked', 'GitHub account connected', 10)
|
|
542
|
+
if (githubAge >= 3 * YEAR_MS) {
|
|
543
|
+
add('github_age', 'GitHub account over 3 years old', 20)
|
|
544
|
+
} else if (githubAge >= YEAR_MS) {
|
|
545
|
+
add('github_age', 'GitHub account over a year old', 15)
|
|
546
|
+
} else if (githubAge >= 6 * MONTH_MS) {
|
|
547
|
+
add('github_age', 'GitHub account over 6 months old', 10)
|
|
548
|
+
} else {
|
|
549
|
+
step({
|
|
550
|
+
id: 'github_age',
|
|
551
|
+
label: 'Your GitHub account is still new',
|
|
552
|
+
detail:
|
|
553
|
+
'Account age is worth up to 20 points and grows on its own — nothing to do here but keep the same account connected.',
|
|
554
|
+
points: 10,
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const repoAge = ageMs(signals.githubOldestRepoCreatedAt, now)
|
|
559
|
+
if (repoAge !== null && repoAge >= 6 * MONTH_MS) {
|
|
560
|
+
add('github_repo', 'Public repo over 6 months old', 10)
|
|
561
|
+
}
|
|
562
|
+
if ((signals.githubPublicRepos ?? 0) >= 3) {
|
|
563
|
+
add('github_repos', '3 or more public repos', 5)
|
|
564
|
+
}
|
|
565
|
+
if ((signals.githubFollowers ?? 0) >= 5) {
|
|
566
|
+
add('github_followers', '5 or more GitHub followers', 5)
|
|
567
|
+
}
|
|
568
|
+
if (signals.githubTwoFactorEnabled) {
|
|
569
|
+
add('github_2fa', 'Two-factor auth enabled on GitHub', 5)
|
|
570
|
+
} else if (signals.githubTwoFactorEnabled === false) {
|
|
571
|
+
step({
|
|
572
|
+
id: 'github_2fa',
|
|
573
|
+
label: 'Turn on two-factor auth for GitHub',
|
|
574
|
+
detail:
|
|
575
|
+
'Worth 5 points, and it protects the account your Anvil limits now depend on.',
|
|
576
|
+
points: 5,
|
|
577
|
+
href: 'https://github.com/settings/security',
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// --- Account history ----------------------------------------------------
|
|
583
|
+
// Time and use, which cost an operator real calendar days per account and
|
|
584
|
+
// are the only signals a user gets for free by simply being real.
|
|
585
|
+
const accountAge = ageMs(signals.accountCreatedAt, now)
|
|
586
|
+
if (accountAge !== null) {
|
|
587
|
+
if (accountAge >= 90 * DAY_MS) {
|
|
588
|
+
add('account_age', 'Anvil account over 90 days old', 15)
|
|
589
|
+
} else if (accountAge >= 30 * DAY_MS) {
|
|
590
|
+
add('account_age', 'Anvil account over 30 days old', 10)
|
|
591
|
+
} else if (accountAge >= 7 * DAY_MS) {
|
|
592
|
+
add('account_age', 'Anvil account over 7 days old', 5)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (signals.activeDays >= 30) {
|
|
597
|
+
add('active_days', 'Used Anvil on 30+ days', 10)
|
|
598
|
+
} else if (signals.activeDays >= 7) {
|
|
599
|
+
add('active_days', 'Used Anvil on 7+ days', 5)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// --- Earned -------------------------------------------------------------
|
|
603
|
+
// The routes that work from anywhere, on any account age. This is the answer
|
|
604
|
+
// to "I am in an unsupported region and my account is new": both of these
|
|
605
|
+
// are available today and together are worth 35 points, which is a level and
|
|
606
|
+
// a half.
|
|
607
|
+
const bountyPoints =
|
|
608
|
+
Math.min(signals.approvedBounties, ANVIL_TRUST_EARNED.BOUNTY_CAP) *
|
|
609
|
+
ANVIL_TRUST_EARNED.BOUNTY_POINTS
|
|
610
|
+
if (bountyPoints > 0) {
|
|
611
|
+
add(
|
|
612
|
+
'bounties',
|
|
613
|
+
`${signals.approvedBounties} approved ${signals.approvedBounties === 1 ? 'bounty' : 'bounties'}`,
|
|
614
|
+
bountyPoints,
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
if (bountyPoints < MAX_BOUNTY_POINTS) {
|
|
618
|
+
step({
|
|
619
|
+
id: 'bounties',
|
|
620
|
+
label: 'Complete a bounty',
|
|
621
|
+
detail: `Approved bounties are worth ${ANVIL_TRUST_EARNED.BOUNTY_POINTS} points each, up to ${MAX_BOUNTY_POINTS}. They are reviewed, they work from any country, and they pay session grants on top.`,
|
|
622
|
+
points: MAX_BOUNTY_POINTS - bountyPoints,
|
|
623
|
+
href: '/web/earn',
|
|
624
|
+
})
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const referralPoints =
|
|
628
|
+
Math.min(signals.qualifiedReferrals, ANVIL_TRUST_EARNED.REFERRAL_CAP) *
|
|
629
|
+
ANVIL_TRUST_EARNED.REFERRAL_POINTS
|
|
630
|
+
if (referralPoints > 0) {
|
|
631
|
+
add(
|
|
632
|
+
'referrals',
|
|
633
|
+
`${signals.qualifiedReferrals} qualified ${signals.qualifiedReferrals === 1 ? 'referral' : 'referrals'}`,
|
|
634
|
+
referralPoints,
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
if (referralPoints < MAX_REFERRAL_POINTS) {
|
|
638
|
+
step({
|
|
639
|
+
id: 'referrals',
|
|
640
|
+
label: 'Invite other developers',
|
|
641
|
+
detail: `Each friend who signs up with a real GitHub account and uses Anvil is worth ${ANVIL_TRUST_EARNED.REFERRAL_POINTS} points, up to ${MAX_REFERRAL_POINTS} — plus the referral rewards themselves.`,
|
|
642
|
+
points: MAX_REFERRAL_POINTS - referralPoints,
|
|
643
|
+
href: '/web/earn',
|
|
644
|
+
})
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (signals.hasPaid) {
|
|
648
|
+
add('paid', 'Supported Anvil with a purchase', 25)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// --- Provenance ---------------------------------------------------------
|
|
652
|
+
// Small positives only. These cannot earn a level on their own; their job is
|
|
653
|
+
// to let a clean, ordinary signup reach `verified` without owning anything.
|
|
654
|
+
const signupPrivacy = hasPrivacyEgressAtSignup(signals.signupPrivacySignals)
|
|
655
|
+
if (signupPrivacy === false) {
|
|
656
|
+
add('clean_signup', 'Signed up from a residential connection', 5)
|
|
657
|
+
}
|
|
658
|
+
if (
|
|
659
|
+
signals.signupIpSource === 'edge_secret' ||
|
|
660
|
+
signals.signupIpSource === 'cloudflare'
|
|
661
|
+
) {
|
|
662
|
+
add('verified_signup_ip', 'Verified network at signup', 5)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const score = Math.max(
|
|
666
|
+
0,
|
|
667
|
+
Math.min(
|
|
668
|
+
100,
|
|
669
|
+
factors.reduce((sum, factor) => sum + factor.points, 0),
|
|
670
|
+
),
|
|
671
|
+
)
|
|
672
|
+
const uncappedLevel = levelForScore(score)
|
|
673
|
+
|
|
674
|
+
// --- Caps ---------------------------------------------------------------
|
|
675
|
+
// Applied after scoring, lowest wins. Each one names itself so the UI can
|
|
676
|
+
// explain a cap instead of telling a user with a high score to earn more.
|
|
677
|
+
let level = uncappedLevel
|
|
678
|
+
let cappedBy: string | null = null
|
|
679
|
+
const cap = (limit: AnvilTrustLevel, reason: string) => {
|
|
680
|
+
const capped = lowerOf(level, limit)
|
|
681
|
+
if (capped !== level) {
|
|
682
|
+
level = capped
|
|
683
|
+
cappedBy = reason
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// A reversed ban is invisible here by construction — only unreversed events
|
|
688
|
+
// reach this field — so this is an account we actioned and did not take
|
|
689
|
+
// back. Not a ban (they are already unbanned) and not permanent, but not
|
|
690
|
+
// something to hand extra capacity to either.
|
|
691
|
+
if (signals.hasUnreversedBanEvent) {
|
|
692
|
+
cap('verified', 'past_enforcement')
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Signed up behind a VPN/proxy/Tor/hosting egress. Capped, not zeroed: this
|
|
696
|
+
// describes a lot of privacy-conscious developers as well as every farm, and
|
|
697
|
+
// `established` is what everyone had before this file existed.
|
|
698
|
+
if (signupPrivacy === true) {
|
|
699
|
+
cap('established', 'signup_privacy_egress')
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// The live request is on an anonymizing network. Deliberately the only cap
|
|
703
|
+
// driven by a per-request signal, and deliberately the harshest, because it
|
|
704
|
+
// is the one an abuser toggles: without it, a farm signs up cleanly once and
|
|
705
|
+
// then runs everything through a proxy pool at core-member limits.
|
|
706
|
+
if (signals.currentRiskScore !== null && signals.currentRiskScore >= 75) {
|
|
707
|
+
cap('verified', 'anonymous_network')
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// The sticky flags: things this account has DONE, remembered past the
|
|
711
|
+
// request that revealed them. Without these, every network cap above is
|
|
712
|
+
// defeated by toggling the VPN off for a day — the exact wash-trading of
|
|
713
|
+
// signals the caps exist to prevent. They cap rather than subtract for the
|
|
714
|
+
// standard reason (see "Points earn, penalties cap"), and they grade by
|
|
715
|
+
// evidence weight:
|
|
716
|
+
//
|
|
717
|
+
// corroborated egress -> verified two providers agreed
|
|
718
|
+
// foreign tool schema -> verified behavioural, not network luck
|
|
719
|
+
// ipinfo-only egress -> established one provider, the weak signal --
|
|
720
|
+
// `established` is what every
|
|
721
|
+
// account had before trust levels
|
|
722
|
+
// existed, so this cap forfeits
|
|
723
|
+
// only the `core` upside
|
|
724
|
+
//
|
|
725
|
+
// The 2026-08-03 mass-reversal is the reason none of these goes lower:
|
|
726
|
+
// network-derived evidence has wrongly actioned real users before, and a
|
|
727
|
+
// permanent flag with a harsh cap would make that mistake permanent too.
|
|
728
|
+
if (signals.privacyCorroboratedAt !== null) {
|
|
729
|
+
cap('verified', 'past_corroborated_egress')
|
|
730
|
+
}
|
|
731
|
+
if (signals.thirdPartyClientAt !== null) {
|
|
732
|
+
cap('verified', 'third_party_client')
|
|
733
|
+
}
|
|
734
|
+
if (signals.privacyFlaggedAt !== null) {
|
|
735
|
+
cap('established', 'past_privacy_egress')
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Signup networks and mailboxes that many accounts share. `?? 1` matters:
|
|
739
|
+
// null is unknown (pre-provenance accounts), and unknown must not cap.
|
|
740
|
+
if ((signals.signupPrefixAccountCount ?? 1) >= 8) {
|
|
741
|
+
cap('established', 'shared_signup_network')
|
|
742
|
+
}
|
|
743
|
+
if ((signals.mailboxAccountCount ?? 1) >= 3) {
|
|
744
|
+
cap('verified', 'shared_mailbox')
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Steps are ordered by what they are worth, EXCEPT that a binding cap goes
|
|
748
|
+
// first regardless. A capped account told to "complete a bounty for 20
|
|
749
|
+
// points" when points are not what binds them is being sent on an errand, so
|
|
750
|
+
// the cap is prepended after the sort rather than competing in it — it
|
|
751
|
+
// carries no points and would otherwise sink to the bottom.
|
|
752
|
+
const earnedSteps = nextSteps.sort((a, b) => b.points - a.points)
|
|
753
|
+
const actionableSteps =
|
|
754
|
+
cappedBy === null
|
|
755
|
+
? earnedSteps
|
|
756
|
+
: [
|
|
757
|
+
{
|
|
758
|
+
id: `cap_${cappedBy}`,
|
|
759
|
+
label: CAP_REMEDIES[cappedBy]?.label ?? 'Your level is limited',
|
|
760
|
+
detail:
|
|
761
|
+
CAP_REMEDIES[cappedBy]?.detail ??
|
|
762
|
+
'Something about this account limits how high your level can go.',
|
|
763
|
+
points: 0,
|
|
764
|
+
},
|
|
765
|
+
...earnedSteps,
|
|
766
|
+
]
|
|
767
|
+
|
|
768
|
+
return {
|
|
769
|
+
level,
|
|
770
|
+
score,
|
|
771
|
+
uncappedLevel,
|
|
772
|
+
cappedBy,
|
|
773
|
+
factors: factors.sort((a, b) => b.points - a.points),
|
|
774
|
+
nextSteps: actionableSteps,
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* User-facing explanation of each cap.
|
|
780
|
+
*
|
|
781
|
+
* Two of these describe something the user can fix in under a minute (turn the
|
|
782
|
+
* VPN off, use a different network) and are written to say exactly that. The
|
|
783
|
+
* other two describe history, and say so honestly rather than implying an
|
|
784
|
+
* action that does not exist — a "next step" a user cannot take is worse than
|
|
785
|
+
* no step at all.
|
|
786
|
+
*/
|
|
787
|
+
const CAP_REMEDIES: Record<string, { label: string; detail: string }> = {
|
|
788
|
+
past_corroborated_egress: {
|
|
789
|
+
label: 'This account has used an anonymizing network',
|
|
790
|
+
detail:
|
|
791
|
+
'Requests from this account were confirmed to come through a VPN, proxy or similar exit. That history caps this account at Verified. Everything else still counts toward your level.',
|
|
792
|
+
},
|
|
793
|
+
past_privacy_egress: {
|
|
794
|
+
label: 'This account has connected over a flagged network',
|
|
795
|
+
detail:
|
|
796
|
+
'A connection from this account looked like an anonymizing network. That caps this account at Established. If this seems wrong — some office and university networks are misread — contact support.',
|
|
797
|
+
},
|
|
798
|
+
third_party_client: {
|
|
799
|
+
label: 'A non-Anvil client has used this account',
|
|
800
|
+
detail:
|
|
801
|
+
'Requests from this account carried a client we do not ship. That caps this account at Verified. Only official Anvil apps are supported on free mode.',
|
|
802
|
+
},
|
|
803
|
+
anonymous_network: {
|
|
804
|
+
label: 'Turn off your VPN or proxy',
|
|
805
|
+
detail:
|
|
806
|
+
'We cannot tell where requests from a VPN, proxy or Tor exit node come from, so those connections are capped at Verified no matter how much you have earned. Reconnect from your normal network and your level updates within a few minutes.',
|
|
807
|
+
},
|
|
808
|
+
signup_privacy_egress: {
|
|
809
|
+
label: 'You signed up over a VPN or proxy',
|
|
810
|
+
detail:
|
|
811
|
+
'That caps this account at Established. Everything else still counts, and the cap applies to this account only — it is not a strike against you.',
|
|
812
|
+
},
|
|
813
|
+
shared_signup_network: {
|
|
814
|
+
label: 'Many accounts signed up from your network',
|
|
815
|
+
detail:
|
|
816
|
+
'Shared offices, campuses and carrier NATs all look like this, so it caps rather than blocks. Approved bounties and referrals still raise your limits within the cap.',
|
|
817
|
+
},
|
|
818
|
+
shared_mailbox: {
|
|
819
|
+
label: 'Several accounts share your email address',
|
|
820
|
+
detail:
|
|
821
|
+
'Address variations that reach one inbox (dots, or anything after a +) count as one mailbox. Using a single account raises your level.',
|
|
822
|
+
},
|
|
823
|
+
past_enforcement: {
|
|
824
|
+
label: 'This account was actioned in the past',
|
|
825
|
+
detail:
|
|
826
|
+
'Your access is fully restored, but the level is capped at Verified. Contact support if you think that is wrong.',
|
|
827
|
+
},
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ---------------------------------------------------------------------------
|
|
831
|
+
// Wire shape
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* What a client is told about its own standing.
|
|
836
|
+
*
|
|
837
|
+
* Carries the resolved LIMITS as well as the level, because a client that had
|
|
838
|
+
* to map level → limits itself would hold a second copy of the matrix above
|
|
839
|
+
* and drift from it on the first tuning pass. The server owns the numbers; the
|
|
840
|
+
* client renders whatever it is sent.
|
|
841
|
+
*/
|
|
842
|
+
/**
|
|
843
|
+
* What a level means, in words.
|
|
844
|
+
*
|
|
845
|
+
* ## Why the numbers do not leave the server
|
|
846
|
+
*
|
|
847
|
+
* Three reasons, and the first is the one that matters most:
|
|
848
|
+
*
|
|
849
|
+
* 1. **A published limit is a published target.** `docs/anvil-abuse-
|
|
850
|
+
* detection.md` records that the abuse pattern here is not bursting, it is
|
|
851
|
+
* "sustained pacing just under the daily caps" — so telling an operator
|
|
852
|
+
* exactly where the cap sits is telling them exactly how to sit under it.
|
|
853
|
+
* Every threshold in this file is a number we would rather they had to
|
|
854
|
+
* discover.
|
|
855
|
+
* 2. **The numbers are ours, not theirs.** `dailySpendUsd` in particular is
|
|
856
|
+
* our provider cost, and a user shown "$25/day" learns something about our
|
|
857
|
+
* margins and nothing about what they may do.
|
|
858
|
+
* 3. **Exact figures invite exactly the wrong conversation.** The first
|
|
859
|
+
* version showed them and produced people comparing screenshots and asking
|
|
860
|
+
* whether a smaller number meant they had been punished. A limit is meant
|
|
861
|
+
* to answer "can I get my work done", and that question has a qualitative
|
|
862
|
+
* answer.
|
|
863
|
+
*
|
|
864
|
+
* So `AnvilStandingInfo` carries these phrases and NOT `AnvilTrustLimits`
|
|
865
|
+
* — the raw matrix never crosses the wire, which means no client can render it
|
|
866
|
+
* by accident and no future surface has to remember not to.
|
|
867
|
+
*
|
|
868
|
+
* Where a user genuinely needs a count, they already have an exact one: the
|
|
869
|
+
* model picker renders "N of M sessions used" from the live quota snapshot,
|
|
870
|
+
* which is authoritative and per-model. Duplicating it here could only
|
|
871
|
+
* disagree with it.
|
|
872
|
+
*/
|
|
873
|
+
export interface AnvilStandingHighlight {
|
|
874
|
+
label: string
|
|
875
|
+
value: string
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const LIMIT_PHRASES: Record<
|
|
879
|
+
AnvilTrustLevel,
|
|
880
|
+
{ prompts: string; depth: string; premium: string }
|
|
881
|
+
> = {
|
|
882
|
+
new: {
|
|
883
|
+
prompts: 'Enough to get a project started',
|
|
884
|
+
depth: 'Focused, shorter agent runs',
|
|
885
|
+
premium: 'Occasional access',
|
|
886
|
+
},
|
|
887
|
+
verified: {
|
|
888
|
+
prompts: 'Comfortable for everyday work',
|
|
889
|
+
depth: 'Full agent runs',
|
|
890
|
+
premium: 'Regular access',
|
|
891
|
+
},
|
|
892
|
+
established: {
|
|
893
|
+
prompts: 'Comfortable on heavy days',
|
|
894
|
+
depth: 'Long runs with plenty of subagents',
|
|
895
|
+
premium: 'Generous access',
|
|
896
|
+
},
|
|
897
|
+
core: {
|
|
898
|
+
prompts: 'The most we offer',
|
|
899
|
+
depth: 'The most we offer',
|
|
900
|
+
premium: 'The most we offer',
|
|
901
|
+
},
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
export function anvilStandingHighlights(
|
|
905
|
+
accessTier: AnvilAccessTier,
|
|
906
|
+
level: AnvilTrustLevel,
|
|
907
|
+
): AnvilStandingHighlight[] {
|
|
908
|
+
const phrases = LIMIT_PHRASES[level]
|
|
909
|
+
return [
|
|
910
|
+
{ label: 'Prompts a day', value: phrases.prompts },
|
|
911
|
+
{ label: 'Work per prompt', value: phrases.depth },
|
|
912
|
+
{
|
|
913
|
+
label: 'Premium models',
|
|
914
|
+
// Stated as a region fact rather than as something this account lacks:
|
|
915
|
+
// no level in the limited row can reach a premium model, so framing it
|
|
916
|
+
// as a level shortfall would send the user chasing points that cannot
|
|
917
|
+
// buy it.
|
|
918
|
+
value:
|
|
919
|
+
anvilTrustLimits(accessTier, level).premiumSessionsPerDay > 0
|
|
920
|
+
? phrases.premium
|
|
921
|
+
: 'Not available in your region yet',
|
|
922
|
+
},
|
|
923
|
+
]
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* NOTE FOR CALLERS: `highlights` is what the level WOULD grant, which is only
|
|
928
|
+
* what the account actually gets once `ANVIL_TRUST_LEVELS=enforce`. Both
|
|
929
|
+
* producers gate on that (the Earn route and the session `standing` field), so
|
|
930
|
+
* a client that receives this can render it as fact. A third producer must do
|
|
931
|
+
* the same — see the comment in anvil/web/src/app/api/web/standing/route.ts
|
|
932
|
+
* for what happens otherwise.
|
|
933
|
+
*/
|
|
934
|
+
export interface AnvilStandingInfo {
|
|
935
|
+
level: AnvilTrustLevel
|
|
936
|
+
label: string
|
|
937
|
+
blurb: string
|
|
938
|
+
score: number
|
|
939
|
+
/** Score at which the next level starts, or null at `core`. */
|
|
940
|
+
nextLevelAt: number | null
|
|
941
|
+
nextLevel: AnvilTrustLevel | null
|
|
942
|
+
cappedBy: string | null
|
|
943
|
+
cappedReason: string | null
|
|
944
|
+
factors: AnvilTrustFactor[]
|
|
945
|
+
nextSteps: AnvilTrustNextStep[]
|
|
946
|
+
accessTier: AnvilAccessTier
|
|
947
|
+
/** Semantic, never numeric — see AnvilStandingHighlight. */
|
|
948
|
+
highlights: AnvilStandingHighlight[]
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export function toAnvilStandingInfo(
|
|
952
|
+
assessment: AnvilTrustAssessment,
|
|
953
|
+
accessTier: AnvilAccessTier,
|
|
954
|
+
): AnvilStandingInfo {
|
|
955
|
+
const index = ANVIL_TRUST_LEVELS.indexOf(assessment.level)
|
|
956
|
+
const nextLevel = ANVIL_TRUST_LEVELS[index + 1] ?? null
|
|
957
|
+
return {
|
|
958
|
+
level: assessment.level,
|
|
959
|
+
label: ANVIL_TRUST_LEVEL_LABELS[assessment.level],
|
|
960
|
+
blurb: ANVIL_TRUST_LEVEL_BLURBS[assessment.level],
|
|
961
|
+
score: assessment.score,
|
|
962
|
+
nextLevel,
|
|
963
|
+
nextLevelAt:
|
|
964
|
+
nextLevel && nextLevel !== 'new'
|
|
965
|
+
? ANVIL_TRUST_THRESHOLDS[nextLevel]
|
|
966
|
+
: null,
|
|
967
|
+
cappedBy: assessment.cappedBy,
|
|
968
|
+
cappedReason: assessment.cappedBy
|
|
969
|
+
? (CAP_REMEDIES[assessment.cappedBy]?.detail ?? null)
|
|
970
|
+
: null,
|
|
971
|
+
factors: assessment.factors,
|
|
972
|
+
nextSteps: assessment.nextSteps,
|
|
973
|
+
accessTier,
|
|
974
|
+
highlights: anvilStandingHighlights(accessTier, assessment.level),
|
|
975
|
+
}
|
|
976
|
+
}
|