Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
28 changes: 13 additions & 15 deletions .github/workflows/lazurio-fork-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,26 +138,20 @@ jobs:
apps/web/src/rightPanelStore.test.ts
apps/web/src/routes/_chat.pull-requests.tsx
# The managed Lazurio Folder (LazurioPlatform F51): with T3CODE_LAZURIO_FOLDER, Chat
# starts in the Environment's Folder. The server adopts or creates its project at
# startup, keeps commands and agent turns there, never makes the Folder root a
# repository and tells clients the Folder; `t3 lazurio` reports its capabilities and
# reconciles the Platform's recommended model where the person chose none. The web
# starts new threads there, hides other projects' entry points and shows the managed
# welcome. Five server test fixtures gain the settings service's new method.
# starts in the Environment's Folder. Its code lives in apps/*/src/lazurio/; these are
# the hooks. The server adopts or creates the Folder's project at startup, the engine,
# the provider reactor and the VCS service keep commands, agent turns and repositories
# there, the settings writer reconciles the Platform's recommended model where the
# person chose none, `t3 lazurio` reports both, and clients learn the Folder. The web
# starts new threads there, leaves out what leads elsewhere and shows the managed
# welcome. The engine's, the reactor's and the VCS service's tests cover the hooks.
apps/server/src/bin.ts
apps/server/src/cli/lazurio.ts
apps/server/src/device/DeviceService.test.ts
apps/server/src/http.ts
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
apps/server/src/orchestration/Layers/OrchestrationEngine.ts
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
apps/server/src/orchestration/ThreadSettlementReactor.test.ts
apps/server/src/provider/Layers/ProviderRegistry.test.ts
apps/server/src/provider/makeManagedServerProvider.test.ts
apps/server/src/serverRuntimeStartup.ts
apps/server/src/serverSettings.test.ts
apps/server/src/serverSettings.ts
apps/server/src/terminal/Manager.test.ts
apps/server/src/vcs/VcsProvisioningService.test.ts
apps/server/src/vcs/VcsProvisioningService.ts
apps/server/src/ws.ts
Expand All @@ -173,7 +167,6 @@ jobs:
apps/web/src/routes/welcome.tsx
packages/contracts/src/server.ts
packages/contracts/src/settings.ts
packages/shared/src/serverSettings.ts
)
unexpected="$(git diff --name-only "$UPSTREAM_SHA..HEAD" |
grep -E '^(apps|packages|scripts)/' |
Expand Down Expand Up @@ -203,6 +196,11 @@ jobs:
- name: Check formatting
run: pnpm exec vp fmt --check

# Upstream's lint script with upstream's rules, including the per-file ceilings on
# ManagedRuntime.make in tests that its own `vp check` holds.
- name: Lint
run: pnpm run lint

- name: Typecheck server and web
run: |
pnpm exec vp run --filter t3 typecheck
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { claudeHistoryCommand } from "./cli/claudeHistory.ts";
import { serviceLauncherCommand } from "./cli/serviceLauncher.ts";
import { servicePreflightCommand } from "./cli/servicePreflight.ts";
import { sshHelperCommand } from "./cli/sshHelper.ts";
import { lazurioCommand } from "./cli/lazurio.ts";
import { lazurioCommand } from "./lazurio/cli.ts";
import { themeCommand } from "./cli/theme.ts";
import { traceCommand } from "./cli/trace.ts";
import { triageCommand } from "./cli/triage.ts";
Expand Down
62 changes: 5 additions & 57 deletions apps/server/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Argument, Flag } from "effect/unstable/cli";

import { readBootstrapEnvelope } from "../bootstrap.ts";
import * as ServerConfig from "../config.ts";
import { resolveManagedFolderConfig } from "../lazurio/managedFolderConfig.ts";
import { expandHomePath, resolveBaseDir } from "../os-jank.ts";

const modeFlag = Flag.Literals("mode", ServerConfig.RuntimeMode.literals).pipe(
Expand Down Expand Up @@ -92,14 +93,6 @@ export const traceMaxFilesConfig = Config.Int("T3CODE_TRACE_MAX_FILES").pipe(
);

const EnvServerConfig = Config.all({
lazurioToolsUrl: Config.URL("T3CODE_LAZURIO_TOOLS_URL").pipe(
Config.option,
Config.map(Option.getOrUndefined),
),
lazurioFolder: Config.String("T3CODE_LAZURIO_FOLDER").pipe(
Config.option,
Config.map(Option.getOrUndefined),
),
logLevel: Config.LogLevel("T3CODE_LOG_LEVEL").pipe(Config.withDefault("Info")),
traceMinLevel: Config.LogLevel("T3CODE_TRACE_MIN_LEVEL").pipe(Config.withDefault("Info")),
traceTimingEnabled: Config.Boolean("T3CODE_TRACE_TIMING_ENABLED").pipe(Config.withDefault(true)),
Expand Down Expand Up @@ -377,50 +370,7 @@ export const resolveServerConfig = (
const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd());
const cwd = path.resolve(yield* expandHomePath(rawCwd.trim()));
yield* fs.makeDirectory(cwd, { recursive: true });
const declaredFolder = env.lazurioFolder;
const lazurioFolder =
declaredFolder === undefined
? undefined
: yield* Effect.gen(function* () {
if (!path.isAbsolute(declaredFolder))
return yield* Effect.fail(
new Config.ConfigError(
new Schema.SchemaError(
new SchemaIssue.InvalidValue({
message: "T3CODE_LAZURIO_FOLDER must be an absolute existing directory.",
}),
),
),
);
const root = yield* fs.realPath(declaredFolder);
const info = yield* fs.stat(root);
if (info.type !== "Directory")
return yield* Effect.fail(
new Config.ConfigError(
new Schema.SchemaError(
new SchemaIssue.InvalidValue({
message: "T3CODE_LAZURIO_FOLDER must be a directory.",
}),
),
),
);
return root;
});
if (
env.lazurioToolsUrl !== undefined &&
(env.lazurioToolsUrl.protocol !== "https:" ||
env.lazurioToolsUrl.username ||
env.lazurioToolsUrl.password)
)
return yield* Effect.fail(
new Config.ConfigError(
new Schema.SchemaError(
new SchemaIssue.InvalidValue({
message: "T3CODE_LAZURIO_TOOLS_URL must be an HTTPS URL without credentials.",
}),
),
),
);
const managedFolder = yield* resolveManagedFolderConfig;
const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl, {
baseDirIsExplicit: Option.isSome(explicitBaseDir),
});
Expand Down Expand Up @@ -535,11 +485,7 @@ export const resolveServerConfig = (
otelEnvironment: otel,
mode,
port,
cwd: lazurioFolder ?? cwd,
...(lazurioFolder === undefined ? {} : { lazurioFolder }),
...(env.lazurioToolsUrl === undefined
? {}
: { lazurioToolsUrl: env.lazurioToolsUrl.toString() }),
cwd,
baseDir,
...derivedPaths,
serverTracePath,
Expand All @@ -560,6 +506,8 @@ export const resolveServerConfig = (
logWebSocketEvents,
tailscaleServeEnabled,
tailscaleServePort,
// Lazurio (LazurioPlatform F51): T3CODE_LAZURIO_FOLDER makes the Folder the cwd, unbootstrapped.
...managedFolder,
};

return config;
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/device/DeviceService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ const fixture = Effect.fn("fixture")(function* (
start: Effect.void,
ready: Effect.void,
getSettings: Ref.get(settings),
reconcileRecommendedModel: () => Effect.die("unexpected recommendation in this fixture"),
updateSettings: (patch) =>
Ref.updateAndGet(settings, (current) => ({
...current,
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/http.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as Mime from "effect/unstable/http/Mime";
import {
type AuthEnvironmentScope,
AuthOrchestrationOperateScope,
AuthOrchestrationReadScope,
EnvironmentHttpApi,
Expand Down Expand Up @@ -273,7 +272,9 @@ export function resolveDevRedirectUrl(devUrl: URL, requestUrl: URL): string {
return redirectUrl.toString();
}

export const authenticateRawRouteWithScope = (scope: AuthEnvironmentScope) =>
const authenticateRawRouteWithScope = (
scope: typeof AuthOrchestrationReadScope | typeof AuthOrchestrationOperateScope,
) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
Expand Down
10 changes: 3 additions & 7 deletions apps/server/src/lazurio/agentBrowserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
* On a Lazurio Environment the agents of every thread work in the Environment browser, one
* Chromium on the Environment, through the agent-browser CLI, each thread in a window of its
* own. The CLI takes its session from AGENT_BROWSER_SESSION, so every provider process T3 starts
* for a thread carries the thread's name. The web client asks the Environment for the view of
* the same session, so `apps/web/src/lazurio/agentBrowserSession.ts` holds a copy of
* `agentBrowserSessionName`: sharing it through packages/shared would take an entry in that
* package's export map, which upstream rewrites every few days. The release contract test keeps
* the two copies equal.
* for a thread carries the thread's name. The same name is the thread's strip, whose holder file
* keeps its tabs (threadTabs.ts); the web client reaches them only through the server.
*/

import * as NodeCrypto from "node:crypto";
Expand All @@ -21,8 +18,7 @@ const SESSION_NAME_LIMIT = 64;
/** Hex digits of the id's SHA-256 kept in a cut or rewritten name. */
const DIGEST_LENGTH = 32;

/** SHA-256 of the UTF-8 bytes, as hex (node:crypto; the web copy computes the same with
* @noble/hashes, and the release contract test compares the two). */
/** SHA-256 of the UTF-8 bytes, as hex. */
const sha256Hex = (value: string): string =>
NodeCrypto.createHash("sha256").update(value, "utf8").digest("hex");

Expand Down
151 changes: 151 additions & 0 deletions apps/server/src/lazurio/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// @effect-diagnostics nodeBuiltinImport:off - CLI integration exercises Node HTTP and filesystem boundaries.
import * as NodeHttp from "node:http";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";

import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as NetService from "@t3tools/shared/Net";
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as TestConsole from "effect/testing/TestConsole";
import { Command } from "effect/unstable/cli";
import * as HttpRouter from "effect/unstable/http/HttpRouter";
import * as HttpServer from "effect/unstable/http/HttpServer";

import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
import { cli } from "../bin.ts";
import * as ServerConfig from "../config.ts";
import {
makePersistedServerRuntimeState,
persistServerRuntimeState,
} from "../serverRuntimeState.ts";
import * as Settings from "../serverSettings.ts";
import * as ModelRecommendationRoute from "./modelRecommendationRoute.ts";

// `t3 lazurio` is a contract: Machines checks `capabilities` and the Platform's
// `lazurio chat defaults --apply` reads `model`'s answer. These pin their output byte for byte.

const run = (args: ReadonlyArray<string>) =>
Command.runWith(cli, { version: "0.0.0" })(args).pipe(
Effect.provide(Layer.mergeAll(NodeServices.layer, NetService.layer)),
);
const printed = Effect.map(TestConsole.logLines, (lines) =>
lines.filter((line): line is string => typeof line === "string"),
);
const makeBaseDir = () => NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-lazurio-cli-"));
// The recommendation as the Platform sends it, and as T3 prints it back.
const selectionJson =
'{"instanceId":"codex","model":"gpt-6.1-sol","options":[{"id":"reasoningEffort","value":"high"}]}';
// The Folder exists, as Machines' T3CODE_LAZURIO_FOLDER does; the Platform sends its realpath.
const folder = NodeFS.realpathSync(
NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-lazurio-cli-folder-")),
);
const request = `{"folder":"${folder}","selection":${selectionJson}}`;

/** The model route on 127.0.0.1 over `baseDir`'s auth and settings, as the running service has it. */
const withLiveService = <A, E, R>(baseDir: string, body: Effect.Effect<A, E, R>) =>
Effect.gen(function* () {
const configLayer = Layer.effect(
ServerConfig.ServerConfig,
Effect.map(ServerConfig.ServerConfig, (config) => ({
...config,
host: "127.0.0.1",
lazurioFolder: folder,
})),
).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));
const serviceLayer = HttpRouter.serve(ModelRecommendationRoute.layer, {
disableListenLog: true,
disableLogger: true,
}).pipe(
Layer.provideMerge(Settings.layer.pipe(Layer.provide(ServerSecretStore.layer))),
Layer.provideMerge(EnvironmentAuth.runtimeLayer),
Layer.provideMerge(
NodeHttpServer.layer(NodeHttp.createServer, { host: "127.0.0.1", port: 0 }),
),
Layer.provideMerge(configLayer),
Layer.provideMerge(NodeServices.layer),
);
return yield* Effect.scoped(
Effect.gen(function* () {
const config = yield* ServerConfig.ServerConfig;
const address = (yield* HttpServer.HttpServer).address;
if (typeof address === "string" || !("port" in address)) {
return yield* Effect.die(`Expected a TCP address, got ${String(address)}`);
}
yield* persistServerRuntimeState({
path: config.serverRuntimeStatePath,
state: yield* makePersistedServerRuntimeState({ config, port: address.port }),
});
return yield* body;
}).pipe(Effect.provide(serviceLayer)),
);
});

describe("t3 lazurio", () => {
it.effect("capabilities: contract version 1, without reading any state", () =>
Effect.gen(function* () {
yield* run(["lazurio", "capabilities"]);
assert.deepStrictEqual(yield* printed, [
'{"version":1,"managedFolder":true,"modelRecommendation":true}',
]);
}).pipe(Effect.provide(TestConsole.layer)),
);

it.effect("model: reads the saved model without changing or creating anything", () =>
Effect.gen(function* () {
const empty = makeBaseDir();
yield* run(["lazurio", "model", "--base-dir", empty]);
assert.deepStrictEqual(NodeFS.readdirSync(empty), []);

const saved = makeBaseDir();
NodeFS.mkdirSync(NodePath.join(saved, "userdata"));
const settingsPath = NodePath.join(saved, "userdata", "settings.json");
const settingsText = `{"defaultModelSelection":${selectionJson},"lazurioManagedModelSelection":${selectionJson}}`;
NodeFS.writeFileSync(settingsPath, settingsText);
yield* run(["lazurio", "model", "--base-dir", saved]);
assert.strictEqual(NodeFS.readFileSync(settingsPath, "utf8"), settingsText);

assert.deepStrictEqual(yield* printed, [
'{"version":1,"current":null,"lastManaged":null}',
`{"version":1,"current":${selectionJson},"lastManaged":${selectionJson}}`,
]);
}).pipe(Effect.provide(TestConsole.layer)),
);

it.effect("model <selection>: only through the running service, never offline", () =>
Effect.gen(function* () {
const baseDir = makeBaseDir();
const error = yield* run(["lazurio", "model", "--base-dir", baseDir, request]).pipe(
Effect.flip,
);
assert.strictEqual(
error.message,
"The T3 service must be running. No settings were changed.",
);
assert.isFalse(NodeFS.existsSync(NodePath.join(baseDir, "userdata", "settings.json")));
assert.deepStrictEqual(yield* printed, []);
}).pipe(Effect.provide(TestConsole.layer)),
);

it.effect("model <selection>: prints the running service's answer as it is", () =>
Effect.gen(function* () {
const baseDir = makeBaseDir();
// The service runs in this process here, so only what the command printed counts.
const output = yield* withLiveService(
baseDir,
Effect.gen(function* () {
const before = (yield* printed).length;
yield* run(["lazurio", "model", "--base-dir", baseDir, request]);
return (yield* printed).slice(before);
}),
);
assert.deepStrictEqual(output, [
`{"version":1,"current":${selectionJson},"lastManaged":${selectionJson}}`,
]);
}).pipe(Effect.provide(TestConsole.layer)),
);
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Lazurio overlay (LazurioPlatform F51): `t3 lazurio`. Machines checks `t3 lazurio capabilities`
* (contract version 1) and the Platform's `lazurio chat defaults --apply` runs `t3 lazurio model`,
* so both commands' output is a contract: cli.test.ts pins it byte for byte.
*/
import { fromLenientJson } from "@t3tools/shared/schemaJson";
import * as Config from "effect/Config";
import { AuthOrchestrationOperateScope, ServerSettings } from "@t3tools/contracts";
Expand All @@ -13,11 +18,8 @@ import { FetchHttpClient, HttpBody, HttpClient } from "effect/unstable/http";
import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
import * as ServerConfig from "../config.ts";
import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
import {
MODEL_RECOMMENDATION_ROUTE,
ModelRecommendationRequest,
} from "../lazurio/modelRecommendation.ts";
import { projectLocationFlags, resolveCliAuthConfig } from "../cli/config.ts";
import { MODEL_RECOMMENDATION_ROUTE, ModelRecommendationRequest } from "./modelRecommendation.ts";

class RecommendationCommandError extends Schema.TaggedError<RecommendationCommandError>()(
"RecommendationCommandError",
Expand Down
Loading
Loading