@intentic/sandbox-contract 1.240.0 → 1.243.0
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/README.md +52 -16
- package/dist/agent-catalog.d.ts +5 -28
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +21 -157
- package/dist/agent-catalog.js.map +1 -1
- package/dist/agent-run-model.d.ts +3 -2
- package/dist/agent-run-model.d.ts.map +1 -1
- package/dist/agent-run-model.js +9 -6
- package/dist/agent-run-model.js.map +1 -1
- package/dist/agent-runtimes.d.ts +27 -0
- package/dist/agent-runtimes.d.ts.map +1 -0
- package/dist/agent-runtimes.js +113 -0
- package/dist/agent-runtimes.js.map +1 -0
- package/dist/approvals-execution.d.ts +11 -0
- package/dist/approvals-execution.d.ts.map +1 -0
- package/dist/approvals-execution.js +44 -0
- package/dist/approvals-execution.js.map +1 -0
- package/dist/arrival.d.ts +151 -0
- package/dist/arrival.d.ts.map +1 -0
- package/dist/arrival.js +56 -0
- package/dist/arrival.js.map +1 -0
- package/dist/card-status.d.ts +5 -0
- package/dist/card-status.d.ts.map +1 -0
- package/dist/card-status.js +74 -0
- package/dist/card-status.js.map +1 -0
- package/dist/chores/chores.d.ts.map +1 -1
- package/dist/chores/chores.js +1 -1
- package/dist/chores/chores.js.map +1 -1
- package/dist/command-classes.d.ts +0 -1
- package/dist/command-classes.d.ts.map +1 -1
- package/dist/command-classes.js +9 -2
- package/dist/command-classes.js.map +1 -1
- package/dist/command-run.d.ts +10 -0
- package/dist/command-run.d.ts.map +1 -0
- package/dist/command-run.js +46 -0
- package/dist/command-run.js.map +1 -0
- package/dist/contracts/agent.contract.d.ts +991 -771
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +345 -337
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +20 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/approvals.contract.d.ts +99 -0
- package/dist/contracts/approvals.contract.d.ts.map +1 -0
- package/dist/contracts/approvals.contract.js +32 -0
- package/dist/contracts/approvals.contract.js.map +1 -0
- package/dist/contracts/ci.contract.d.ts +1 -4
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.js +1 -9
- package/dist/contracts/ci.contract.js.map +1 -1
- package/dist/contracts/claude.contract.d.ts +9 -0
- package/dist/contracts/claude.contract.d.ts.map +1 -1
- package/dist/contracts/cursor.contract.d.ts +6 -0
- package/dist/contracts/cursor.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +63 -1
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.js +22 -4
- package/dist/contracts/git.contract.js.map +1 -1
- package/dist/contracts/grok.contract.d.ts +3 -0
- package/dist/contracts/grok.contract.d.ts.map +1 -1
- package/dist/contracts/keys.contract.d.ts +81 -0
- package/dist/contracts/keys.contract.d.ts.map +1 -0
- package/dist/contracts/keys.contract.js +51 -0
- package/dist/contracts/keys.contract.js.map +1 -0
- package/dist/contracts/panels.contract.d.ts +7 -0
- package/dist/contracts/panels.contract.d.ts.map +1 -1
- package/dist/contracts/prepush.contract.js +2 -2
- package/dist/contracts/providers.contract.d.ts +2 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +84 -86
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/safety.contract.d.ts +35 -0
- package/dist/contracts/safety.contract.d.ts.map +1 -0
- package/dist/contracts/safety.contract.js +32 -0
- package/dist/contracts/safety.contract.js.map +1 -0
- package/dist/contracts/sessions.contract.d.ts +73 -337
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +30 -32
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +139 -337
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +31 -3
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.d.ts +29 -20
- package/dist/contracts/usage.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.js +13 -0
- package/dist/contracts/usage.contract.js.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +8 -0
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/conversation-ids.d.ts +2 -0
- package/dist/conversation-ids.d.ts.map +1 -1
- package/dist/conversation-ids.js +8 -0
- package/dist/conversation-ids.js.map +1 -1
- package/dist/credential-material.d.ts +1 -0
- package/dist/credential-material.d.ts.map +1 -1
- package/dist/credential-material.js +24 -2
- package/dist/credential-material.js.map +1 -1
- package/dist/definition.d.ts +51 -93
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +6 -28
- package/dist/definition.js.map +1 -1
- package/dist/events.d.ts +1831 -2285
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +155 -55
- package/dist/events.js.map +1 -1
- package/dist/history-state.d.ts.map +1 -1
- package/dist/history-state.js +1 -0
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +1959 -1903
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/ingress-protocol.d.ts +13 -1
- package/dist/ingress-protocol.d.ts.map +1 -1
- package/dist/ingress-protocol.js +19 -0
- package/dist/ingress-protocol.js.map +1 -1
- package/dist/mentions.d.ts +3 -0
- package/dist/mentions.d.ts.map +1 -0
- package/dist/mentions.js +13 -0
- package/dist/mentions.js.map +1 -0
- package/dist/needs-action.d.ts +7 -0
- package/dist/needs-action.d.ts.map +1 -0
- package/dist/needs-action.js +3 -0
- package/dist/needs-action.js.map +1 -0
- package/dist/overlay-lint.d.ts +7 -0
- package/dist/overlay-lint.d.ts.map +1 -0
- package/dist/overlay-lint.js +68 -0
- package/dist/overlay-lint.js.map +1 -0
- package/dist/owner-ticket.d.ts +18 -0
- package/dist/owner-ticket.d.ts.map +1 -0
- package/dist/owner-ticket.js +51 -0
- package/dist/owner-ticket.js.map +1 -0
- package/dist/plan-pools.d.ts +13 -0
- package/dist/plan-pools.d.ts.map +1 -0
- package/dist/plan-pools.js +31 -0
- package/dist/plan-pools.js.map +1 -0
- package/dist/provider-specs.d.ts +228 -0
- package/dist/provider-specs.d.ts.map +1 -0
- package/dist/provider-specs.js +124 -0
- package/dist/provider-specs.js.map +1 -0
- package/dist/quick-model.d.ts.map +1 -1
- package/dist/quick-model.js +2 -1
- package/dist/quick-model.js.map +1 -1
- package/dist/runner-protocol.js +2 -2
- package/dist/runner-protocol.js.map +1 -1
- package/dist/runtime-state.d.ts +2 -2
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/runtime-state.js +1 -1
- package/dist/runtime-state.js.map +1 -1
- package/dist/safety-policy.d.ts +49 -0
- package/dist/safety-policy.d.ts.map +1 -0
- package/dist/safety-policy.js +52 -0
- package/dist/safety-policy.js.map +1 -0
- package/dist/schemas/agent.d.ts +21 -3
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +18 -8
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +19 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +15 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/approvals.d.ts +241 -0
- package/dist/schemas/approvals.d.ts.map +1 -0
- package/dist/schemas/approvals.js +63 -0
- package/dist/schemas/approvals.js.map +1 -0
- package/dist/schemas/automations.d.ts +5 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/ci.d.ts +5 -9
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js +3 -10
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/computers.d.ts +40 -5
- package/dist/schemas/computers.d.ts.map +1 -1
- package/dist/schemas/computers.js +16 -5
- package/dist/schemas/computers.js.map +1 -1
- package/dist/schemas/environment.d.ts +11 -131
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js +5 -42
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/git-history.d.ts +4 -0
- package/dist/schemas/git-history.d.ts.map +1 -1
- package/dist/schemas/git.d.ts +179 -0
- package/dist/schemas/git.d.ts.map +1 -1
- package/dist/schemas/git.js +39 -1
- package/dist/schemas/git.js.map +1 -1
- package/dist/schemas/panels.d.ts +21 -0
- package/dist/schemas/panels.d.ts.map +1 -1
- package/dist/schemas/panels.js +5 -0
- package/dist/schemas/panels.js.map +1 -1
- package/dist/schemas/plan-limits.d.ts +50 -3
- package/dist/schemas/plan-limits.d.ts.map +1 -1
- package/dist/schemas/plan-limits.js +11 -6
- package/dist/schemas/plan-limits.js.map +1 -1
- package/dist/schemas/provider-oauth.d.ts +6 -0
- package/dist/schemas/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/provider-subscriptions.d.ts +2 -1
- package/dist/schemas/provider-subscriptions.d.ts.map +1 -1
- package/dist/schemas/provider-subscriptions.js +2 -1
- package/dist/schemas/provider-subscriptions.js.map +1 -1
- package/dist/schemas/settings.d.ts +29 -16
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +13 -20
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +3 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +5 -1
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/workspace-repos.d.ts +14 -0
- package/dist/schemas/workspace-repos.d.ts.map +1 -1
- package/dist/schemas/workspace-repos.js +5 -0
- package/dist/schemas/workspace-repos.js.map +1 -1
- package/dist/schemas/workspace-tree.d.ts +1 -0
- package/dist/schemas/workspace-tree.d.ts.map +1 -1
- package/dist/schemas/workspace-tree.js +3 -0
- package/dist/schemas/workspace-tree.js.map +1 -1
- package/dist/transcript-fold.d.ts +31 -0
- package/dist/transcript-fold.d.ts.map +1 -0
- package/dist/transcript-fold.js +457 -0
- package/dist/transcript-fold.js.map +1 -0
- package/dist/workspace-state.d.ts +20 -3
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +22 -2
- package/dist/workspace-state.js.map +1 -1
- package/package.json +27 -5
- package/src/agent-catalog.test.ts +1 -1
- package/src/agent-catalog.ts +76 -555
- package/src/agent-run-model.test.ts +31 -15
- package/src/agent-run-model.ts +26 -12
- package/src/agent-runtimes.ts +419 -0
- package/src/approvals-execution.ts +96 -0
- package/src/arrival.ts +160 -0
- package/src/capability-ledger.test.ts +6 -4
- package/src/card-status.ts +93 -0
- package/src/chores/chores.ts +6 -3
- package/src/command-classes.test.ts +25 -0
- package/src/command-classes.ts +47 -5
- package/src/command-run.ts +78 -0
- package/src/contract-lock.test.ts +1 -1
- package/src/contract-lock.ts +2 -2
- package/src/contracts/agents.contract.ts +35 -2
- package/src/contracts/approvals.contract.ts +37 -0
- package/src/contracts/ci.contract.ts +0 -13
- package/src/contracts/git.contract.ts +28 -3
- package/src/contracts/keys.contract.ts +79 -0
- package/src/contracts/prepush.contract.ts +3 -3
- package/src/contracts/safety.contract.ts +48 -0
- package/src/contracts/usage.contract.ts +22 -0
- package/src/conversation-ids.test.ts +31 -1
- package/src/conversation-ids.ts +37 -3
- package/src/credential-material.test.ts +69 -1
- package/src/credential-material.ts +86 -5
- package/src/definition.ts +25 -53
- package/src/events.ts +374 -173
- package/src/history-state.ts +6 -0
- package/src/index.ts +24 -5
- package/src/ingress-protocol.ts +52 -1
- package/src/mentions.test.ts +22 -0
- package/src/mentions.ts +25 -0
- package/src/needs-action.ts +14 -0
- package/src/overlay-lint.test.ts +89 -0
- package/src/overlay-lint.ts +116 -0
- package/src/owner-ticket.test.ts +54 -0
- package/src/owner-ticket.ts +102 -0
- package/src/plan-pools.test.ts +71 -0
- package/src/plan-pools.ts +92 -0
- package/src/provider-specs.test.ts +130 -0
- package/src/provider-specs.ts +352 -0
- package/src/quick-model.ts +9 -5
- package/src/runner-protocol.ts +2 -2
- package/src/runtime-state.ts +6 -6
- package/src/safety-policy.ts +165 -0
- package/src/schemas/agent.ts +62 -17
- package/src/schemas/agents.ts +71 -0
- package/src/schemas/approvals.ts +174 -0
- package/src/schemas/ci.ts +19 -26
- package/src/schemas/computers.ts +91 -15
- package/src/schemas/environment.ts +21 -100
- package/src/schemas/git.ts +121 -6
- package/src/schemas/issues.ts +1 -1
- package/src/schemas/panels.ts +20 -0
- package/src/schemas/plan-limits.ts +58 -11
- package/src/schemas/provider-subscriptions.ts +7 -2
- package/src/schemas/settings.ts +67 -93
- package/src/schemas/system.ts +12 -1
- package/src/schemas/workspace-repos.ts +9 -0
- package/src/schemas/workspace-tree.ts +11 -0
- package/src/transcript-fold.test.ts +493 -0
- package/src/transcript-fold.ts +655 -0
- package/src/workspace-state.test.ts +77 -29
- package/src/workspace-state.ts +99 -31
- package/dist/contracts/drafts.contract.d.ts +0 -57
- package/dist/contracts/drafts.contract.d.ts.map +0 -1
- package/dist/contracts/drafts.contract.js +0 -32
- package/dist/contracts/drafts.contract.js.map +0 -1
- package/dist/definition-action.d.ts +0 -7
- package/dist/definition-action.d.ts.map +0 -1
- package/dist/definition-action.js +0 -3
- package/dist/definition-action.js.map +0 -1
- package/dist/publish-drafts.d.ts +0 -7
- package/dist/publish-drafts.d.ts.map +0 -1
- package/dist/publish-drafts.js +0 -20
- package/dist/publish-drafts.js.map +0 -1
- package/dist/schemas/drafts.d.ts +0 -84
- package/dist/schemas/drafts.d.ts.map +0 -1
- package/dist/schemas/drafts.js +0 -44
- package/dist/schemas/drafts.js.map +0 -1
- package/src/contracts/drafts.contract.ts +0 -35
- package/src/definition-action.ts +0 -8
- package/src/publish-drafts.ts +0 -59
- package/src/schemas/drafts.ts +0 -108
package/src/agent-catalog.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ACP, type AgentCapabilities, CLAUDE_CODE, PI } from "./agent-runtimes.js";
|
|
2
|
+
import { type NativeProvider, PROVIDER_SPECS, type ProviderAccess, providerSpec } from "./provider-specs.js";
|
|
3
|
+
import type { AgentHarness, AgentProvider, PermissionMode } from "./schemas/agent.js";
|
|
2
4
|
import type { Model, ModelBadge } from "./schemas/provider-oauth.js";
|
|
3
5
|
|
|
4
6
|
/* The provider / harness / model catalog every picker shares (the chat menu, the automations dialog), pure
|
|
5
7
|
* data keyed by the wire vocabulary in schemas/agent.ts, so the surfaces can't drift. Live state stays with the
|
|
6
8
|
* consumer (native Grok's model list is the daemon's own catalog for it, layered on top of modelsFor by the
|
|
7
|
-
* web; ACP providers are merged in from the installed `agent` capabilities).
|
|
9
|
+
* web; ACP providers are merged in from the installed `agent` capabilities).
|
|
10
|
+
*
|
|
11
|
+
* NOTHING HERE IS A LIST OF PROVIDERS ANY MORE. Every table below is DERIVED from PROVIDER_SPECS, which is the
|
|
12
|
+
* one row-per-provider table this package keeps (provider-specs.ts, and its header says why). What remains here
|
|
13
|
+
* is the shape each surface wants that table in, plus the rules that are about something other than a provider
|
|
14
|
+
* (the trial, the endpoint namespace, the effort and fast-mode gates). */
|
|
8
15
|
|
|
9
16
|
export interface CatalogOption {
|
|
10
17
|
readonly label: string;
|
|
@@ -13,86 +20,35 @@ export interface CatalogOption {
|
|
|
13
20
|
|
|
14
21
|
// The NATIVE agent runtimes; ACP providers are appended by the consumer from the installed capabilities.
|
|
15
22
|
// New conversations use the selection, open ones stay locked (the pill reflects the locked provider). The
|
|
16
|
-
// brand logo per provider is drawn by ProviderLogo (by value).
|
|
17
|
-
export const PROVIDERS: readonly { label: string; value: NativeProvider }[] =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{ label: "Kimi Code", value: "kimi" },
|
|
22
|
-
// Labelled for the ACCOUNT, not the model family: the `gemini` id names one channel. Google's Antigravity,
|
|
23
|
-
// and that channel vends Claude and GPT-OSS models alongside Gemini's own (see gemini-models.ts). A section
|
|
24
|
-
// headed "Gemini" holding Claude Opus would be a lie; "Google" is what the whole list has in common.
|
|
25
|
-
{ label: "Google", value: "gemini" },
|
|
26
|
-
// Cursor's own agent runtime, driven through the SDK Anysphere publishes, on the user's Cursor subscription.
|
|
27
|
-
// Like Google above, the label names the ACCOUNT rather than a model family: the channel vends Anthropic,
|
|
28
|
-
// OpenAI and xAI models alongside Cursor's own Composer, and no model name covers that list.
|
|
29
|
-
{ label: "Cursor", value: "cursor" },
|
|
30
|
-
];
|
|
23
|
+
// brand logo per provider is drawn by ProviderLogo (by value). Order is the spec table's order.
|
|
24
|
+
export const PROVIDERS: readonly { label: string; value: NativeProvider }[] = PROVIDER_SPECS.map((spec) => ({
|
|
25
|
+
label: spec.label,
|
|
26
|
+
value: spec.id,
|
|
27
|
+
}));
|
|
31
28
|
|
|
32
29
|
// What it COSTS to unlock a provider, and what the user connects to do it, the axis the picker groups on, since
|
|
33
|
-
// "can this row actually run" is the first thing a model list has to answer.
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
readonly requirement: string;
|
|
42
|
-
// What connecting it lets them run, for the connect gate's one-line pitch.
|
|
43
|
-
readonly runs: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = {
|
|
47
|
-
claude: { kind: "subscription", requirement: "Claude subscription", runs: "Claude Code" },
|
|
48
|
-
codex: { kind: "subscription", requirement: "ChatGPT subscription", runs: "Codex" },
|
|
49
|
-
grok: { kind: "subscription", requirement: "SuperGrok subscription", runs: "Grok" },
|
|
50
|
-
kimi: { kind: "subscription", requirement: "Kimi Code subscription", runs: "Kimi Code" },
|
|
51
|
-
gemini: { kind: "free", requirement: "Google sign-in", runs: "Gemini, Claude and GPT-OSS under Claude Code" },
|
|
52
|
-
// A `subscription` like the first four, and the requirement names the PLAN rather than the account, because
|
|
53
|
-
// a free Cursor account signs in perfectly and still cannot run a turn here: the SDK behind this provider is
|
|
54
|
-
// gated to the paid tiers. Saying "Cursor account" would send someone to a sign-in that ends in a refusal
|
|
55
|
-
// they had no way to predict.
|
|
56
|
-
cursor: { kind: "subscription", requirement: "Cursor Pro subscription", runs: "Cursor Agent" },
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/* THE PROVIDERS THAT COST NOTHING, derived from the table above rather than named a second time, and read by
|
|
60
|
-
* every surface that LEADS with a free option instead of merely labelling one.
|
|
30
|
+
// "can this row actually run" is the first thing a model list has to answer. See ProviderAccess in
|
|
31
|
+
// provider-specs.ts for what `free` means here, and why it is not a courtesy tier.
|
|
32
|
+
export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = Object.fromEntries(
|
|
33
|
+
PROVIDER_SPECS.map((spec) => [spec.id, spec.access] as const),
|
|
34
|
+
) as Record<NativeProvider, ProviderAccess>;
|
|
35
|
+
|
|
36
|
+
/* THE PROVIDERS THAT COST NOTHING, read by every surface that LEADS with a free option instead of merely
|
|
37
|
+
* labelling one.
|
|
61
38
|
*
|
|
62
39
|
* The distinction is worth the export. `accessBadge` answers "what does this row cost" for a row the user is
|
|
63
40
|
* already looking at; this answers "which row should a user who has connected nothing be shown FIRST", which is
|
|
64
41
|
* the connect gate's whole job. Ranking the one free channel last among equal buttons is how a user with
|
|
65
42
|
* no subscription concluded the product needed one. Deriving the list keeps that promotion honest: a channel
|
|
66
|
-
* that stops being free stops being promoted, from one edit to
|
|
67
|
-
export const FREE_PROVIDERS: readonly NativeProvider[] =
|
|
43
|
+
* that stops being free stops being promoted, from one edit to its spec row. */
|
|
44
|
+
export const FREE_PROVIDERS: readonly NativeProvider[] = PROVIDER_SPECS.filter((spec) => spec.access.kind === "free").map((spec) => spec.id);
|
|
68
45
|
export const isFreeProvider = (provider: AgentProvider): boolean => FREE_PROVIDERS.includes(provider as NativeProvider);
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* The routed providers are why this can't be inferred from the harness: a `gemini` turn drives Claude Opus 4.6
|
|
76
|
-
* through Google's Antigravity channel on a plain Google sign-in, so the quota that refuses it is Google's and
|
|
77
|
-
* Anthropic has no part in it. Saying "Claude usage limit reached" there sends the user to check the wrong
|
|
78
|
-
* account, and to a reset that is days out on a pool they never touched. */
|
|
79
|
-
export const PROVIDER_VENDOR: Record<NativeProvider, string> = {
|
|
80
|
-
claude: "Claude",
|
|
81
|
-
codex: "ChatGPT",
|
|
82
|
-
grok: "xAI",
|
|
83
|
-
kimi: "Kimi Code",
|
|
84
|
-
gemini: "Google",
|
|
85
|
-
// The plan that gets billed is Cursor's, whichever vendor's model actually answered. A Cursor turn on Claude
|
|
86
|
-
// Opus spends Cursor's included usage and Anthropic has no part in it, the same reasoning that makes a
|
|
87
|
-
// `gemini` turn say "Google" above.
|
|
88
|
-
cursor: "Cursor",
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// What a turn on this provider costs at the MARGIN, ordering the same three kinds by the only question a
|
|
92
|
-
// helper spending the user's money on their behalf has to answer: free is free; a subscription is already paid
|
|
93
|
-
// but has a quota the user watches; a key is metered, so every call is real money. Deliberately not folded into
|
|
94
|
-
// AccessKind's declaration order, a union's order is not a runtime fact, and this one is relied on.
|
|
95
|
-
export const ACCESS_COST: Record<AccessKind, number> = { free: 0, subscription: 1, key: 2 };
|
|
47
|
+
// WHOSE ALLOWANCE A TURN ON THIS PROVIDER SPENDS, as the subject of a sentence. See ProviderSpec.vendor for why
|
|
48
|
+
// this is a third naming of the same ids rather than a duplicate of the label or the requirement.
|
|
49
|
+
export const PROVIDER_VENDOR: Record<NativeProvider, string> = Object.fromEntries(
|
|
50
|
+
PROVIDER_SPECS.map((spec) => [spec.id, spec.vendor] as const),
|
|
51
|
+
) as Record<NativeProvider, string>;
|
|
96
52
|
|
|
97
53
|
/* THE PROVIDER ID OF AN `endpoint` CAPABILITY, a model API the user pointed us at, native or not, near or far.
|
|
98
54
|
*
|
|
@@ -155,495 +111,52 @@ export const endpointIdOf = (provider: AgentProvider): string | undefined =>
|
|
|
155
111
|
// so there is likewise nothing left to connect. What a turn on it COSTS is deliberately not claimed here, a
|
|
156
112
|
// self-hosted model on the user's own GPU and a metered gateway key are the same shape to us, and inventing an
|
|
157
113
|
// AccessKind for them would have the picker assert a price the daemon has no way to know.
|
|
158
|
-
export const accessFor = (provider: AgentProvider): ProviderAccess | undefined =>
|
|
114
|
+
export const accessFor = (provider: AgentProvider): ProviderAccess | undefined => providerSpec(provider)?.access;
|
|
159
115
|
|
|
160
116
|
// An ACP provider's label is its capability's display name, which the web layers on top, the raw id is the
|
|
161
117
|
// static fallback.
|
|
162
|
-
export const providerLabel = (provider: AgentProvider): string =>
|
|
118
|
+
export const providerLabel = (provider: AgentProvider): string => providerSpec(provider)?.label ?? provider;
|
|
163
119
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
* difference between "this plan publishes nothing" and "we haven't measured yet", two states that look
|
|
168
|
-
* identical as a blank row and mean opposite things.
|
|
169
|
-
*
|
|
170
|
-
* Four can be read, by two mechanisms that stop at the daemon's readers: Claude's rides its own turn (the
|
|
171
|
-
* OAuth usage endpoint, agent.ts), ChatGPT's, Google's and Kimi's are pulled through the translator's
|
|
172
|
-
* credential-scoped api-call. Kimi's endpoint is the platform's own `/coding/v1/usages`, which the Kimi Code
|
|
173
|
-
* subscription's OAuth token reads directly, the bundled translator does not route it, but it does not have
|
|
174
|
-
* to: the api-call substitutes that token server-side like it does for the other two.
|
|
175
|
-
*
|
|
176
|
-
* Grok is the one absence, because xAI's usable billing data needs a subject id CLIProxyAPI keeps out of its
|
|
177
|
-
* auth-file listing, and the fallback probe spends a token to answer. Adding it is adding a reader and its name
|
|
178
|
-
* here, and nothing else. */
|
|
179
|
-
export const PLAN_LIMIT_PROVIDERS: readonly NativeProvider[] = ["claude", "codex", "gemini", "kimi"];
|
|
120
|
+
// Whether a plan-limit reading for this provider is OBTAINABLE at all. See ProviderSpec.planLimits for which
|
|
121
|
+
// four can be read, by which two mechanisms, and why the absences are absences rather than gaps.
|
|
122
|
+
export const PLAN_LIMIT_PROVIDERS: readonly NativeProvider[] = PROVIDER_SPECS.filter((spec) => spec.planLimits).map((spec) => spec.id);
|
|
180
123
|
export const reportsPlanLimits = (provider: AgentProvider): boolean => PLAN_LIMIT_PROVIDERS.includes(provider as NativeProvider);
|
|
181
124
|
|
|
182
125
|
// The harness (agentic loop) a turn runs on, orthogonal to the provider. `native` = the provider's own runtime;
|
|
183
126
|
// `claude-code` = the Claude Code loop for any provider (codex/grok then route through the translator).
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// Gemini's `native` is OpenCode rather than a Google CLI: the image ships no Gemini binary, and OpenCode is
|
|
190
|
-
// already here driving Grok. It spends the same translator accounts a routed turn would have, what Google
|
|
191
|
-
// refuses is the loop, never the credential.
|
|
127
|
+
// WHICH providers actually offer the choice is not a list any more: a spec whose two runtime records are the
|
|
128
|
+
// same one is a provider with nothing to choose, and every surface reads that rather than remembering the
|
|
129
|
+
// exceptions (Claude is always its own loop; kimi, meta and zai have no native runtime to switch to; and GEMINI
|
|
130
|
+
// IS THE MIRROR, it only exists under its native one, because Google refuses Claude Code's traffic outright).
|
|
131
|
+
// See AgentHarness in schemas/agent.ts.
|
|
192
132
|
export const HARNESSES: readonly { label: string; value: AgentHarness }[] = [
|
|
193
133
|
{ label: "Native", value: "native" },
|
|
194
134
|
{ label: "Claude Code", value: "claude-code" },
|
|
195
135
|
];
|
|
196
136
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
* every tool call is pre-approved, and offered a reasoning-effort scale to a runtime that drops the field.
|
|
204
|
-
*
|
|
205
|
-
* A capability is listed here only if something READS it: the daemon gates a seam on it, the composer hides or
|
|
206
|
-
* clamps a control by it, or `limitationsOf` tells the user about it. That is the whole point, an ability the
|
|
207
|
-
* matrix claims and nothing consults is how the drift started.
|
|
208
|
-
*
|
|
209
|
-
* Adding a provider is a row here, not a hunt for literals; agent-catalog.test.ts walks PROVIDERS × HARNESSES
|
|
210
|
-
* and demands one, so a pair can never be silently absent. */
|
|
211
|
-
|
|
212
|
-
// An execution backend: one way a turn runs work of its own, named for the AgentCapabilities.execution axis
|
|
213
|
-
// and for the persona switch that grants it. Adding a language is a member here and a backend in the daemon's
|
|
214
|
-
// execution/ module, never a new one-off tool wired where nothing else can see it.
|
|
215
|
-
export type ExecutionBackend = "shell" | "js";
|
|
216
|
-
|
|
217
|
-
export interface AgentCapabilities {
|
|
218
|
-
// Which agentic loop actually serves the turn, the question "is the harness `claude-code`" only looks like.
|
|
219
|
-
// Claude is always its own Claude Code loop and Kimi has no native runtime, so both run it whatever harness
|
|
220
|
-
// the client sent; codex/grok/gemini each have a native runtime to switch away from. Names the session store
|
|
221
|
-
// a finished conversation's transcript is backfilled from, too.
|
|
222
|
-
//
|
|
223
|
-
// `opencode-gemini` is the OpenCode loop pointed at Gemini rather than at xAI, and it is a SEPARATE runtime
|
|
224
|
-
// id from `opencode` on purpose: adapter health is keyed by this field (adapter-health.ts), so sharing one
|
|
225
|
-
// would make Grok's xAI credential decide whether the picker greys out Gemini, and the reverse.
|
|
226
|
-
readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "pi" | "cursor";
|
|
227
|
-
// Mid-turn injection (the SteeringQueue behind /agent/steer). Needs the SDK's streaming-input mode.
|
|
228
|
-
readonly steering: boolean;
|
|
229
|
-
// How much of the permission-mode axis the runtime honours. "modes" = every PermissionMode, with per-tool
|
|
230
|
-
// permission cards and `mode` frames when the agent moves itself; "plan" = propose-then-approve or run, and
|
|
231
|
-
// nothing in between, the container is the isolation boundary and every tool call is pre-approved.
|
|
232
|
-
readonly permissions: "modes" | "plan";
|
|
233
|
-
// Can stop mid-turn and ask the user a multiple-choice question (`question` frames).
|
|
234
|
-
readonly questions: boolean;
|
|
235
|
-
/* Which of the turn's tools reach the agent. "full" = http MCP tools + in-process SDK servers + plugin
|
|
236
|
-
* checkouts + the browser servers; "tools" = all of that EXCEPT plugin checkouts; "browser" = the
|
|
237
|
-
* process-backed browser servers alone; "http" = the http MCP tools alone, and only if the agent advertises
|
|
238
|
-
* http MCP support; "none" = the runtime has no seam for them at all. Keeping the partial answers distinct
|
|
239
|
-
* matters: a runtime that can drive a connected account must not be described as tool-less, and one that
|
|
240
|
-
* cannot host daemon-side SDK servers must not claim full.
|
|
241
|
-
*
|
|
242
|
-
* "tools" exists for the Cursor runtime and would have been a lie either way without it. Cursor's SDK takes
|
|
243
|
-
* stdio AND http/sse MCP servers, and its `customTools` run host callbacks in this process, which is the
|
|
244
|
-
* seam an in-process SDK server needs, so calling it "browser" would understate it by three whole
|
|
245
|
-
* categories. What it genuinely cannot host is a Claude Code PLUGIN checkout: that is a directory layout the
|
|
246
|
-
* Agent SDK loads, not a protocol, and no other runtime will ever read one. So the gap is real, permanent
|
|
247
|
-
* and worth its own word rather than being rounded to "full". */
|
|
248
|
-
readonly mcp: "full" | "tools" | "browser" | "http" | "none";
|
|
249
|
-
/* WHICH EXECUTION BACKENDS THE RUNTIME HOSTS, the ways a turn RUNS things, as opposed to the tools it is
|
|
250
|
-
* handed. "shell" is the runtime's own command tool (Bash on the Claude Code loop, each foreign loop's
|
|
251
|
-
* equivalent); "js" is the sandbox's JavaScript backend (execution/ in the daemon): the model writes a
|
|
252
|
-
* script instead of a command line, and the daemon runs it in a permission-fenced Node subprocess.
|
|
253
|
-
*
|
|
254
|
-
* A first-class axis rather than a corollary of `mcp`, because the two answer different questions: `mcp`
|
|
255
|
-
* says which TOOLS reach the model's context, this says which ways of EXECUTING the daemon can stand
|
|
256
|
-
* behind for this runtime, with the same guard, secret and persona seams the shell gets. A runtime that
|
|
257
|
-
* cannot host a backend simply never shows it, and the persona switch for it (PersonaPowersSchema.code)
|
|
258
|
-
* then has nothing to grant there. */
|
|
259
|
-
readonly execution: readonly ExecutionBackend[];
|
|
260
|
-
// Reasoning-effort selection is forwarded to the model.
|
|
261
|
-
readonly effort: boolean;
|
|
262
|
-
/* The runtime can serve a turn at fast speed when asked (AgentTurn.fast). A statement about the LOOP, not
|
|
263
|
-
* about the route: the Claude Code loop knows how to ask for it, which is why every provider this record
|
|
264
|
-
* hands the loop to reads true here, including the ones served through the translator, whose turns the
|
|
265
|
-
* harness will then refuse fast mode for because a translator endpoint is not first-party. That second
|
|
266
|
-
* question is answered where the endpoint is decided (planHarnessTurn), because it is a fact about the
|
|
267
|
-
* CREDENTIAL rather than about the runtime, and this record is a pure function of (provider, harness). */
|
|
268
|
-
readonly fastMode: boolean;
|
|
269
|
-
// How an isolated conversation's worktree is enforced. "namespace" = the worktree IS /work inside the turn's
|
|
270
|
-
// mount namespace (with the tool-input rewrite as the fallback when the container can't build one); "cwd" =
|
|
271
|
-
// the turn is merely cwd'd into the worktree, so an absolute /work path still reaches the shared checkout,
|
|
272
|
-
// which is why those turns are told where their tree is (turn-preamble.ts).
|
|
273
|
-
readonly isolation: "namespace" | "cwd";
|
|
274
|
-
// Publishes its slash commands (`commands` frames) for the composer's `/` popover.
|
|
275
|
-
readonly commands: boolean;
|
|
276
|
-
// Runs its shell in a tmux session the terminal panel can attach to (`terminal` frames).
|
|
277
|
-
readonly terminals: boolean;
|
|
278
|
-
// Fails with the coded frames the daemon's auto-resume keys off (rate_limit, provider-outage), so a turn the
|
|
279
|
-
// provider killed is re-run once the breaker says the provider is back (turn-resume.ts).
|
|
280
|
-
readonly recovery: boolean;
|
|
281
|
-
/* HOW MUCH OF ITS STANDING INSTRUCTIONS THIS RUNTIME WILL TAKE FROM US, the axis behind the sandbox's
|
|
282
|
-
* system-prompt setting (SandboxSettings.systemPromptMode) and the persona's own override.
|
|
283
|
-
*
|
|
284
|
-
* It exists because that setting was silently a Claude Code setting. The composer offers Codex, Grok and
|
|
285
|
-
* Gemini on their own runtimes, and a turn on any of them ignored the prompt the owner had written without
|
|
286
|
-
* saying so anywhere, the one failure mode a settings page cannot recover from, because nothing on screen
|
|
287
|
-
* is wrong. Naming it here means every surface reads the same answer and the daemon composes to it
|
|
288
|
-
* (agent/system-prompt.ts), rather than each learning the exception separately.
|
|
289
|
-
*
|
|
290
|
-
* "replace", the whole base prompt can be swapped for the owner's text, and extra guidance appended on
|
|
291
|
-
* top of whichever base is in force. The Claude Code loop (SDK `systemPrompt`) and native
|
|
292
|
-
* Codex (`model_instructions_file` replaces its base; `developer_instructions` adds a
|
|
293
|
-
* developer message, both verified on the wire against codex-cli 0.147).
|
|
294
|
-
* "append" , extra system text only; the runtime's own base prompt stands. OpenCode takes one per
|
|
295
|
-
* message (`system` on the prompt body), and there is no seam for replacing its base.
|
|
296
|
-
* "none" , no system seam at all. What must still reach the model (the persona note) rides the user
|
|
297
|
-
* message instead, which is the door the delegation note already uses.
|
|
298
|
-
*
|
|
299
|
-
* The BASE CHOICE. Intentic's prompt or Claude Code's, is a "replace" runtime's question and, of those,
|
|
300
|
-
* only the Claude Code loop's: Codex's own base describes Codex's own tools, so swapping it for a prompt
|
|
301
|
-
* written about another harness is the owner's deliberate act (their custom text), never ours. */
|
|
302
|
-
readonly instructions: "replace" | "append" | "none";
|
|
303
|
-
/* HOW THIS RUNTIME DISCOVERS THE WORKSPACE'S LOADED SKILLS.
|
|
304
|
-
*
|
|
305
|
-
* "native", the runtime scans one of the filesystem projections itself: `.agents/skills/` for Codex,
|
|
306
|
-
* `.claude/skills/` for the Claude Code loop. Its own loader injects the catalogue and reads
|
|
307
|
-
* the matching SKILL.md on demand, so adding our own note would duplicate it.
|
|
308
|
-
* "prompt", the runtime has no loader the daemon can rely on. turn-plan.ts puts the same name,
|
|
309
|
-
* description and absolute SKILL.md path into the opening user-message preamble. This is a
|
|
310
|
-
* separate axis from `instructions`: Pi and ACP take no system prompt at all, while OpenCode
|
|
311
|
-
* and Cursor take an append, but all four still need skill discovery. */
|
|
312
|
-
readonly skillDiscovery: "native" | "prompt";
|
|
313
|
-
/* WHETHER THE OWNER'S COMMAND RULEBOOK REACHES THIS RUNTIME (SandboxSettings.commandRules, decided by
|
|
314
|
-
* guard/actions.ts commandRun, delivered by guard/command-gate.ts).
|
|
315
|
-
*
|
|
316
|
-
* It exists because the rulebook was silently a Claude Code rulebook. The gate is a PreToolUse hook, which
|
|
317
|
-
* is an Agent SDK seam, so an owner who set `files.destructive: hold` was asked on a Claude turn and never
|
|
318
|
-
* on a Codex, Grok, Gemini, Pi or ACP one, with nothing on screen saying so. Same failure mode as the
|
|
319
|
-
* `instructions` axis above, and the same fix: name it once, let every surface read it.
|
|
320
|
-
*
|
|
321
|
-
* "hooks" , the runtime's own pre-execution hook carries the verdict and a HOLD can park the call.
|
|
322
|
-
* The Claude Code loop, whose PreToolUse hook fires even under bypassPermissions.
|
|
323
|
-
* "approval" , the vendor publishes a per-call approval channel the daemon answers from the same
|
|
324
|
-
* rulebook, and a hold parks on a card because the vendor is blocked on the answer
|
|
325
|
-
* (Codex's `item/commandExecution/requestApproval`, ACP's `session/request_permission`).
|
|
326
|
-
* Weaker than "hooks" in one stated way: the vendor decides WHICH calls it asks about, so a
|
|
327
|
-
* class it never raises is a class the rulebook cannot see. What it does raise is judged by
|
|
328
|
-
* the same decide fn.
|
|
329
|
-
* "refuse-only", the same channel, but the vendor puts a CLOCK on the wait, so a hold cannot park and
|
|
330
|
-
* arrives as a refusal instead. OpenCode's turn has an inactivity watchdog that reads a
|
|
331
|
-
* paused approval as a stalled turn; a card there would break the turn rather than gate it.
|
|
332
|
-
* `deny` rules work fully; `hold` rules stop the command and say they could not ask.
|
|
333
|
-
* "none" , the runtime publishes no seam before it runs a command, so no rule can apply. Pi runs its
|
|
334
|
-
* bash in-process with no approval channel at all.
|
|
335
|
-
*
|
|
336
|
-
* The taint floor rides this axis too: a runtime with no consult has no place to apply it, which is why
|
|
337
|
-
* `conversationTainted` must read a "none" runtime as tainted rather than as clean (guard/turn-taint.ts). */
|
|
338
|
-
readonly rulebook: "hooks" | "approval" | "refuse-only" | "none";
|
|
339
|
-
/* WHETHER A STORED CREDENTIAL IS MASKED IN WHAT THIS RUNTIME'S MODEL READS (secrets/secret-registry.ts and
|
|
340
|
-
* the two seams around it).
|
|
341
|
-
*
|
|
342
|
-
* "masked" is the full round trip: every stored value is replaced by its `{{secret:name}}` reference on the
|
|
343
|
-
* way into the model's context, and the same reference resolves back to the value at the two exits that
|
|
344
|
-
* spend it (a shell command, a script). The Claude Code loop, via PostToolUse and PreToolUse.
|
|
345
|
-
*
|
|
346
|
-
* "none" is a STRUCTURAL limit, not an unfinished wire, and it is the reason this axis is honest rather
|
|
347
|
-
* than aspirational. On every other runtime the tool runs inside the VENDOR'S own loop: the model has read
|
|
348
|
-
* the result before the daemon sees any frame about it, so there is no seam left to rewrite. A PostToolUse
|
|
349
|
-
* hook is the only thing that can edit what a model reads, and only the Claude Code loop has one. Nothing
|
|
350
|
-
* about wiring more transports changes that, which is why the answer here is a disclosure and the real fix
|
|
351
|
-
* is to stop putting credentials where a vendor's tool can read them at all.
|
|
352
|
-
*
|
|
353
|
-
* Read by limitationsOf, and by agent/system-prompt.ts, which must not teach the reference language to a
|
|
354
|
-
* runtime that has no exit for it. */
|
|
355
|
-
readonly secrets: "masked" | "none";
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// The Claude Code Agent SDK loop, the ceiling every other runtime is measured against, and the only one that
|
|
359
|
-
// owns the whole request: permission callbacks, the ask tool, plugins, hooks, and the spawn seam a mount
|
|
360
|
-
// namespace needs.
|
|
361
|
-
const CLAUDE_CODE: AgentCapabilities = {
|
|
362
|
-
runtime: "claude-code",
|
|
363
|
-
steering: true,
|
|
364
|
-
permissions: "modes",
|
|
365
|
-
questions: true,
|
|
366
|
-
mcp: "full",
|
|
367
|
-
// The one loop with a seam the daemon can put its own backend through, so it hosts the JS backend beside
|
|
368
|
-
// its Bash. Every other runtime below hosts only its own shell.
|
|
369
|
-
execution: ["shell", "js"],
|
|
370
|
-
effort: true,
|
|
371
|
-
fastMode: true,
|
|
372
|
-
isolation: "namespace",
|
|
373
|
-
commands: true,
|
|
374
|
-
terminals: true,
|
|
375
|
-
recovery: true,
|
|
376
|
-
instructions: "replace",
|
|
377
|
-
skillDiscovery: "native",
|
|
378
|
-
// The only runtime with a pre-execution hook of its own, which is why it is the only one where a HOLD can
|
|
379
|
-
// park the call and wait for a card rather than having to refuse it.
|
|
380
|
-
rulebook: "hooks",
|
|
381
|
-
secrets: "masked",
|
|
137
|
+
// Whether the harness axis is a real choice for this provider, or a switch whose two positions run the same
|
|
138
|
+
// loop. Derived, so a provider that gains a native runtime tomorrow gains the switch with it, and one that
|
|
139
|
+
// never had one never shows a control that does nothing.
|
|
140
|
+
export const harnessChoosable = (provider: AgentProvider): boolean => {
|
|
141
|
+
const spec = providerSpec(provider);
|
|
142
|
+
return spec !== undefined && spec.runtimes.native.runtime !== spec.runtimes.claudeCode.runtime;
|
|
382
143
|
};
|
|
383
144
|
|
|
384
|
-
/*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
145
|
+
/* THE PAIR → ITS RECORD. A native provider answers its spec row's two runtimes. An `endpoint/<id>` provider is
|
|
146
|
+
* a model API the user configured, driven BY the Claude Code loop on either harness, so it gets that loop's
|
|
147
|
+
* full ceiling, which is the entire point of routing a model through it rather than adopting a second runtime.
|
|
148
|
+
* The reserved `pi` id is the Pi coding agent on its own RPC runtime (harness doesn't apply. Pi is its own
|
|
149
|
+
* loop, like ACP). Any other id that names no native provider is an installed `agent`-kind capability, served
|
|
150
|
+
* over ACP.
|
|
389
151
|
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
|
|
394
|
-
runtime: "codex",
|
|
395
|
-
steering: true,
|
|
396
|
-
permissions: "plan",
|
|
397
|
-
questions: true,
|
|
398
|
-
mcp: "browser",
|
|
399
|
-
execution: ["shell"],
|
|
400
|
-
effort: true,
|
|
401
|
-
fastMode: false,
|
|
402
|
-
isolation: "namespace",
|
|
403
|
-
commands: true,
|
|
404
|
-
terminals: false,
|
|
405
|
-
recovery: false,
|
|
406
|
-
/* Both halves, through the per-thread `config` block the adapter already sends: `model_instructions_file`
|
|
407
|
-
* takes the place of Codex's own base prompt, `developer_instructions` arrives as an extra developer
|
|
408
|
-
* message ahead of its skills and team blocks. Verified against codex-cli 0.147 by reading what actually
|
|
409
|
-
* reached the wire, the keys are undocumented, and a strings dump proves only that they parse. */
|
|
410
|
-
instructions: "replace",
|
|
411
|
-
skillDiscovery: "native",
|
|
412
|
-
/* App-server publishes `item/commandExecution/requestApproval`, whose params carry the command text, and
|
|
413
|
-
* takes `accept`/`decline` back (codex-cli 0.147's own generated JSON Schema, read with
|
|
414
|
-
* `codex app-server generate-json-schema`). The daemon only asks Codex to raise those requests when the
|
|
415
|
-
* owner has written command rules, so an unconfigured workspace keeps `approvalPolicy: "never"` and pays
|
|
416
|
-
* nothing (codex/codex-agent.ts threadOptions). */
|
|
417
|
-
rulebook: "approval",
|
|
418
|
-
secrets: "none",
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
// OpenCode (the Grok runtime): its own agentic loop, its own tools, allow-all permissions. It takes a model id,
|
|
422
|
-
// a prompt and one system message of ours, no effort scale, no tools of ours, no command list.
|
|
423
|
-
const OPENCODE: AgentCapabilities = {
|
|
424
|
-
runtime: "opencode",
|
|
425
|
-
steering: false,
|
|
426
|
-
permissions: "plan",
|
|
427
|
-
questions: false,
|
|
428
|
-
mcp: "none",
|
|
429
|
-
execution: ["shell"],
|
|
430
|
-
effort: false,
|
|
431
|
-
fastMode: false,
|
|
432
|
-
isolation: "cwd",
|
|
433
|
-
commands: false,
|
|
434
|
-
terminals: false,
|
|
435
|
-
recovery: false,
|
|
436
|
-
// `system` on the prompt body, per message. It ADDS to OpenCode's own prompt, there is no seam for
|
|
437
|
-
// replacing that, so a custom prompt lands here as extra instructions, and the settings page says so
|
|
438
|
-
// rather than letting "replaces everything" quietly mean something else on two providers.
|
|
439
|
-
instructions: "append",
|
|
440
|
-
skillDiscovery: "prompt",
|
|
441
|
-
/* OpenCode asks over its own permission channel (`permission.updated`, replied on
|
|
442
|
-
* `/session/{id}/permissions/{permissionID}`, vocabulary once/always/reject), and the daemon judges what it
|
|
443
|
-
* raises with the same decide fn every other runtime uses.
|
|
444
|
-
*
|
|
445
|
-
* REFUSE-ONLY because of its watchdog, not because of its protocol. A Grok/Gemini turn is aborted after two
|
|
446
|
-
* minutes without an event for its session (grok/grok-agent.ts GROK_INACTIVITY_MS), and a permission paused
|
|
447
|
-
* on a person is exactly that silence, so a parked card would turn "ask me" into a broken turn. A `deny`
|
|
448
|
-
* rule is enforced in full; a `hold` stops the command and tells the agent it could not be asked about. */
|
|
449
|
-
rulebook: "refuse-only",
|
|
450
|
-
secrets: "none",
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
/* The same OpenCode loop, serving Gemini instead of xAI, identical abilities, which is the point of giving it
|
|
454
|
-
* its own row rather than its own record shape.
|
|
455
|
-
*
|
|
456
|
-
* It exists because the alternative was Gemini's ONLY route being the Claude Code loop, and that loop announces
|
|
457
|
-
* itself to whatever it is pointed at: the CLI prepends its own "You are a Claude agent, built on Anthropic's
|
|
458
|
-
* Claude Agent SDK." to every request, baked into the binary with no option to suppress it. Google's Antigravity
|
|
459
|
-
* channel matches that exact sentence and refuses the request, reported as a quota error, which sent the
|
|
460
|
-
* translator walking all 31 connected accounts looking for one with room, ~60s per attempt, none of which could
|
|
461
|
-
* ever have answered. Under this runtime the request carries OpenCode's own prompt, so the turn is simply not
|
|
462
|
-
* Claude Code traffic and the block has nothing to match.
|
|
463
|
-
*
|
|
464
|
-
* The credential is unchanged: both harnesses reach Google through the translator and the same auth files. Only
|
|
465
|
-
* the loop around the model differs. */
|
|
466
|
-
const OPENCODE_GEMINI: AgentCapabilities = {
|
|
467
|
-
...OPENCODE,
|
|
468
|
-
runtime: "opencode-gemini",
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
// Any agent speaking the Agent Client Protocol: a documented floor rather than the native ceiling. It publishes
|
|
472
|
-
// commands, runs its terminals in the conversation's tmux session, and takes our http MCP tools when it says it
|
|
473
|
-
// can, but it owns its own model, effort and permission posture.
|
|
474
|
-
const ACP: AgentCapabilities = {
|
|
475
|
-
runtime: "acp",
|
|
476
|
-
steering: false,
|
|
477
|
-
permissions: "plan",
|
|
478
|
-
questions: false,
|
|
479
|
-
mcp: "http",
|
|
480
|
-
execution: ["shell"],
|
|
481
|
-
effort: false,
|
|
482
|
-
fastMode: false,
|
|
483
|
-
isolation: "cwd",
|
|
484
|
-
commands: true,
|
|
485
|
-
terminals: true,
|
|
486
|
-
recovery: false,
|
|
487
|
-
// ACP's `session/new` and `session/prompt` carry no system field: the agent owns its own instructions the
|
|
488
|
-
// same way it owns its model and its permission posture. The persona note takes the user message instead.
|
|
489
|
-
instructions: "none",
|
|
490
|
-
skillDiscovery: "prompt",
|
|
491
|
-
/* `session/request_permission` is in the protocol floor, so every conforming agent has the channel and the
|
|
492
|
-
* daemon answers it from the rulebook (acp/acp-permissions.ts). The caveat the "approval" value already
|
|
493
|
-
* carries is at its widest here: WHICH calls an agent asks about is entirely the agent's choice, and one
|
|
494
|
-
* that never asks is one no rule can reach. */
|
|
495
|
-
rulebook: "approval",
|
|
496
|
-
secrets: "none",
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
/* THE PI CAPABILITY ID IS RESERVED, the same way the six native ids are: an `agent`-kind capability installed
|
|
500
|
-
* under it is served over Pi's own RPC protocol rather than ACP. Pi closed ACP support deliberately (its RPC
|
|
501
|
-
* mode is the embedding surface), and the two want different records. A bare id rather than a namespace like
|
|
502
|
-
* `endpoint/`, because there is exactly one Pi runtime to name; capabilitiesOf still answers from the id alone,
|
|
503
|
-
* which is what keeps it a pure function of (provider, harness). */
|
|
504
|
-
export const PI_PROVIDER = "pi";
|
|
505
|
-
|
|
506
|
-
// Pi driven over its RPC mode (`pi --mode rpc`, strict-LF JSONL over stdio): above the ACP floor and below the
|
|
507
|
-
// Claude Code ceiling. Its `steer` command is real mid-turn injection; `set_thinking_level` takes the effort
|
|
508
|
-
// tiers; `get_commands` publishes its extension/skill commands. It has no MCP seam (Pi's own extensions are its
|
|
509
|
-
// tool surface), no approval channel (plan is the shared two-phase emulation), and runs bash in-process, no
|
|
510
|
-
// tmux session for the terminal panel to attach to.
|
|
511
|
-
const PI: AgentCapabilities = {
|
|
512
|
-
runtime: "pi",
|
|
513
|
-
steering: true,
|
|
514
|
-
permissions: "plan",
|
|
515
|
-
questions: false,
|
|
516
|
-
mcp: "none",
|
|
517
|
-
execution: ["shell"],
|
|
518
|
-
effort: true,
|
|
519
|
-
fastMode: false,
|
|
520
|
-
isolation: "cwd",
|
|
521
|
-
commands: true,
|
|
522
|
-
terminals: false,
|
|
523
|
-
recovery: false,
|
|
524
|
-
// Pi's RPC opens a session with a prompt and steers it; nothing in that protocol sets standing
|
|
525
|
-
// instructions, so like ACP it hears the persona note through the user message.
|
|
526
|
-
instructions: "none",
|
|
527
|
-
skillDiscovery: "prompt",
|
|
528
|
-
/* THE ONE RUNTIME WITH NO SEAM AT ALL. Pi runs bash in-process and its RPC publishes no approval request,
|
|
529
|
-
* so there is nothing to consult before a command runs and no rule the owner writes can apply here. Said
|
|
530
|
-
* out loud rather than left to be discovered: limitationsOf renders it, and the taint floor treats a "none"
|
|
531
|
-
* runtime as permanently tainted, because a bit nobody can act on is worse than no bit. */
|
|
532
|
-
rulebook: "none",
|
|
533
|
-
secrets: "none",
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
/* CURSOR'S OWN AGENT RUNTIME, driven through `@cursor/sdk`, the SDK Anysphere publishes, in this daemon's own
|
|
537
|
-
* process. The second-richest row in this file after the Claude Code loop, and the reason is the SDK rather
|
|
538
|
-
* than the vendor: it is an EMBEDDING surface, not a CLI wrapped in a pipe, so most of the seams the other
|
|
539
|
-
* foreign runtimes lack are simply function arguments here.
|
|
540
|
-
*
|
|
541
|
-
* WHY NOT THROUGH OPENCODE, which is already in this image and already serves two providers. Every Cursor
|
|
542
|
-
* bridge for OpenCode is a community reverse-engineering of Cursor's private agent RPC or a localhost shim
|
|
543
|
-
* around its CLI, and the OPENCODE record above is the weakest in this file. Routing Cursor through it would
|
|
544
|
-
* have capped a first-party SDK at Grok's ceiling and made the row depend on a third party's spare time.
|
|
545
|
-
*
|
|
546
|
-
* WHY THE HARNESS AXIS DOESN'T APPLY, the same way it doesn't for Gemini, and for the mirror-image reason.
|
|
547
|
-
* Gemini has no Claude Code route because Google refuses that traffic; Cursor has none because there is no
|
|
548
|
-
* translator route at all, CLIProxyAPI does not serve Cursor as a provider (asked for repeatedly upstream and
|
|
549
|
-
* closed as not planned), and Cursor publishes no OpenAI-compatible endpoint on a subscription. The SDK IS the
|
|
550
|
-
* only door, so `capabilitiesOf` answers this record whatever harness the client sent.
|
|
551
|
-
*
|
|
552
|
-
* The three axes below that read weaker than they could are deliberate, not unfinished: see the notes on each. */
|
|
553
|
-
const CURSOR: AgentCapabilities = {
|
|
554
|
-
runtime: "cursor",
|
|
555
|
-
// The SDK's Run can be cancelled but not written to mid-flight: a second `send` on a busy agent is an
|
|
556
|
-
// AgentBusyError, not an injection. So the steering queue has nowhere to go and the composer hides it.
|
|
557
|
-
steering: false,
|
|
558
|
-
/* Cursor's OWN plan mode (`mode: "agent" | "plan"`), not this repo's two-phase emulation, which is the
|
|
559
|
-
* better version of the same bargain: the model is put in a read-only posture by the vendor rather than
|
|
560
|
-
* being asked to behave.
|
|
561
|
-
*
|
|
562
|
-
* Not "modes", and that is the honest half. The hook seam below can gate shell, MCP, file reads and file
|
|
563
|
-
* edits, which is most of the tool surface but not all of it, and a per-tool posture with a silent gap in
|
|
564
|
-
* it is worse than one that says where it stops. */
|
|
565
|
-
permissions: "plan",
|
|
566
|
-
/* TRUE BECAUSE WE SUPPLY THE TOOL, not because Cursor's own askQuestion is wired. That one is put in
|
|
567
|
-
* `disallowedTools`: in a headless run it has been reported to answer itself with a fabricated "Questions
|
|
568
|
-
* skipped by the user", which is the single worst failure shape available here, an agent acting on consent
|
|
569
|
-
* nobody gave. The ask tool the daemon registers through `customTools` runs in this process, parks on a
|
|
570
|
-
* real card, and cannot invent an answer because it is the thing that receives one. */
|
|
571
|
-
questions: true,
|
|
572
|
-
// stdio + http/sse MCP servers, plus host callbacks through `customTools` (which is where the browser stack
|
|
573
|
-
// and the in-process SDK servers land). Everything but a Claude Code plugin checkout, see the axis note.
|
|
574
|
-
mcp: "tools",
|
|
575
|
-
execution: ["shell"],
|
|
576
|
-
/* Cursor publishes effort as MODEL PARAMETERS rather than as one scale (`ModelListItem.parameters` /
|
|
577
|
-
* `variants` → `ModelSelection.params`), so the shared tiers are mapped onto whatever the selected model
|
|
578
|
-
* declares, and a model that declares none simply offers no control. True here because the axis is
|
|
579
|
-
* forwardable at all; which tiers exist is the live catalog's answer, not this record's. */
|
|
580
|
-
effort: true,
|
|
581
|
-
fastMode: false,
|
|
582
|
-
/* "cwd", and this is the one place the SDK's in-process design costs something. A namespace is built around
|
|
583
|
-
* a CHILD the daemon spawns (that is how the Claude Code loop and Codex app-server get theirs); Cursor's
|
|
584
|
-
* loop runs inside the daemon, whose own /work must stay the shared checkout, so an isolated conversation
|
|
585
|
-
* gets its worktree by working directory and the turn is told where its tree is (turn-preamble.ts). */
|
|
586
|
-
isolation: "cwd",
|
|
587
|
-
// Cursor's commands are files on disk (`.cursor/commands`), which the SDK loads but does not publish back,
|
|
588
|
-
// so there is no list to hand the `/` popover.
|
|
589
|
-
commands: false,
|
|
590
|
-
// The SDK runs its shell in-process; there is no tmux session for the terminal panel to attach to.
|
|
591
|
-
terminals: false,
|
|
592
|
-
// The SDK throws typed errors (RateLimitError and friends) rather than dissolving a refusal into prose, so
|
|
593
|
-
// the adapter can file the coded frames auto-resume keys off.
|
|
594
|
-
recovery: true,
|
|
595
|
-
/* "append", the OpenCode answer, reached by a completely different road. There is no system-prompt argument
|
|
596
|
-
* on `Agent.create`; what there is, is the `beforeSubmitPrompt` hook, whose reply carries
|
|
597
|
-
* `additional_context` that is folded into the request. So the owner's prompt and the persona note DO reach
|
|
598
|
-
* the model, on top of Cursor's own base prompt, and nothing can replace that base. */
|
|
599
|
-
instructions: "append",
|
|
600
|
-
skillDiscovery: "prompt",
|
|
601
|
-
/* THE FULL HOOK TIER, the only foreign runtime that reaches it. Cursor reads `.cursor/hooks.json` in its
|
|
602
|
-
* local runtime, and `beforeShellExecution` answers with `allow` / `deny` / `ask` plus the messages that
|
|
603
|
-
* explain it, with `failClosed` available so a crashed gate blocks instead of waving the command through.
|
|
604
|
-
*
|
|
605
|
-
* What earns "hooks" rather than "approval" is that a HOLD can genuinely park: the hook is a process the
|
|
606
|
-
* daemon wrote, so it blocks on the card and the vendor is simply waiting on a script, exactly the shape
|
|
607
|
-
* that makes the Claude Code loop's PreToolUse hook able to stop and ask. The vendor never decides which
|
|
608
|
-
* calls to raise, either, which is the caveat the "approval" tier carries and this one does not. */
|
|
609
|
-
rulebook: "hooks",
|
|
610
|
-
/* "none", and structurally so, like every other foreign runtime. Masking needs a seam that rewrites what
|
|
611
|
-
* the model READS after a tool ran; Cursor's `afterShellExecution` fires with the output but its reply is
|
|
612
|
-
* discarded upstream, and `beforeReadFile` sees the content only to allow or deny it. Both are gates, not
|
|
613
|
-
* filters, so there is nothing here to substitute a reference back into. */
|
|
614
|
-
secrets: "none",
|
|
615
|
-
};
|
|
616
|
-
|
|
617
|
-
// The pair → its record. An `endpoint/<id>` provider is a model API the user configured, driven BY the Claude
|
|
618
|
-
// Code loop on either harness, so it gets that loop's full ceiling, which is the entire point of routing a
|
|
619
|
-
// model through it rather than adopting a second runtime. The reserved `pi` id is the Pi coding agent on its
|
|
620
|
-
// own RPC runtime (harness doesn't apply. Pi is its own loop, like ACP). Any other id that names no native
|
|
621
|
-
// provider is an installed `agent`-kind capability, served over ACP.
|
|
152
|
+
* The three providers whose spec names ONE record on both harnesses are answering the harness with a fact
|
|
153
|
+
* rather than a preference, and each has its own reason, stated on its row: Kimi, Meta and Z.ai have no native
|
|
154
|
+
* runtime at all; Google refuses Claude Code's traffic; Cursor has no route but its own SDK. Reading it off the
|
|
155
|
+
* table is what makes those structural instead of a rule each surface has to remember. */
|
|
622
156
|
export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness): AgentCapabilities => {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (provider === "grok") {
|
|
627
|
-
return harness === "claude-code" ? CLAUDE_CODE : OPENCODE;
|
|
628
|
-
}
|
|
629
|
-
/* GEMINI IGNORES THE HARNESS, and it is the only routed provider that does. The Claude Code loop announces
|
|
630
|
-
* itself in every request it sends and Google refuses on that announcement (see OPENCODE_GEMINI), so
|
|
631
|
-
* "Gemini under Claude Code" was never a slower or poorer option, it was one that could not complete a
|
|
632
|
-
* single turn, on any of the connected accounts, ever.
|
|
633
|
-
*
|
|
634
|
-
* Answering OPENCODE_GEMINI whatever the caller asked for is what makes that structural rather than a rule
|
|
635
|
-
* each surface has to remember. Everything downstream reads the runtime off this record, the adapter that
|
|
636
|
-
* serves a turn, the transcript store, the quick helper's choice of loop, so there is exactly one place
|
|
637
|
-
* where Gemini's loop is decided, and no way left to route Claude Code traffic at Google by asking for it. */
|
|
638
|
-
if (provider === "gemini") {
|
|
639
|
-
return OPENCODE_GEMINI;
|
|
640
|
-
}
|
|
641
|
-
// Cursor ignores the harness for the mirror of Gemini's reason: there is no route to it but its own SDK. No
|
|
642
|
-
// translator serves Cursor and Cursor publishes no model endpoint on a subscription, so "Cursor under Claude
|
|
643
|
-
// Code" names a road that does not exist. Answering this record whatever was asked for is what keeps that a
|
|
644
|
-
// fact of the catalog rather than a rule each surface has to remember.
|
|
645
|
-
if (provider === "cursor") {
|
|
646
|
-
return CURSOR;
|
|
157
|
+
const spec = providerSpec(provider);
|
|
158
|
+
if (spec !== undefined) {
|
|
159
|
+
return harness === "claude-code" ? spec.runtimes.claudeCode : spec.runtimes.native;
|
|
647
160
|
}
|
|
648
161
|
if (isEndpointProvider(provider)) {
|
|
649
162
|
return CLAUDE_CODE;
|
|
@@ -651,9 +164,16 @@ export const capabilitiesOf = (provider: AgentProvider, harness: AgentHarness):
|
|
|
651
164
|
if (provider === PI_PROVIDER) {
|
|
652
165
|
return PI;
|
|
653
166
|
}
|
|
654
|
-
return
|
|
167
|
+
return ACP;
|
|
655
168
|
};
|
|
656
169
|
|
|
170
|
+
/* THE PI CAPABILITY ID IS RESERVED, the same way the native ids are: an `agent`-kind capability installed
|
|
171
|
+
* under it is served over Pi's own RPC protocol rather than ACP. Pi closed ACP support deliberately (its RPC
|
|
172
|
+
* mode is the embedding surface), and the two want different records. A bare id rather than a namespace like
|
|
173
|
+
* `endpoint/`, because there is exactly one Pi runtime to name; capabilitiesOf still answers from the id alone,
|
|
174
|
+
* which is what keeps it a pure function of (provider, harness). */
|
|
175
|
+
export const PI_PROVIDER = "pi";
|
|
176
|
+
|
|
657
177
|
// Which permission modes a runtime can actually be put in. Under "plan" every other mode collapses onto the
|
|
658
178
|
// autonomous posture the runtime already runs, so offering them would be offering four names for two behaviours.
|
|
659
179
|
export const modesFor = (capabilities: AgentCapabilities): readonly PermissionMode[] =>
|
|
@@ -694,9 +214,7 @@ export const limitationsOf = (capabilities: AgentCapabilities): string[] => [
|
|
|
694
214
|
...(capabilities.instructions === "append" ? ["system prompt appended, not replaced"] : []),
|
|
695
215
|
...(capabilities.instructions === "none" ? ["system prompt not applied"] : []),
|
|
696
216
|
...(capabilities.rulebook === "approval" ? ["command rules apply only to calls this agent raises"] : []),
|
|
697
|
-
...(capabilities.rulebook === "refuse-only"
|
|
698
|
-
? ["command rules can refuse but not hold"]
|
|
699
|
-
: []),
|
|
217
|
+
...(capabilities.rulebook === "refuse-only" ? ["command rules can refuse but not hold"] : []),
|
|
700
218
|
...(capabilities.rulebook === "none" ? ["command rules not applied"] : []),
|
|
701
219
|
...(capabilities.secrets === "none" ? ["secrets reach the model unmasked"] : []),
|
|
702
220
|
];
|
|
@@ -716,14 +234,16 @@ export const CLAUDE_SEED_MODELS: readonly Model[] = [
|
|
|
716
234
|
|
|
717
235
|
// The STATIC floor of the model catalog, harness-independent: every provider's real list is the daemon's live
|
|
718
236
|
// catalog (/providers/{provider}/models, discovery with a persisted/seed floor, never empty),
|
|
719
|
-
// which consumers layer on top.
|
|
720
|
-
// under the Claude Code harness
|
|
721
|
-
//
|
|
237
|
+
// which consumers layer on top. Every provider but Claude is empty here (nothing sensible to offer before the
|
|
238
|
+
// live load, and under the Claude Code harness the routed ones serve the SAME subscription model ids as their
|
|
239
|
+
// native catalog, so the harness no longer changes the list).
|
|
722
240
|
export const modelsFor = (provider: AgentProvider): CatalogOption[] => {
|
|
723
241
|
if (provider === "claude") {
|
|
724
242
|
return CLAUDE_SEED_MODELS.map((model) => ({ label: model.label, value: model.id }));
|
|
725
243
|
}
|
|
726
|
-
//
|
|
244
|
+
// Every other native provider (live catalog only) and ACP providers (the agent owns its model): nothing
|
|
245
|
+
// static. A keyed provider's floor is the daemon's, not the browser's: it is discovered from the vendor's
|
|
246
|
+
// own /models with a seed behind it, and duplicating that seed here would be a second list to keep right.
|
|
727
247
|
return [];
|
|
728
248
|
};
|
|
729
249
|
|
|
@@ -759,8 +279,9 @@ export const sendableEffort = (effort: string | undefined, thinking: boolean | u
|
|
|
759
279
|
*
|
|
760
280
|
* - the RUNTIME has to know how to ask (capabilities.fastMode). Only the Claude Code loop does.
|
|
761
281
|
* - the ROUTE has to be first-party. Every non-Claude provider the Claude Code loop serves is served through
|
|
762
|
-
* the sandbox's translator, and the harness refuses fast mode on a
|
|
763
|
-
*
|
|
282
|
+
* the sandbox's translator or pointed at the vendor's own endpoint, and the harness refuses fast mode on a
|
|
283
|
+
* non-Anthropic endpoint ("not_first_party"), so a `grok` turn on the claude-code harness reads true on the
|
|
284
|
+
* capability and still cannot go fast.
|
|
764
285
|
* - the MODEL has to publish it, which is the `fast` badge Anthropic's own catalog reports per model
|
|
765
286
|
* (claude-models.ts maps supportsFastMode onto it). Curating a list of ids here instead is what this repo
|
|
766
287
|
* deliberately does not do, a model that gains or loses fast mode moves the badge, and this follows.
|