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
22 changes: 22 additions & 0 deletions .github/workflows/lazurio-fork-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ jobs:
# the exact files below. Each one is a reviewed decision; every other
# path under those roots fails.
allowed_upstream_changes=(
# Managed Folder entry uses the shared native project and settings paths.
apps/web/src/hooks/useScratchProject.ts
apps/web/src/components/sidebar/SidebarThreadHeader.tsx
apps/web/src/components/Sidebar.tsx
apps/web/src/components/chat/DraftHeroHeadline.tsx
# Managed Lazurio Folder: server-owned start context and model recommendation.
apps/web/src/components/CommandPalette.tsx
apps/web/src/components/GitActionsControl.tsx
apps/web/src/components/onboarding/FirstRunGate.tsx
apps/web/src/hooks/useHandleNewThread.ts
apps/web/src/hooks/useHandleNewThread.test.ts
apps/web/src/routes/welcome.tsx
apps/web/src/lazurio/ManagedFolderWelcome.tsx
apps/web/src/lazurio/managedFolder.ts
apps/web/src/lazurio/managedFolder.test.ts
packages/contracts/src/server.ts
packages/contracts/src/settings.ts
packages/shared/src/serverSettings.ts
# In-app update from the configured release channel; proposed upstream.
apps/web/src/components/ChatView.tsx
apps/web/src/components/chat/useAutoBalanceUpdateBanner.tsx
Expand Down Expand Up @@ -154,6 +172,10 @@ jobs:
working-directory: apps/server
run: pnpm exec vp test run src/lazurio

- name: Test native managed Folder and preference boundaries
working-directory: apps/server
run: pnpm exec vp test run src/orchestration/Layers/OrchestrationEngine.test.ts src/serverSettings.test.ts src/vcs/VcsProvisioningService.test.ts

- name: Test the Lazurio web overlay
working-directory: apps/web
run: pnpm exec vp test run src/lazurio
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +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 { themeCommand } from "./cli/theme.ts";
import { traceCommand } from "./cli/trace.ts";
import { triageCommand } from "./cli/triage.ts";
Expand Down Expand Up @@ -72,6 +73,7 @@ export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>
servicePreflightCommand,
sshHelperCommand,
themeCommand,
lazurioCommand,
traceCommand,
triageCommand,
cloudEnabled ? connectCommand : connectUnavailableCommand,
Expand Down
58 changes: 57 additions & 1 deletion apps/server/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ 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 @@ -369,6 +377,50 @@ 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 derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl, {
baseDirIsExplicit: Option.isSome(explicitBaseDir),
});
Expand Down Expand Up @@ -483,7 +535,11 @@ export const resolveServerConfig = (
otelEnvironment: otel,
mode,
port,
cwd,
cwd: lazurioFolder ?? cwd,
...(lazurioFolder === undefined ? {} : { lazurioFolder }),
...(env.lazurioToolsUrl === undefined
? {}
: { lazurioToolsUrl: env.lazurioToolsUrl.toString() }),
baseDir,
...derivedPaths,
serverTracePath,
Expand Down
124 changes: 124 additions & 0 deletions apps/server/src/cli/lazurio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { fromLenientJson } from "@t3tools/shared/schemaJson";
import * as Config from "effect/Config";
import { AuthOrchestrationOperateScope, ServerSettings } from "@t3tools/contracts";
import * as FileSystem from "effect/FileSystem";
import { resolveBaseDir } from "../os-jank.ts";
import * as Console from "effect/Console";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import { Argument, Command, GlobalFlag } from "effect/unstable/cli";
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";

class RecommendationCommandError extends Schema.TaggedError<RecommendationCommandError>()(
"RecommendationCommandError",
{ detail: Schema.String },
) {
override get message() {
return this.detail;
}
}

const modelCommand = Command.make("model", {
...projectLocationFlags,
selection: Argument.String("selection-json").pipe(Argument.optional),
}).pipe(
Command.withDescription(
"Read saved model metadata without changing state, or reconcile a Platform recommendation through the running service.",
),
Command.withHandler((flags) =>
Effect.gen(function* () {
// Metadata-only status must not mint a session, create directories or
// start a second settings writer. The server remains the sole writer.
if (Option.isNone(flags.selection)) {
const configuredHome = yield* Config.String("T3CODE_HOME").pipe(Config.option);
const baseDir = yield* resolveBaseDir(
Option.getOrUndefined(Option.orElse(flags.baseDir, () => configuredHome)),
);
const paths = yield* ServerConfig.deriveServerPaths(baseDir, undefined, {
baseDirIsExplicit: true,
});
const fs = yield* FileSystem.FileSystem;
const settings = (yield* fs.exists(paths.settingsPath))
? yield* Schema.decodeEffect(fromLenientJson(ServerSettings))(
yield* fs.readFileString(paths.settingsPath),
)
: { defaultModelSelection: null, lazurioManagedModelSelection: null };
yield* Console.log(
yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({
version: 1,
current: settings.defaultModelSelection,
lastManaged: settings.lazurioManagedModelSelection,
}),
);
return;
}
const logLevel = yield* GlobalFlag.LogLevel;
const config = yield* resolveCliAuthConfig(flags, logLevel);
const selection = yield* Schema.decodeEffect(
Schema.fromJsonString(ModelRecommendationRequest),
)(flags.selection.value);
const state = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);
if (Option.isNone(state) || !isProcessAlive(state.value.pid))
return yield* new RecommendationCommandError({
detail: "The T3 service must be running. No settings were changed.",
});
return yield* Effect.gen(function* () {
const auth = yield* EnvironmentAuth.EnvironmentAuth;
const client = yield* HttpClient.HttpClient;
const output = yield* Effect.acquireUseRelease(
auth.issueSession({
scopes: [AuthOrchestrationOperateScope],
label: "lazurio model reconciliation",
}),
(issued) =>
Effect.gen(function* () {
const headers = { authorization: `Bearer ${issued.token}` };
const url = `${state.value.origin}${MODEL_RECOMMENDATION_ROUTE}`;
const response = yield* client.post(url, {
headers,
body: yield* HttpBody.json(selection),
});
if (response.status !== 200)
return yield* new RecommendationCommandError({
detail: `The live T3 service refused the recommendation (${response.status}). No offline fallback was attempted.`,
});
return yield* response.json;
}).pipe(Effect.timeout("10 seconds")),
(issued) => auth.revokeSession(issued.sessionId).pipe(Effect.ignore({ log: true })),
);
yield* Console.log(
yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(output),
);
}).pipe(
Effect.provide(
Layer.mergeAll(
EnvironmentAuth.runtimeLayer.pipe(Layer.provide(ServerConfig.layer(config))),
FetchHttpClient.layer,
),
),
);
}),
),
);
const capabilitiesCommand = Command.make("capabilities").pipe(
Command.withDescription(
"Report the versioned Lazurio hosting contract without reading or changing user state.",
),
Command.withHandler(() =>
Console.log(JSON.stringify({ version: 1, managedFolder: true, modelRecommendation: true })),
),
);

export const lazurioCommand = Command.make("lazurio").pipe(
Command.withSubcommands([modelCommand, capabilitiesCommand]),
);
3 changes: 3 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export class ServerConfig extends Context.Service<
/** Canonical HTTPS browser origin of a reverse-proxied server (`T3CODE_EXTERNAL_ORIGIN`). */
readonly externalOrigin?: URL | undefined;
readonly cwd: string;
/** Opt-in hosted Folder entry contract, declared by Machines. */
readonly lazurioFolder?: string;
readonly lazurioToolsUrl?: string;
readonly baseDir: string;
readonly staticDir: string | undefined;
readonly devUrl: URL | undefined;
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/device/DeviceService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ 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: 2 additions & 3 deletions apps/server/src/http.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as Mime from "effect/unstable/http/Mime";
import {
type AuthEnvironmentScope,
AuthOrchestrationOperateScope,
AuthOrchestrationReadScope,
EnvironmentHttpApi,
Expand Down Expand Up @@ -272,9 +273,7 @@ export function resolveDevRedirectUrl(devUrl: URL, requestUrl: URL): string {
return redirectUrl.toString();
}

const authenticateRawRouteWithScope = (
scope: typeof AuthOrchestrationReadScope | typeof AuthOrchestrationOperateScope,
) =>
export const authenticateRawRouteWithScope = (scope: AuthEnvironmentScope) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
Expand Down
54 changes: 54 additions & 0 deletions apps/server/src/lazurio/managedFolder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { OrchestrationCommand, OrchestrationReadModel } from "@t3tools/contracts";

// This is an entry-context contract, not a filesystem sandbox. Agents start
// at the Folder and can then work in its repositories with their normal tools.
export function managedFolderRefusal(
folder: string | undefined,
command: OrchestrationCommand,
state: Pick<OrchestrationReadModel, "projects" | "threads">,
): string | null {
if (folder === undefined) return null;
const message =
"Start a new chat in the Lazurio project. This Environment starts agents in its Lazurio Folder; existing conversations remain available as history.";
const project = (id: string) => state.projects.find((item) => item.id === id);
const allowedProject = (id: string) => project(id)?.workspaceRoot === folder;
switch (command.type) {
case "project.create":
return command.workspaceRoot === folder
? null
: "This Environment provides the Lazurio project at its Folder root.";
case "project.meta.update":
if (
allowedProject(command.projectId) &&
command.title !== undefined &&
command.title !== "Lazurio"
)
return "The managed project is named Lazurio.";
return command.workspaceRoot !== undefined &&
command.workspaceRoot !== project(command.projectId)?.workspaceRoot
? "A managed Environment does not retarget projects. Use the Lazurio project."
: null;
case "project.delete":
return allowedProject(command.projectId)
? "The Lazurio project is provided by this Environment and cannot be removed."
: null;
case "thread.create":
return allowedProject(command.projectId) && command.worktreePath === null ? null : message;
case "thread.meta.update":
return command.worktreePath != null
? "Chats in this Environment start at the Lazurio Folder root, without a T3 worktree."
: null;
case "thread.turn.start": {
const thread = state.threads.find((item) => item.id === command.threadId);
const target = command.bootstrap?.createThread ?? thread;
return target !== undefined &&
allowedProject(target.projectId) &&
target.worktreePath === null &&
command.bootstrap?.prepareWorktree === undefined
? null
: message;
}
default:
return null;
}
}
40 changes: 40 additions & 0 deletions apps/server/src/lazurio/managedFolderStartup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { CommandId, ProjectId } from "@t3tools/contracts";
import * as Crypto from "effect/Crypto";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as ServerConfig from "../config.ts";
import * as OrchestrationEngine from "../orchestration/Services/OrchestrationEngine.ts";
import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";

// The server is the only writer. Machines declares the Folder in its unit;
// this uses native commands, never the CLI's offline fallback or a DB edit.
export const ensureManagedFolderProject = Effect.gen(function* () {
const { lazurioFolder } = yield* ServerConfig.ServerConfig;
if (lazurioFolder === undefined) return {};
const query = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
const engine = yield* OrchestrationEngine.OrchestrationEngineService;
const crypto = yield* Crypto.Crypto;
const existing = yield* query.getActiveProjectByWorkspaceRoot(lazurioFolder);
const projectId = Option.isSome(existing)
? existing.value.id
: ProjectId.make(yield* crypto.randomUUIDv4);
if (Option.isNone(existing)) {
yield* engine.dispatch({
type: "project.create",
commandId: CommandId.make(yield* crypto.randomUUIDv4),
projectId,
title: "Lazurio",
workspaceRoot: lazurioFolder,
createdAt: DateTime.formatIso(yield* DateTime.now),
});
} else if (existing.value.title !== "Lazurio") {
yield* engine.dispatch({
type: "project.meta.update",
commandId: CommandId.make(yield* crypto.randomUUIDv4),
projectId,
title: "Lazurio",
});
}
return { bootstrapProjectId: projectId, bootstrapProjectCreated: Option.isNone(existing) };
});
7 changes: 7 additions & 0 deletions apps/server/src/lazurio/modelRecommendation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ModelSelection } from "@t3tools/contracts";
import * as Schema from "effect/Schema";
export const MODEL_RECOMMENDATION_ROUTE = "/api/lazurio/model-recommendation";
export const ModelRecommendationRequest = Schema.Struct({
folder: Schema.String,
selection: ModelSelection,
});
Loading
Loading