Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import {
GrokSettings,
ProjectId,
ProviderInstanceId,
ProviderSessionId,
type RuntimeMode,
Expand All @@ -12,6 +13,7 @@ import * as EffectAcpErrors from "effect-acp/errors";
import { xAiRateLimitedErrorCode } from "../../provider/acp/XAiAcpExtension.ts";
import { assert, describe, it } from "@effect/vitest";
import * as Crypto from "effect/Crypto";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
Expand All @@ -21,8 +23,16 @@ import { ChildProcessSpawner } from "effect/unstable/process";
import type * as EffectAcpSchema from "effect-acp/compat";

import { ServerConfig } from "../../config.ts";
import { ProjectionProjectRepository } from "../../persistence/Services/ProjectionProjects.ts";
import { buildInitialGrokProviderSnapshot } from "../../provider/Layers/GrokProvider.ts";
import type { ProviderInstance } from "../../provider/ProviderDriver.ts";
import { ProviderInstanceRegistry } from "../../provider/Services/ProviderInstanceRegistry.ts";
import { layer as idAllocatorLayer, IdAllocatorV2 } from "../IdAllocator.ts";
import { ProviderAdapterV2RuntimePolicy } from "../ProviderAdapter.ts";
import {
layerFromProjectRepository as runtimePolicyLayerFromProjectRepository,
RuntimePolicyV2,
} from "../RuntimePolicy.ts";
import { acpPermissionDisposition } from "../../provider/acp/AcpClientPolicy.ts";
import {
AcpProviderCapabilitiesV2,
Expand Down Expand Up @@ -341,7 +351,6 @@ describe("Grok launch permission mode", () => {

for (const [runtimeMode, args] of [
["approval-required", ["--permission-mode", "default", "agent", "stdio"]],
["auto-accept-edits", ["--permission-mode", "default", "agent", "stdio"]],
["auto", ["--permission-mode", "auto", "agent", "stdio"]],
["full-access", ["agent", "--always-approve", "stdio"]],
] as const) {
Expand All @@ -352,6 +361,68 @@ describe("Grok launch permission mode", () => {
);
}

it.effect("launches a thread stored as Auto-accept edits asking", () =>
Effect.gen(function* () {
// The policy the orchestrator resolves from Grok's own provider snapshot.
const snapshot = yield* buildInitialGrokProviderSnapshot(LAUNCH_TEST_GROK_SETTINGS);
const instanceId = ProviderInstanceId.make("grok-launch-test");
const now = yield* DateTime.now;
const threadId = ThreadId.make("grok-launch-test");
const modelSelection = { instanceId, model: "grok-build" } as const;
const resolved = yield* Effect.gen(function* () {
const runtimePolicy = yield* RuntimePolicyV2;
return yield* runtimePolicy.resolve({
thread: {
createdBy: "user",
creationSource: "web",
id: threadId,
projectId: ProjectId.make("grok-launch-test"),
title: "Grok launch test",
providerInstanceId: instanceId,
modelSelection,
runtimeMode: "auto-accept-edits",
interactionMode: "default",
branch: null,
worktreePath: process.cwd(),
activeProviderThreadId: null,
lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
forkedFrom: null,
createdAt: now,
updatedAt: now,
archivedAt: null,
settledOverride: null,
settledAt: null,
lastVisitedAt: null,
deletedAt: null,
},
modelSelection,
});
}).pipe(
Effect.provide(
runtimePolicyLayerFromProjectRepository.pipe(
Layer.provide(
Layer.mock(ProjectionProjectRepository)({
getById: () => Effect.die("the thread has a worktree"),
}),
),
Layer.provide(
Layer.mock(ProviderInstanceRegistry)({
getInstance: () =>
Effect.succeed({
snapshot: { getSnapshot: Effect.succeed(snapshot) },
} as ProviderInstance),
}),
),
),
),
);
assert.equal(resolved.runtimeMode, "approval-required");
assert.deepEqual(yield* launchArgs(resolved), [
["--permission-mode", "default", "agent", "stdio"],
]);
}),
);

it.effect("launches asking when an explicit approval or sandbox policy governs the thread", () =>
Effect.gen(function* () {
const asking = [["--permission-mode", "default", "agent", "stdio"]];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ const providerInstance = {
},
displayName: "Codex test",
enabled: true,
snapshot: {} as ProviderInstance["snapshot"],
// No supportedRuntimeModes: every runtime mode runs as stored.
snapshot: { getSnapshot: Effect.succeed({}) } as unknown as ProviderInstance["snapshot"],
orchestrationAdapter,
textGeneration: {} as ProviderInstance["textGeneration"],
} satisfies ProviderInstance;
Expand Down
52 changes: 51 additions & 1 deletion apps/server/src/orchestration-v2/RuntimePolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import {
type OrchestrationV2AppThread,
ProjectId,
ProviderInstanceId,
type RuntimeMode,
type ServerProvider,
ThreadId,
} from "@t3tools/contracts";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Stream from "effect/Stream";

import * as ProjectionProjects from "../persistence/Services/ProjectionProjects.ts";
import type { ProviderInstance } from "../provider/ProviderDriver.ts";
import { ProviderInstanceRegistry } from "../provider/Services/ProviderInstanceRegistry.ts";
import { layerFromProjectRepository, RuntimePolicyV2 } from "./RuntimePolicy.ts";

const projectId = ProjectId.make("project:runtime-policy");
Expand All @@ -24,6 +29,7 @@ const modelSelection = {
function makeThread(input: {
readonly now: DateTime.Utc;
readonly worktreePath: string | null;
readonly runtimeMode?: RuntimeMode;
}): OrchestrationV2AppThread {
const threadId = ThreadId.make("thread:runtime-policy");
return {
Expand All @@ -34,7 +40,7 @@ function makeThread(input: {
title: "Runtime policy",
providerInstanceId,
modelSelection,
runtimeMode: "full-access",
runtimeMode: input.runtimeMode ?? "full-access",
interactionMode: "default",
branch: null,
worktreePath: input.worktreePath,
Expand All @@ -55,7 +61,31 @@ function makeThread(input: {
};
}

// Grok's instance offers no Auto-accept edits; the Codex instance advertises no
// restriction.
const grokInstanceId = ProviderInstanceId.make("grok");
const supportedRuntimeModesByInstance = new Map<ProviderInstanceId, ReadonlyArray<RuntimeMode>>([
[grokInstanceId, ["approval-required", "auto", "full-access"]],
]);
const providerInstanceFor = (instanceId: ProviderInstanceId) =>
({
snapshot: {
getSnapshot: Effect.succeed({
supportedRuntimeModes: supportedRuntimeModesByInstance.get(instanceId),
} as ServerProvider),
},
}) as ProviderInstance;

const TestLayer = layerFromProjectRepository.pipe(
Layer.provide(
Layer.succeed(ProviderInstanceRegistry, {
getInstance: (instanceId) => Effect.succeed(providerInstanceFor(instanceId)),
listInstances: Effect.succeed([]),
listUnavailable: Effect.succeed([]),
streamChanges: Stream.empty,
subscribeChanges: Effect.never,
}),
),
Layer.provide(
Layer.mock(ProjectionProjects.ProjectionProjectRepository)({
getById: () =>
Expand Down Expand Up @@ -101,4 +131,24 @@ it.layer(TestLayer)("RuntimePolicyV2", (it) => {
assert.equal(resolved.cwd, "/project-worktree");
}),
);

it.effect("runs a mode the provider does not offer in Supervised", () =>
Effect.gen(function* () {
const policy = yield* RuntimePolicyV2;
const now = yield* DateTime.now;
const modeFor = (instanceId: ProviderInstanceId, runtimeMode: RuntimeMode) =>
policy
.resolve({
thread: makeThread({ now, worktreePath: null, runtimeMode }),
modelSelection: { instanceId, model: "test-model" },
})
.pipe(Effect.map((resolved) => resolved.runtimeMode));

assert.equal(yield* modeFor(grokInstanceId, "auto-accept-edits"), "approval-required");
assert.equal(yield* modeFor(grokInstanceId, "auto"), "auto");
assert.equal(yield* modeFor(grokInstanceId, "full-access"), "full-access");
// A provider that advertises no restriction runs every mode as stored.
assert.equal(yield* modeFor(providerInstanceId, "auto-accept-edits"), "auto-accept-edits");
}),
);
});
28 changes: 26 additions & 2 deletions apps/server/src/orchestration-v2/RuntimePolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
OrchestrationV2AppThread,
ProjectId,
ProviderInstanceId,
type RuntimeMode,
} from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
Expand All @@ -11,6 +12,7 @@ import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as ProjectionProjects from "../persistence/Services/ProjectionProjects.ts";
import { ProviderInstanceRegistry } from "../provider/Services/ProviderInstanceRegistry.ts";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import this service module as a namespace at the new service boundary, then use ProviderInstanceRegistry.ProviderInstanceRegistry in the layer requirement and yield* acquisition. This preserves the module's public service shape.

Posted via Macroscope — Effect Service Conventions

import {
ProviderAdapterV2RuntimePolicy,
type ProviderAdapterV2RuntimePolicy as ProviderAdapterV2RuntimePolicyType,
Expand Down Expand Up @@ -69,16 +71,38 @@ export const layer: Layer.Layer<RuntimePolicyV2> = Layer.succeed(RuntimePolicyV2
}),
});

/**
* The mode a provider runs a thread in. A mode the provider does not offer
* (a thread set before it stopped offering it, or a stale client) runs in
* Supervised rather than having T3 imitate it.
*/
function providerRuntimeMode(
runtimeMode: RuntimeMode,
supportedRuntimeModes: ReadonlyArray<RuntimeMode> | undefined,
): RuntimeMode {
return supportedRuntimeModes === undefined ||
supportedRuntimeModes.length === 0 ||
supportedRuntimeModes.includes(runtimeMode)
? runtimeMode
: "approval-required";
}

export const layerFromProjectRepository: Layer.Layer<
RuntimePolicyV2,
never,
ProjectionProjects.ProjectionProjectRepository
ProjectionProjects.ProjectionProjectRepository | ProviderInstanceRegistry
> = Layer.effect(
RuntimePolicyV2,
Effect.gen(function* () {
const projects = yield* ProjectionProjects.ProjectionProjectRepository;
const providerInstances = yield* ProviderInstanceRegistry;
return RuntimePolicyV2.of({
resolve: Effect.fn("RuntimePolicyV2.resolve")(function* (input) {
const instance = yield* providerInstances.getInstance(input.modelSelection.instanceId);
const supportedRuntimeModes =
instance === undefined
? undefined
: (yield* instance.snapshot.getSnapshot).supportedRuntimeModes;
const cwd =
input.thread.worktreePath ??
(yield* projects.getById({ projectId: input.thread.projectId }).pipe(
Expand All @@ -105,7 +129,7 @@ export const layerFromProjectRepository: Layer.Layer<
),
));
return ProviderAdapterV2RuntimePolicy.make({
runtimeMode: input.thread.runtimeMode,
runtimeMode: providerRuntimeMode(input.thread.runtimeMode, supportedRuntimeModes),
interactionMode: input.thread.interactionMode,
cwd,
});
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/orchestration-v2/runtimeLayer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ const providerInstance = {
},
displayName: "Codex test",
enabled: true,
snapshot: {} as ProviderInstance["snapshot"],
// No supportedRuntimeModes: every runtime mode runs as stored.
snapshot: { getSnapshot: Effect.succeed({}) } as unknown as ProviderInstance["snapshot"],
orchestrationAdapter,
textGeneration: {} as ProviderInstance["textGeneration"],
} satisfies ProviderInstance;
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/provider/Layers/GrokProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
} from "../providerMaintenance.ts";
import {
GROK_DEFAULT_MODEL_SLUG,
GROK_SUPPORTED_RUNTIME_MODES,
isValidGrokReasoningEffortToken,
makeGrokAcpRuntime,
resolveGrokAcpBaseModelId,
Expand All @@ -48,6 +49,7 @@ const GROK_PRESENTATION = {
displayName: "Grok",
supportsConversationRollback: false,
showInteractionModeToggle: false,
supportedRuntimeModes: GROK_SUPPORTED_RUNTIME_MODES,
} as const;
const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
optionDescriptors: [],
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/acp/AcpClientPolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe("acpPermissionDisposition", () => {
});

it("auto-accept-edits approves file changes without locations and asks for the rest", () => {
// Grok's session/request_permission carries no locations.
// ACP permission requests need not carry locations.
const autoAcceptEdits: AcpRuntimePolicy = { runtimeMode: "auto-accept-edits", cwd };
for (const kind of ["edit", "delete", "move"] as const) {
assert.equal(acpPermissionDisposition(autoAcceptEdits, permissionRequest(kind)), "allow");
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/acp/AcpClientPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ function acpOperationDisposition(
case undefined:
if (runtimePolicy.runtimeMode === "approval-required") return "deny";
// Auto-accept edits approves file changes wherever the agent makes them
// (Grok's prompts carry no locations to confine); other actions still ask.
// (ACP prompts need not carry locations to confine); other actions still ask.
if (
runtimePolicy.runtimeMode === "auto-accept-edits" &&
runtimePolicy.approvalPolicy === undefined &&
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/provider/acp/GrokAcpSupport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ describe("grokAcpSpawnArgs", () => {
expect(grokAcpSpawnArgs("full-access")).toEqual(["agent", "--always-approve", "stdio"]);
});

it("launches Auto-accept edits asking and Auto on Grok's classifier", () => {
it("launches Auto on Grok's classifier and a mode Grok does not offer asking", () => {
expect(grokAcpSpawnArgs("auto")).toEqual(["--permission-mode", "auto", "agent", "stdio"]);
expect(grokAcpSpawnArgs("auto-accept-edits")).toEqual([
"--permission-mode",
"default",
"agent",
"stdio",
]);
expect(grokAcpSpawnArgs("auto")).toEqual(["--permission-mode", "auto", "agent", "stdio"]);
});
});

Expand Down
22 changes: 15 additions & 7 deletions apps/server/src/provider/acp/GrokAcpSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,32 @@ interface GrokAcpRuntimeInput extends Omit<
readonly runtimeMode?: RuntimeMode;
}

/**
* The runtime modes `grok agent` can launch in: ask, its auto classifier, and
* always-approve. It has no Auto-accept edits: `acceptEdits` only exists as a
* settings-file `permissions.defaultMode`, and `grok agent` treats it as ask.
*/
export const GROK_SUPPORTED_RUNTIME_MODES = [
"approval-required",
"auto",
"full-access",
] as const satisfies ReadonlyArray<RuntimeMode>;

/**
* Launch argv for a runtime mode. `--permission-mode` on the argv beats the
* user's Grok config, so Supervised cannot inherit a configured always-approve.
* `grok agent` only wires always-approve and auto at launch; `acceptEdits`
* would behave exactly like `default`, so Auto-accept edits launches asking
* and T3's ACP client policy approves the edit prompts.
* A mode Grok does not offer launches asking.
*/
export function grokAcpSpawnArgs(runtimeMode?: RuntimeMode): ReadonlyArray<string> {
switch (runtimeMode) {
case "approval-required":
case "auto-accept-edits":
return ["--permission-mode", "default", "agent", "stdio"];
case undefined:
return ["agent", "stdio"];
case "auto":
return ["--permission-mode", "auto", "agent", "stdio"];
case "full-access":
return ["agent", "--always-approve", "stdio"];
default:
return ["agent", "stdio"];
return ["--permission-mode", "default", "agent", "stdio"];
}
}

Expand Down
Loading