@intentic/sandbox-contract 1.333.1 → 1.335.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 +8 -3
- package/dist/contracts/accounts.contract.d.ts +11 -11
- package/dist/contracts/activity.contract.d.ts +1 -0
- package/dist/contracts/activity.contract.d.ts.map +1 -1
- package/dist/contracts/activity.contract.js +1 -1
- package/dist/contracts/activity.contract.js.map +1 -1
- package/dist/contracts/agent.contract.d.ts +102 -18
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +163 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +10 -1
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/audit.contract.d.ts +84 -0
- package/dist/contracts/audit.contract.d.ts.map +1 -0
- package/dist/contracts/audit.contract.js +43 -0
- package/dist/contracts/audit.contract.js.map +1 -0
- package/dist/contracts/automations.contract.d.ts +15 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.d.ts +10 -0
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/compliance.contract.d.ts +163 -0
- package/dist/contracts/compliance.contract.d.ts.map +1 -0
- package/dist/contracts/compliance.contract.js +41 -0
- package/dist/contracts/compliance.contract.js.map +1 -0
- package/dist/contracts/device.contract.d.ts +6 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +2 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/loops.contract.d.ts +15 -0
- package/dist/contracts/loops.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.d.ts +49 -0
- package/dist/contracts/needs.contract.d.ts.map +1 -1
- package/dist/contracts/offload.contract.d.ts +5 -0
- package/dist/contracts/offload.contract.d.ts.map +1 -1
- package/dist/contracts/personas.contract.d.ts +10 -0
- package/dist/contracts/personas.contract.d.ts.map +1 -1
- package/dist/contracts/providers.contract.d.ts +2 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/providers.contract.js +4 -0
- package/dist/contracts/providers.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts +42 -10
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +17 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +84 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.js +7 -0
- package/dist/contracts/settings.contract.js.map +1 -1
- package/dist/contracts/system.contract.d.ts +16 -2
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/workflows.contract.d.ts +30 -0
- package/dist/contracts/workflows.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +3 -0
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +74 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +1 -0
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/requests.d.ts +45 -0
- package/dist/events/requests.d.ts.map +1 -1
- package/dist/events/system-events.d.ts +12 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +117 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/index.d.ts +828 -73
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/models/agent-catalog.d.ts +18 -3
- package/dist/models/agent-catalog.d.ts.map +1 -1
- package/dist/models/agent-catalog.js +61 -12
- package/dist/models/agent-catalog.js.map +1 -1
- package/dist/models/agent-runtimes.d.ts +5 -2
- package/dist/models/agent-runtimes.d.ts.map +1 -1
- package/dist/models/agent-runtimes.js +73 -40
- package/dist/models/agent-runtimes.js.map +1 -1
- package/dist/models/model-setups.d.ts +5 -0
- package/dist/models/model-setups.d.ts.map +1 -1
- package/dist/models/provider-specs.d.ts +153 -32
- package/dist/models/provider-specs.d.ts.map +1 -1
- package/dist/models/provider-specs.js +97 -11
- package/dist/models/provider-specs.js.map +1 -1
- package/dist/policy/reserved-servers.d.ts.map +1 -1
- package/dist/policy/reserved-servers.js +1 -0
- package/dist/policy/reserved-servers.js.map +1 -1
- package/dist/policy/sandbox-power.d.ts +7 -2
- package/dist/policy/sandbox-power.d.ts.map +1 -1
- package/dist/policy/sandbox-power.js +34 -1
- package/dist/policy/sandbox-power.js.map +1 -1
- package/dist/policy/turned-away.d.ts.map +1 -1
- package/dist/policy/turned-away.js +1 -0
- package/dist/policy/turned-away.js.map +1 -1
- package/dist/protocol/doors/listener-protocol.d.ts +43 -1
- package/dist/protocol/doors/listener-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/listener-protocol.js +10 -0
- package/dist/protocol/doors/listener-protocol.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +92 -0
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-json-routes.js +2 -1
- package/dist/protocol/raw/raw-json-routes.js.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +13 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +3 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/schemas/activity.d.ts +2 -0
- package/dist/schemas/activity.d.ts.map +1 -1
- package/dist/schemas/agent.d.ts +34 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +7 -6
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents-list.d.ts +7 -0
- package/dist/schemas/agents-list.d.ts.map +1 -1
- package/dist/schemas/agents.d.ts +18 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/ai-literacy.d.ts +54 -0
- package/dist/schemas/ai-literacy.d.ts.map +1 -0
- package/dist/schemas/ai-literacy.js +15 -0
- package/dist/schemas/ai-literacy.js.map +1 -0
- package/dist/schemas/audit.d.ts +166 -0
- package/dist/schemas/audit.d.ts.map +1 -0
- package/dist/schemas/audit.js +100 -0
- package/dist/schemas/audit.js.map +1 -0
- package/dist/schemas/automations.d.ts +27 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +11 -0
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/ci.d.ts +10 -0
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/compliance.d.ts +423 -0
- package/dist/schemas/compliance.d.ts.map +1 -0
- package/dist/schemas/compliance.js +135 -0
- package/dist/schemas/compliance.js.map +1 -0
- package/dist/schemas/data-protection.d.ts +28 -0
- package/dist/schemas/data-protection.d.ts.map +1 -0
- package/dist/schemas/data-protection.js +49 -0
- package/dist/schemas/data-protection.js.map +1 -0
- package/dist/schemas/devices.d.ts +12 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/engines.d.ts +22 -1
- package/dist/schemas/engines.d.ts.map +1 -1
- package/dist/schemas/engines.js +1 -1
- package/dist/schemas/engines.js.map +1 -1
- package/dist/schemas/extension-updates.d.ts +4 -0
- package/dist/schemas/extension-updates.d.ts.map +1 -1
- package/dist/schemas/git/git.d.ts +33 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/git/git.js +29 -0
- package/dist/schemas/git/git.js.map +1 -1
- package/dist/schemas/hosts.d.ts +36 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +2 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/intentic-os.d.ts +117 -0
- package/dist/schemas/intentic-os.d.ts.map +1 -0
- package/dist/schemas/intentic-os.js +66 -0
- package/dist/schemas/intentic-os.js.map +1 -0
- package/dist/schemas/loops.d.ts +15 -0
- package/dist/schemas/loops.d.ts.map +1 -1
- package/dist/schemas/members.d.ts +90 -0
- package/dist/schemas/members.d.ts.map +1 -1
- package/dist/schemas/members.js +73 -3
- package/dist/schemas/members.js.map +1 -1
- package/dist/schemas/needs.d.ts +56 -0
- package/dist/schemas/needs.d.ts.map +1 -1
- package/dist/schemas/offload.d.ts +5 -0
- package/dist/schemas/offload.d.ts.map +1 -1
- package/dist/schemas/personas.d.ts +10 -0
- package/dist/schemas/personas.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.d.ts +20 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/provider-oauth.js +1 -1
- package/dist/schemas/providers/provider-oauth.js.map +1 -1
- package/dist/schemas/providers/usage.d.ts +1 -0
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +4 -0
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/settings.d.ts +48 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +14 -0
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +8 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +2 -0
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/workflows.d.ts +50 -0
- package/dist/schemas/workflows.d.ts.map +1 -1
- package/dist/schemas/workspace/workspace-tree.d.ts +4 -0
- package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
- package/dist/schemas/workspace/workspace-tree.js +16 -1
- package/dist/schemas/workspace/workspace-tree.js.map +1 -1
- package/dist/state/definition.d.ts +80 -4
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +2 -0
- package/dist/state/history-state.js.map +1 -1
- package/package.json +5 -5
- package/src/contracts/activity.contract.ts +1 -1
- package/src/contracts/agents.contract.ts +13 -1
- package/src/contracts/audit.contract.ts +48 -0
- package/src/contracts/compliance.contract.ts +57 -0
- package/src/contracts/providers.contract.ts +9 -0
- package/src/contracts/settings.contract.ts +7 -0
- package/src/events/agent-events.ts +4 -0
- package/src/index.ts +11 -0
- package/src/models/agent-catalog.test.ts +317 -53
- package/src/models/agent-catalog.ts +115 -22
- package/src/models/agent-runtimes.ts +273 -75
- package/src/models/capability-ledger.test.ts +12 -1
- package/src/models/provider-specs.test.ts +42 -7
- package/src/models/provider-specs.ts +160 -20
- package/src/policy/reserved-servers.ts +2 -1
- package/src/policy/sandbox-power.ts +67 -8
- package/src/policy/tests/sandbox-power.test.ts +59 -1
- package/src/policy/turned-away.ts +2 -0
- package/src/protocol/doors/listener-protocol.ts +74 -6
- package/src/protocol/raw/raw-json-routes.ts +2 -1
- package/src/protocol/raw/raw-routes.ts +7 -0
- package/src/schemas/activity.ts +2 -2
- package/src/schemas/agent.ts +22 -9
- package/src/schemas/ai-literacy.ts +17 -0
- package/src/schemas/audit.ts +131 -0
- package/src/schemas/automations.ts +13 -0
- package/src/schemas/compliance.ts +195 -0
- package/src/schemas/daemon-health.test.ts +2 -0
- package/src/schemas/data-protection.ts +99 -0
- package/src/schemas/engines.ts +1 -1
- package/src/schemas/git/git.ts +46 -0
- package/src/schemas/hosts.ts +5 -0
- package/src/schemas/intentic-os.test.ts +86 -0
- package/src/schemas/intentic-os.ts +130 -0
- package/src/schemas/members.test.ts +47 -0
- package/src/schemas/members.ts +141 -6
- package/src/schemas/providers/provider-oauth.ts +1 -1
- package/src/schemas/providers/usage.ts +6 -0
- package/src/schemas/settings.ts +32 -0
- package/src/schemas/system.ts +2 -0
- package/src/schemas/workspace/workspace-tree.ts +26 -1
- package/src/state/history-state.ts +7 -0
|
@@ -14,9 +14,18 @@ import {
|
|
|
14
14
|
PROVIDERS,
|
|
15
15
|
sendableEffort,
|
|
16
16
|
sendableThinking,
|
|
17
|
+
termsOf,
|
|
18
|
+
loopsOf,
|
|
19
|
+
harnessChoosable,
|
|
20
|
+
loopOf,
|
|
21
|
+
canonicalHarness,
|
|
22
|
+
loopLabel,
|
|
23
|
+
loopDoor,
|
|
24
|
+
loopDoors,
|
|
25
|
+
accountServesLoop,
|
|
17
26
|
} from "./agent-catalog.js";
|
|
18
27
|
import type { AgentCapabilities } from "./agent-runtimes.js";
|
|
19
|
-
import { type AgentHarness, type AgentProvider, PermissionModeSchema } from "../schemas/agent.js";
|
|
28
|
+
import { type AgentHarness, type AgentProvider, PermissionModeSchema, withRuntimeDefaults } from "../schemas/agent.js";
|
|
20
29
|
|
|
21
30
|
// Every provider × harness pair, generated from the catalog itself, must declare what it can do. A failing pair means a
|
|
22
31
|
// missing row in capabilitiesOf, never a special case in the caller.
|
|
@@ -29,7 +38,9 @@ describe("every provider/harness pair declares what it can do", () => {
|
|
|
29
38
|
it.each(pairs)("$provider on the $harness harness", ({ provider, harness }) => {
|
|
30
39
|
const capabilities = capabilitiesOf(provider, harness);
|
|
31
40
|
|
|
32
|
-
expect(["claude-code", "codex", "cursor", "opencode", "opencode-gemini", "acp", "devin", "pi"]).toContain(
|
|
41
|
+
expect(["claude-code", "codex", "cursor", "opencode", "opencode-gemini", "acp", "devin", "kimi-cli", "grok-build", "pi", "muse"]).toContain(
|
|
42
|
+
capabilities.runtime,
|
|
43
|
+
);
|
|
33
44
|
expect(["modes", "plan"]).toContain(capabilities.permissions);
|
|
34
45
|
expect(["full", "tools", "http", "none"]).toContain(capabilities.mcp);
|
|
35
46
|
expect(capabilities.execution).toContain("shell");
|
|
@@ -39,9 +50,10 @@ describe("every provider/harness pair declares what it can do", () => {
|
|
|
39
50
|
expect(["namespace", "cwd"]).toContain(capabilities.isolation);
|
|
40
51
|
expect(["replace", "append", "none"]).toContain(capabilities.instructions);
|
|
41
52
|
expect(["hooks", "approval", "refuse-only", "none"]).toContain(capabilities.rulebook);
|
|
42
|
-
expect(["masked", "none"]).toContain(capabilities.secrets);
|
|
43
|
-
// Masking
|
|
44
|
-
// legal direction, so this is an implication, not an equality.
|
|
53
|
+
expect(["masked", "shell", "none"]).toContain(capabilities.secrets);
|
|
54
|
+
// Masking everything the model reads needs a hook in front of every result, which only a hooks-tier loop has;
|
|
55
|
+
// hooks without masking is the one legal direction, so this is an implication, not an equality. The shell-only
|
|
56
|
+
// answer needs no such hook: a wrapper around the command masks what it prints.
|
|
45
57
|
if (capabilities.secrets === "masked") {
|
|
46
58
|
expect(capabilities.rulebook).toBe("hooks");
|
|
47
59
|
}
|
|
@@ -57,8 +69,9 @@ test("an unknown provider id is an ACP agent, on either harness", () => {
|
|
|
57
69
|
}
|
|
58
70
|
});
|
|
59
71
|
|
|
60
|
-
// The `pi` id is reserved for the Pi coding agent's own RPC runtime: real mid-turn steering, an effort scale
|
|
61
|
-
// published command list,
|
|
72
|
+
// The `pi` id is reserved for the Pi coding agent's own RPC runtime: real mid-turn steering, an effort scale, a
|
|
73
|
+
// published command list, a process started per turn with the daemon's instructions, skills and namespace, and the
|
|
74
|
+
// daemon's own extension in front of every tool, none of which the plain ACP floor carries.
|
|
62
75
|
describe("the pi provider", () => {
|
|
63
76
|
it("runs the pi runtime on either harness: pi is its own loop", () => {
|
|
64
77
|
for (const harness of HARNESSES) {
|
|
@@ -66,18 +79,49 @@ describe("the pi provider", () => {
|
|
|
66
79
|
}
|
|
67
80
|
});
|
|
68
81
|
|
|
69
|
-
it("sits above the ACP floor: steering, effort
|
|
82
|
+
it("sits above the ACP floor: steering, effort, commands and the terminal panel are real", () => {
|
|
70
83
|
const pi = capabilitiesOf("pi", "native");
|
|
71
84
|
expect(pi.steering).toBe(true);
|
|
72
85
|
expect(pi.effort).toBe(true);
|
|
73
86
|
expect(pi.commands).toBe(true);
|
|
74
|
-
expect(pi.
|
|
75
|
-
expect(pi.terminals).toBe(false);
|
|
87
|
+
expect(pi.terminals).toBe(true);
|
|
76
88
|
const limitations = limitationsOf(pi);
|
|
77
89
|
expect(limitations.length).toBeGreaterThan(0);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
for (const gone of ["no mid-turn steering", "no effort control", "no slash commands", "no terminal panel"]) {
|
|
91
|
+
expect(limitations).not.toContain(gone);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// The extension's `tool_call` and `tool_result` hooks, answered on the turn's own socket (runtimes/pi/pi-hooks.ts):
|
|
96
|
+
// the owner's rules and the turn's tool list before a call runs, every result masked after.
|
|
97
|
+
it("holds every call to the owner's rules and the turn's tool list, and masks every result", () => {
|
|
98
|
+
const pi = capabilitiesOf("pi", "native");
|
|
99
|
+
expect(pi.rulebook).toBe("hooks");
|
|
100
|
+
expect(pi.toolAllowlist).toBe(true);
|
|
101
|
+
expect(pi.secrets).toBe("masked");
|
|
102
|
+
const limitations = limitationsOf(pi);
|
|
103
|
+
for (const gone of ["command rules not applied", "secrets reach the model unmasked", "no tool lists: an automation limited to some tools won't run here"]) {
|
|
104
|
+
expect(limitations).not.toContain(gone);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Each started with the process (runtimes/pi/pi-rpc.ts): the instruction flags, `--skill` paths with Pi's own
|
|
109
|
+
// discovery off, the ask tool and MCP servers through the daemon's extension, and nsenter into the turn's namespace.
|
|
110
|
+
it("takes the turn's instructions, skills, questions, MCP servers, the daemon's own tools and namespace", () => {
|
|
111
|
+
const pi = capabilitiesOf("pi", "native");
|
|
112
|
+
expect(pi.instructions).toBe("replace");
|
|
113
|
+
expect(pi.skillDiscovery).toBe("native");
|
|
114
|
+
expect(pi.questions).toBe(true);
|
|
115
|
+
expect(pi.mcp).toBe("tools");
|
|
116
|
+
expect(pi.execution).toEqual(["shell", "js"]);
|
|
117
|
+
expect(pi.isolation).toBe("namespace");
|
|
118
|
+
expect(pi.recovery).toBe(true);
|
|
119
|
+
const limitations = limitationsOf(pi);
|
|
120
|
+
for (const gone of ["no clarifying questions", "worktree by cwd only", "no auto-resume after outage", "system prompt not applied"]) {
|
|
121
|
+
expect(limitations).not.toContain(gone);
|
|
122
|
+
}
|
|
123
|
+
expect(limitations).toContain("no plugins");
|
|
124
|
+
expect(limitations).not.toContain("no code runs, shell only");
|
|
81
125
|
});
|
|
82
126
|
|
|
83
127
|
it("is an installed capability, so it carries no access requirement to connect", () => {
|
|
@@ -85,10 +129,80 @@ describe("the pi provider", () => {
|
|
|
85
129
|
});
|
|
86
130
|
});
|
|
87
131
|
|
|
88
|
-
|
|
89
|
-
|
|
132
|
+
// Exactly today's provider × loop pairs; a new pair is a row in provider-specs.ts and a line here.
|
|
133
|
+
test("each provider offers exactly its loops, its default first", () => {
|
|
134
|
+
expect(Object.fromEntries(PROVIDERS.map((provider) => [provider.value, loopsOf(provider.value)]))).toEqual({
|
|
135
|
+
claude: ["claude-code"],
|
|
136
|
+
codex: ["codex", "claude-code", "opencode"],
|
|
137
|
+
grok: ["opencode", "claude-code", "codex", "vendor"],
|
|
138
|
+
kimi: ["claude-code", "codex", "opencode", "vendor"],
|
|
139
|
+
gemini: ["opencode"],
|
|
140
|
+
cursor: ["vendor"],
|
|
141
|
+
devin: ["vendor"],
|
|
142
|
+
meta: ["muse", "claude-code"],
|
|
143
|
+
zai: ["claude-code", "codex", "opencode"],
|
|
144
|
+
});
|
|
145
|
+
expect(loopsOf("endpoint/ollama")).toEqual(["claude-code", "codex", "opencode", "muse"]);
|
|
146
|
+
// The trial's one model id is the translator's Anthropic route, and its pool cannot replay Muse's tool history: it
|
|
147
|
+
// stays on the loop it discloses itself under.
|
|
148
|
+
expect(loopsOf("endpoint/free-trial")).toEqual(["claude-code"]);
|
|
149
|
+
expect(loopsOf("pi")).toEqual(["pi"]);
|
|
150
|
+
expect(loopsOf("some-installed-agent")).toEqual(["vendor"]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("every provider with a route a second loop can drive offers a choice of loop", () => {
|
|
154
|
+
expect(PROVIDERS.filter((provider) => harnessChoosable(provider.value)).map((provider) => provider.value)).toEqual(["codex", "grok", "kimi", "meta", "zai"]);
|
|
155
|
+
expect(harnessChoosable("endpoint/ollama")).toBe(true);
|
|
156
|
+
expect(harnessChoosable("endpoint/free-trial")).toBe(false);
|
|
157
|
+
expect(harnessChoosable("some-installed-agent")).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("resolving a harness to the loop that runs", () => {
|
|
161
|
+
it("reads native as the provider's default loop", () => {
|
|
162
|
+
expect(loopOf("codex", "native")).toBe("codex");
|
|
163
|
+
expect(loopOf("grok", "native")).toBe("opencode");
|
|
164
|
+
expect(loopOf("claude", "native")).toBe("claude-code");
|
|
165
|
+
expect(loopOf("cursor", "native")).toBe("vendor");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("keeps an offered loop and runs the default for one the provider lacks", () => {
|
|
169
|
+
expect(loopOf("codex", "claude-code")).toBe("claude-code");
|
|
170
|
+
expect(loopOf("codex", "codex")).toBe("codex");
|
|
171
|
+
expect(loopOf("cursor", "claude-code")).toBe("vendor");
|
|
172
|
+
expect(loopOf("gemini", "claude-code")).toBe("opencode");
|
|
173
|
+
expect(loopOf("claude", "opencode")).toBe("claude-code");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("spells a turn naming its default loop the same as one naming none", () => {
|
|
177
|
+
expect(canonicalHarness("codex", "codex")).toBe("native");
|
|
178
|
+
expect(canonicalHarness("codex", undefined)).toBe("native");
|
|
179
|
+
expect(canonicalHarness("codex", "claude-code")).toBe("claude-code");
|
|
180
|
+
expect(canonicalHarness("cursor", "claude-code")).toBe("native");
|
|
181
|
+
// Typed as the wire carries it, not as a literal the intersection in the return type would narrow.
|
|
182
|
+
const stamped = (agent: AgentProvider, harness: AgentHarness): AgentHarness => withRuntimeDefaults({ agent, harness }).harness;
|
|
183
|
+
expect(stamped("grok", "opencode")).toBe("native");
|
|
184
|
+
expect(stamped("grok", "claude-code")).toBe("claude-code");
|
|
185
|
+
expect(stamped("devin", "claude-code")).toBe("native");
|
|
186
|
+
});
|
|
90
187
|
|
|
91
|
-
|
|
188
|
+
// Only Claude Code turns travel to a runner unasked (runner-scheduler.ts credentialsTravel), and a runner built
|
|
189
|
+
// before the enum widened knows just `native` and `claude-code`: every such turn must be spelled in those.
|
|
190
|
+
it("spells every Claude Code turn in the two values every runner build knows", () => {
|
|
191
|
+
for (const provider of [...PROVIDERS.map((row) => row.value), "endpoint/x", "pi", "some-installed-agent"]) {
|
|
192
|
+
for (const harness of HARNESSES.map((entry) => entry.value)) {
|
|
193
|
+
if (capabilitiesOf(provider, harness).runtime === "claude-code") {
|
|
194
|
+
expect(["native", "claude-code"], `${provider}/${harness}`).toContain(canonicalHarness(provider, harness));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("names each loop, the provider's own agent by the provider", () => {
|
|
201
|
+
expect(loopsOf("codex").map((loop) => loopLabel("codex", loop))).toEqual(["Codex", "Claude Code", "OpenCode"]);
|
|
202
|
+
expect(loopLabel("grok", "opencode")).toBe("OpenCode");
|
|
203
|
+
expect(loopLabel("cursor", "vendor")).toBe("Cursor");
|
|
204
|
+
expect(loopLabel("meta", "muse")).toBe("Muse Code");
|
|
205
|
+
});
|
|
92
206
|
});
|
|
93
207
|
|
|
94
208
|
test("gemini answers with its own runtime whatever harness is asked for", () => {
|
|
@@ -105,41 +219,75 @@ test("codex and grok under the Claude Code harness get the full ceiling, it is t
|
|
|
105
219
|
}
|
|
106
220
|
});
|
|
107
221
|
|
|
222
|
+
// Made up rather than borrowed: real runtimes gain modes as their seams are wired, and a test pinned to one stops testing
|
|
223
|
+
// the plan-only answer the day it does.
|
|
224
|
+
const PLAN_ONLY: AgentCapabilities = { ...capabilitiesOf("claude", "native"), permissions: "plan" };
|
|
225
|
+
|
|
108
226
|
test("a plan-only runtime offers the two postures it has", () => {
|
|
109
|
-
expect(modesFor(
|
|
227
|
+
expect(modesFor(PLAN_ONLY)).toEqual(["plan", "bypassPermissions"]);
|
|
110
228
|
expect(modesFor(capabilitiesOf("claude", "native"))).toEqual(["default", "plan", "bypassPermissions"]);
|
|
229
|
+
// Codex answers its own approval requests per mode, so it offers the ask-each-tool posture too.
|
|
230
|
+
expect(modesFor(capabilitiesOf("codex", "native"))).toEqual(["default", "plan", "bypassPermissions"]);
|
|
111
231
|
});
|
|
112
232
|
|
|
113
233
|
test("a mode the runtime can't hold falls back to the one it runs; one it can holds", () => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
expect(clampMode("default", codex)).toBe("bypassPermissions");
|
|
117
|
-
expect(clampMode("plan", codex)).toBe("plan");
|
|
234
|
+
expect(clampMode("default", PLAN_ONLY)).toBe("bypassPermissions");
|
|
235
|
+
expect(clampMode("plan", PLAN_ONLY)).toBe("plan");
|
|
118
236
|
expect(clampMode("default", capabilitiesOf("claude", "native"))).toBe("default");
|
|
237
|
+
expect(clampMode("default", capabilitiesOf("codex", "native"))).toBe("default");
|
|
119
238
|
});
|
|
120
239
|
|
|
121
240
|
test("the ceiling has nothing to disclose; a floor names what it lacks", () => {
|
|
122
241
|
expect(limitationsOf(capabilitiesOf("claude", "native"))).toEqual([]);
|
|
123
242
|
|
|
124
243
|
const grokCaps = capabilitiesOf("grok", "native");
|
|
125
|
-
|
|
126
|
-
|
|
244
|
+
// OpenCode 2's session API carries steers, per-tool asks, forms, variants, commands and typed failures, the daemon runs
|
|
245
|
+
// its shell (terminal pane, secrets) and serves it the daemon's own tools over the door, and its session rules keep a
|
|
246
|
+
// tool list; what Grok still lacks is the namespace, plugins and masking beyond the shell.
|
|
247
|
+
expect(grokCaps.permissions).toBe("modes");
|
|
248
|
+
expect(grokCaps.steering).toBe(true);
|
|
249
|
+
expect(grokCaps.questions).toBe(true);
|
|
250
|
+
expect(grokCaps.effort).toBe(true);
|
|
251
|
+
expect(grokCaps.commands).toBe(true);
|
|
252
|
+
expect(grokCaps.recovery).toBe(true);
|
|
127
253
|
expect(grokCaps.isolation).toBe("cwd");
|
|
254
|
+
expect(grokCaps.terminals).toBe(true);
|
|
255
|
+
expect(grokCaps.toolAllowlist).toBe(true);
|
|
256
|
+
expect(grokCaps.execution).toEqual(["shell", "js"]);
|
|
128
257
|
expect(limitationsOf(grokCaps).length).toBeGreaterThan(0);
|
|
129
|
-
|
|
130
|
-
expect(grokCaps
|
|
131
|
-
expect(limitationsOf(grokCaps)).toContain("
|
|
132
|
-
|
|
258
|
+
expect(limitationsOf(grokCaps)).not.toContain("no mid-turn steering");
|
|
259
|
+
expect(limitationsOf(grokCaps)).not.toContain("no per-tool approvals");
|
|
260
|
+
expect(limitationsOf(grokCaps)).not.toContain("no auto-resume after outage");
|
|
261
|
+
// The turn's remote servers and the daemon's own, through the door, mounted on the shared OpenCode server per
|
|
262
|
+
// conversation.
|
|
263
|
+
expect(grokCaps.mcp).toBe("tools");
|
|
264
|
+
expect(limitationsOf(grokCaps)).toContain("no plugins");
|
|
265
|
+
expect(limitationsOf(grokCaps)).toEqual(
|
|
266
|
+
expect.not.arrayContaining(["no terminal panel", "no code runs, shell only", "no tool lists: an automation limited to some tools won't run here"]),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// The daemon's own servers reach every ACP agent through the door, over http or the daemon's stdio proxy.
|
|
133
270
|
const acpCaps = capabilitiesOf("some-installed-agent", "native");
|
|
134
|
-
expect(acpCaps.mcp).toBe("
|
|
271
|
+
expect(acpCaps.mcp).toBe("tools");
|
|
272
|
+
expect(acpCaps.execution).toEqual(["shell", "js"]);
|
|
135
273
|
const acp = limitationsOf(acpCaps);
|
|
136
|
-
expect(acp
|
|
274
|
+
expect(acp).toContain("no plugins");
|
|
275
|
+
expect(acp).not.toContain("no code runs, shell only");
|
|
137
276
|
expect(acpCaps.commands).toBe(true);
|
|
138
277
|
expect(acpCaps.terminals).toBe(true);
|
|
139
|
-
|
|
278
|
+
// Per-tool approvals and questions ride the protocol's own seams (request_permission, form elicitation); effort,
|
|
279
|
+
// isolation and recovery stay off, since the record is every agent's and the pooled process is every conversation's.
|
|
280
|
+
expect(acpCaps.permissions).toBe("modes");
|
|
281
|
+
expect(acpCaps.questions).toBe(true);
|
|
282
|
+
expect(acp).not.toContain("no per-tool approvals");
|
|
283
|
+
expect(acp).not.toContain("no clarifying questions");
|
|
284
|
+
expect(acp).toEqual(expect.arrayContaining(["no effort control", "worktree by cwd only", "no auto-resume after outage"]));
|
|
285
|
+
|
|
286
|
+
// Codex takes the daemon's own servers too, over the door, so only plugins are missing.
|
|
140
287
|
const codexCaps = capabilitiesOf("codex", "native");
|
|
141
|
-
expect(codexCaps.mcp).toBe("
|
|
142
|
-
expect(limitationsOf(codexCaps)).toContain("
|
|
288
|
+
expect(codexCaps.mcp).toBe("tools");
|
|
289
|
+
expect(limitationsOf(codexCaps)).toContain("no plugins");
|
|
290
|
+
expect(limitationsOf(codexCaps)).not.toContain("MCP servers only, none of the daemon's own tools, no plugins");
|
|
143
291
|
expect(limitationsOf(codexCaps).length).toBeLessThan(limitationsOf({ ...grokCaps, mcp: "none" }).length + 5);
|
|
144
292
|
});
|
|
145
293
|
|
|
@@ -181,8 +329,12 @@ test("the safety axes disclose the middle answer differently from the floor", ()
|
|
|
181
329
|
expect(claudeCaps.rulebook).toBe("hooks");
|
|
182
330
|
expect(claudeCaps.secrets).toBe("masked");
|
|
183
331
|
|
|
184
|
-
|
|
185
|
-
expect(
|
|
332
|
+
// Codex's own per-thread hooks put every call to the gate before it runs, so its rules hold any call, as Claude's do.
|
|
333
|
+
expect(codexCaps.rulebook).toBe("hooks");
|
|
334
|
+
expect(limitationsOf(codexCaps).join(" ")).not.toContain("command rules");
|
|
335
|
+
// Pi's own extension puts every call to the gate before it runs and masks every result after (runtimes/pi/pi-hooks.ts).
|
|
336
|
+
expect(piCaps.rulebook).toBe("hooks");
|
|
337
|
+
expect(piCaps.secrets).toBe("masked");
|
|
186
338
|
expect(grokCaps.rulebook).toBe("approval");
|
|
187
339
|
// A hold parks on a card now that the turn's watchdog is held while it waits, so nothing says it cannot hold.
|
|
188
340
|
expect(limitationsOf(grokCaps)).toContain("command rules apply only to calls this agent raises");
|
|
@@ -193,31 +345,67 @@ test("the safety axes disclose the middle answer differently from the floor", ()
|
|
|
193
345
|
|
|
194
346
|
expect(limitationsOf(codexCaps).length).toBeGreaterThan(limitationsOf(claudeCaps).length);
|
|
195
347
|
expect(limitationsOf(piCaps).length).toBeGreaterThan(limitationsOf(claudeCaps).length);
|
|
348
|
+
expect(limitationsOf(piCaps).join(" ")).not.toContain("command rules");
|
|
196
349
|
expect(limitationsOf(grokCaps).join(" ")).toBe(limitationsOf(capabilitiesOf("gemini", "native")).join(" "));
|
|
197
350
|
|
|
198
|
-
|
|
351
|
+
// The middle answer: references resolved and output masked in the shell, but not in what the loop's own file tools read.
|
|
352
|
+
// Codex masks through its hooks' wrapper, OpenCode through the daemon's shell script.
|
|
353
|
+
for (const caps of [codexCaps, grokCaps]) {
|
|
354
|
+
expect(caps.secrets).toBe("shell");
|
|
355
|
+
expect(limitationsOf(caps)).toContain("secrets masked in shell output only, not in files the agent reads");
|
|
356
|
+
expect(limitationsOf(caps)).not.toContain("secrets reach the model unmasked");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const provider of ["devin", "some-installed-agent"] as const) {
|
|
199
360
|
expect(capabilitiesOf(provider, "native").secrets).toBe("none");
|
|
200
361
|
}
|
|
201
362
|
});
|
|
202
363
|
|
|
364
|
+
// Cursor's shell runs through the daemon's hook script, which resolves a reference and masks what a command prints; its
|
|
365
|
+
// read tool can only be refused, so the picker says the masking stops at the shell rather than claiming the ceiling's.
|
|
366
|
+
test("secrets on Cursor's runtime are disclosed as reaching the shell only", () => {
|
|
367
|
+
const cursorCaps = capabilitiesOf("cursor", "native");
|
|
368
|
+
expect(cursorCaps.secrets).toBe("shell");
|
|
369
|
+
const limitations = limitationsOf(cursorCaps);
|
|
370
|
+
expect(limitations).toContain("secrets masked in shell output only, not in files the agent reads");
|
|
371
|
+
expect(limitations).not.toContain("secrets reach the model unmasked");
|
|
372
|
+
});
|
|
373
|
+
|
|
203
374
|
test("the instruction axis discloses its two weaker answers, differently", () => {
|
|
204
|
-
|
|
375
|
+
// No runtime appends any more (each found a seam to replace the base), so the append answer is made up.
|
|
376
|
+
const appending: AgentCapabilities = { ...capabilitiesOf("claude", "native"), instructions: "append" };
|
|
205
377
|
const acpCaps = capabilitiesOf("some-installed-agent", "native");
|
|
206
378
|
const codexCaps = capabilitiesOf("codex", "native");
|
|
207
379
|
|
|
208
|
-
expect(grokCaps.instructions).toBe("append");
|
|
209
380
|
expect(acpCaps.instructions).toBe("none");
|
|
210
381
|
expect(codexCaps.instructions).toBe("replace");
|
|
382
|
+
// Grok's owner prompt runs on OpenCode's blank-base agents, so it replaces the base as Codex's does.
|
|
383
|
+
expect(capabilitiesOf("grok", "native").instructions).toBe("replace");
|
|
211
384
|
|
|
212
|
-
expect(limitationsOf(
|
|
213
|
-
expect(limitationsOf(acpCaps)
|
|
385
|
+
expect(limitationsOf(appending)).toEqual(["system prompt appended, not replaced"]);
|
|
386
|
+
expect(limitationsOf(acpCaps)).toContain("system prompt not applied");
|
|
387
|
+
expect(limitationsOf(codexCaps).join(" ")).not.toContain("system prompt");
|
|
214
388
|
});
|
|
215
389
|
|
|
216
|
-
// The backend is one of the daemon's own functions, so it takes a runtime that can call one
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
390
|
+
// The backend is one of the daemon's own functions, so it takes a runtime that can call one AND a gate the daemon
|
|
391
|
+
// controls: Claude Code has the SDK server plus its PreToolUse matcher, Cursor has customTools plus a consult inside the
|
|
392
|
+
// handler, and Codex, OpenCode (Grok, Gemini), an ACP agent, Devin, the Kimi and Grok CLIs, Pi and Muse reach the door's
|
|
393
|
+
// `code` server, whose every script is consulted against the gate its loop attaches to the turn's relay. Every other
|
|
394
|
+
// runtime is a child process with neither, where a script would run unread.
|
|
395
|
+
test("the js execution backend rides the runtimes that can host the daemon's own tools", () => {
|
|
396
|
+
const hosts: ReadonlySet<string> = new Set([
|
|
397
|
+
"claude-code",
|
|
398
|
+
"cursor",
|
|
399
|
+
"codex",
|
|
400
|
+
"opencode",
|
|
401
|
+
"opencode-gemini",
|
|
402
|
+
"acp",
|
|
403
|
+
"devin",
|
|
404
|
+
"kimi-cli",
|
|
405
|
+
"grok-build",
|
|
406
|
+
"pi",
|
|
407
|
+
"muse",
|
|
408
|
+
]);
|
|
221
409
|
for (const { provider, harness } of pairs) {
|
|
222
410
|
const capabilities = capabilitiesOf(provider, harness);
|
|
223
411
|
expect(capabilities.execution.includes("js")).toBe(hosts.has(capabilities.runtime));
|
|
@@ -259,14 +447,22 @@ describe("the max-effort rule", () => {
|
|
|
259
447
|
});
|
|
260
448
|
});
|
|
261
449
|
|
|
262
|
-
// An `endpoint/<id>` provider is a user's own model API, told apart from an ACP agent only by its id: it runs
|
|
263
|
-
// Claude Code loop at full ceiling, never the ACP floor.
|
|
450
|
+
// An `endpoint/<id>` provider is a user's own model API, told apart from an ACP agent only by its id: by default it runs
|
|
451
|
+
// the Claude Code loop at full ceiling, never the ACP floor, and Codex and OpenCode as the owner picks them.
|
|
264
452
|
describe("a configured model endpoint", () => {
|
|
265
|
-
it("runs the Claude Code loop at full ceiling,
|
|
453
|
+
it("runs the Claude Code loop at full ceiling by default, and Codex, OpenCode or Muse Code where asked", () => {
|
|
266
454
|
for (const harness of HARNESSES.map((entry) => entry.value)) {
|
|
267
|
-
|
|
455
|
+
const hosted =
|
|
456
|
+
harness === "codex" || harness === "opencode"
|
|
457
|
+
? capabilitiesOf("grok", harness)
|
|
458
|
+
: harness === "muse"
|
|
459
|
+
? capabilitiesOf("meta", "muse")
|
|
460
|
+
: capabilitiesOf("claude", harness);
|
|
461
|
+
expect(capabilitiesOf("endpoint/ollama", harness), harness).toEqual(hosted);
|
|
268
462
|
}
|
|
269
463
|
expect(limitationsOf(capabilitiesOf("endpoint/ollama", "native"))).toEqual([]);
|
|
464
|
+
expect(capabilitiesOf("endpoint/ollama", "muse").runtime).toBe("muse");
|
|
465
|
+
expect(capabilitiesOf("endpoint/free-trial", "muse").runtime).toBe("claude-code");
|
|
270
466
|
});
|
|
271
467
|
|
|
272
468
|
it("is told apart from an ACP agent by its id alone, so no manifest lookup is needed to read the record", () => {
|
|
@@ -284,7 +480,7 @@ describe("a configured model endpoint", () => {
|
|
|
284
480
|
});
|
|
285
481
|
});
|
|
286
482
|
|
|
287
|
-
// Fast speed needs
|
|
483
|
+
// Fast speed needs every condition at once; a translator-routed provider or a Claude model with no `fast` badge
|
|
288
484
|
// fails silently otherwise: the turn runs at standard speed and only the bill shows it.
|
|
289
485
|
describe("offering fast speed", () => {
|
|
290
486
|
it("is offered for a Claude model that publishes the badge", () => {
|
|
@@ -302,10 +498,14 @@ describe("offering fast speed", () => {
|
|
|
302
498
|
expect(fastAllowed(capabilitiesOf("claude", "native"), "claude", undefined)).toBe(false);
|
|
303
499
|
});
|
|
304
500
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
501
|
+
// Codex asks its own vendor for the tier its own catalog published, so there is no route to second-guess.
|
|
502
|
+
it("is offered by Codex for a model that publishes the badge, and only for one", () => {
|
|
503
|
+
expect(fastAllowed(capabilitiesOf("codex", "native"), "codex", ["fast"])).toBe(true);
|
|
504
|
+
expect(fastAllowed(capabilitiesOf("codex", "native"), "codex", undefined)).toBe(false);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("is refused by every runtime that cannot ask for it", () => {
|
|
508
|
+
expect(fastAllowed(capabilitiesOf("grok", "native"), "grok", ["fast"])).toBe(false);
|
|
309
509
|
expect(fastAllowed(capabilitiesOf("some-installed-agent", "native"), "some-installed-agent", ["fast"])).toBe(false);
|
|
310
510
|
});
|
|
311
511
|
});
|
|
@@ -329,3 +529,67 @@ describe("the free providers", () => {
|
|
|
329
529
|
}
|
|
330
530
|
});
|
|
331
531
|
});
|
|
532
|
+
|
|
533
|
+
// A vendor's own terms ride beside the loop they speak to, said quietly (an icon's hover card). Resolved as the turn is:
|
|
534
|
+
// a loop the provider does not offer reads as the one it runs, so the note follows what actually runs.
|
|
535
|
+
describe("the vendor's terms for a loop", () => {
|
|
536
|
+
it("marks the routes a vendor forbids, whichever loop is asked for", () => {
|
|
537
|
+
expect(termsOf("gemini", "native")?.status).toBe("against-terms");
|
|
538
|
+
expect(termsOf("gemini", "claude-code")).toEqual(termsOf("gemini", "native"));
|
|
539
|
+
expect(termsOf("meta", "claude-code")?.status).toBe("against-terms");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("says nothing for Meta's key in Muse Code, the use Meta's terms sanction", () => {
|
|
543
|
+
expect(termsOf("meta", "native")).toBeUndefined();
|
|
544
|
+
expect(termsOf("meta", "muse")).toBeUndefined();
|
|
545
|
+
expect(termsOf("endpoint/local", "muse")).toBeUndefined();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("marks a loop the vendor does not list, and only that loop", () => {
|
|
549
|
+
expect(termsOf("codex", "claude-code")?.status).toBe("unlisted");
|
|
550
|
+
expect(termsOf("codex", "native")).toBeUndefined();
|
|
551
|
+
expect(termsOf("grok", "claude-code")?.status).toBe("unlisted");
|
|
552
|
+
expect(termsOf("grok", "native")).toBeUndefined();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("says nothing where the vendor sanctions the loop, or for a provider it knows nothing about", () => {
|
|
556
|
+
for (const provider of ["claude", "kimi", "zai", "cursor", "devin", "endpoint/local", "some-installed-agent"]) {
|
|
557
|
+
expect(termsOf(provider, "native"), provider).toBeUndefined();
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("always says where the vendor says it", () => {
|
|
562
|
+
for (const provider of PROVIDERS) {
|
|
563
|
+
for (const harness of HARNESSES) {
|
|
564
|
+
const terms = termsOf(provider.value, harness.value);
|
|
565
|
+
if (terms !== undefined) {
|
|
566
|
+
expect(terms.source).toMatch(/^https:\/\//);
|
|
567
|
+
expect(terms.note).toMatch(/\S/);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// A vendor CLI's loop names the program, since the provider's own name is on its other loops too; every other loop keeps
|
|
575
|
+
// its own name, and an installed agent's `vendor` loop is still the agent's.
|
|
576
|
+
test("the vendor loop of Grok and Kimi is named for the CLI that runs it", () => {
|
|
577
|
+
expect(loopLabel("grok", "vendor")).toBe("Grok Build");
|
|
578
|
+
expect(loopLabel("kimi", "vendor")).toBe("Kimi CLI");
|
|
579
|
+
expect(loopLabel("grok", "opencode")).toBe("OpenCode");
|
|
580
|
+
expect(loopLabel("devin", "vendor")).toBe("Devin");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// The CLI loops sign in through a door variant of their own; the other loops spend the provider's default credential, so
|
|
584
|
+
// one account list serves both and the variant says which loop a row belongs to.
|
|
585
|
+
test("an account serves the loop whose door its variant names", () => {
|
|
586
|
+
expect(loopDoor("grok", "vendor")).toBe("grok-build");
|
|
587
|
+
expect(loopDoor("grok", "native")).toBeUndefined();
|
|
588
|
+
expect(loopDoors("kimi")).toEqual([{ loop: "vendor", door: "kimi-cli" }]);
|
|
589
|
+
expect(accountServesLoop("grok", "vendor", { variant: "grok-build" })).toBe(true);
|
|
590
|
+
expect(accountServesLoop("grok", "native", { variant: "grok-build" })).toBe(false);
|
|
591
|
+
expect(accountServesLoop("grok", "opencode", {})).toBe(true);
|
|
592
|
+
expect(accountServesLoop("grok", "vendor", {})).toBe(false);
|
|
593
|
+
// Z.ai's variants are estates, not doors: every account serves its one loop.
|
|
594
|
+
expect(accountServesLoop("zai", "native", { variant: "bigmodel" })).toBe(true);
|
|
595
|
+
});
|