@xfey/tutti 0.1.119 → 0.1.121
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/dist/approvals/decision-command.d.ts +2 -2
- package/dist/collaboration-state/clarification-snapshot.d.ts +9 -9
- package/dist/collaboration-state/task-compile-context.d.ts +1 -1
- package/dist/control-plane/workflows/openai.d.ts +3 -4
- package/dist/control-plane/workflows/openai.js +3 -2
- package/dist/providers/openai/app-server/invocation-coordinator.d.ts +1 -0
- package/dist/providers/openai/app-server/invocation-coordinator.js +4 -1
- package/dist/providers/openai/app-server/invocation-deadline.d.ts +3 -0
- package/dist/providers/openai/app-server/invocation-deadline.js +15 -0
- package/dist/providers/openai/app-server/read-only-procedure.d.ts +4 -2
- package/dist/providers/openai/app-server/read-only-procedure.js +16 -0
- package/dist/providers/openai/app-server/workspace-write-run.d.ts +4 -2
- package/dist/providers/openai/app-server/workspace-write-run.js +16 -0
- package/dist/providers/openai/chat-assistant.d.ts +3 -4
- package/dist/providers/openai/chat-assistant.js +2 -1
- package/dist/providers/openai/execution-access.d.ts +17 -0
- package/dist/providers/openai/execution-access.js +13 -0
- package/dist/providers/openai/sdk-procedure-runner.d.ts +2 -2
- package/dist/providers/openai/sdk-procedure-runner.js +17 -0
- package/dist/run-pipeline/artifact-applicability.d.ts +3 -4
- package/dist/run-pipeline/artifact-applicability.js +2 -1
- package/dist/run-pipeline/openai.d.ts +3 -4
- package/dist/run-pipeline/openai.js +3 -2
- package/dist/run-pipeline/task-run-invocation.js +6 -12
- package/dist/server-shell/cli/cli.js +3 -0
- package/dist/server-shell/cli/host-lifecycle.js +1 -0
- package/dist/server-shell/cli/host-server-runtime.d.ts +1 -0
- package/dist/server-shell/cli/host-server-runtime.js +83 -34
- package/dist/server-shell/cli/launch-command.js +1 -1
- package/dist/server-shell/cli/launch.d.ts +7 -3
- package/dist/server-shell/cli/launch.js +23 -4
- package/dist/server-shell/cli/machine-local.d.ts +9 -0
- package/dist/server-shell/cli/machine-local.js +60 -6
- package/dist/server-shell/cli/project-resolver.d.ts +2 -2
- package/dist/server-shell/cli/project-resolver.js +6 -3
- package/dist/server-shell/cli/relay-registration.js +38 -1
- package/dist/server-shell/http/routes/local-control.d.ts +1 -1
- package/dist/server-shell/http/routes/local-control.js +2 -46
- package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +45 -137
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +45 -137
- package/dist/server-shell/http/routes/project-api/schemas.d.ts +24 -136
- package/dist/server-shell/http/routes/project-api/schemas.js +4 -58
- package/dist/server-shell/http/routes/project-api/types.d.ts +3 -0
- package/dist/server-shell/local-console/managed-project-routes.d.ts +9 -0
- package/dist/server-shell/local-console/managed-project-routes.js +120 -0
- package/dist/server-shell/local-console/managed-project-service.d.ts +141 -0
- package/dist/server-shell/local-console/managed-project-service.js +158 -0
- package/dist/server-shell/local-console/operation-coordinator.d.ts +1 -1
- package/dist/server-shell/local-console/package-update-hosts.js +2 -1
- package/dist/server-shell/local-console/project-service.d.ts +4 -1
- package/dist/server-shell/local-console/project-service.js +62 -14
- package/dist/server-shell/local-console/server.js +22 -2
- package/dist/server-shell/managed-access/execution-binding.d.ts +9 -0
- package/dist/server-shell/managed-access/execution-binding.js +22 -0
- package/dist/server-shell/managed-access/existing-project.d.ts +15 -0
- package/dist/server-shell/managed-access/existing-project.js +41 -0
- package/dist/server-shell/managed-access/legacy-records.d.ts +2 -0
- package/dist/server-shell/managed-access/legacy-records.js +24 -0
- package/dist/server-shell/managed-access/model-control-client.d.ts +65 -0
- package/dist/server-shell/managed-access/model-control-client.js +112 -0
- package/dist/server-shell/managed-access/private-store.d.ts +19 -0
- package/dist/server-shell/managed-access/private-store.js +275 -0
- package/dist/server-shell/managed-access/project-provider.d.ts +18 -0
- package/dist/server-shell/managed-access/project-provider.js +30 -0
- package/dist/server-shell/managed-access/records.d.ts +103 -0
- package/dist/server-shell/managed-access/records.js +74 -0
- package/dist/server-shell/managed-access/runners.d.ts +11 -0
- package/dist/server-shell/managed-access/runners.js +103 -0
- package/dist/server-shell/managed-access/runtime-records.d.ts +106 -0
- package/dist/server-shell/managed-access/runtime-records.js +65 -0
- package/dist/server-shell/managed-access/runtime.d.ts +48 -0
- package/dist/server-shell/managed-access/runtime.js +300 -0
- package/dist/server-shell/managed-access/state.d.ts +53 -0
- package/dist/server-shell/managed-access/state.js +292 -0
- package/dist/server-shell/managed-access/transport.d.ts +45 -0
- package/dist/server-shell/managed-access/transport.js +39 -0
- package/dist/server-shell/managed-access/workspace.d.ts +10 -0
- package/dist/server-shell/managed-access/workspace.js +69 -0
- package/dist/server-shell/smoke/e2e-client.d.ts +8 -3
- package/dist/server-shell/smoke/e2e-client.js +11 -11
- package/node_modules/@tutti/relay-client/dist/host-control.d.ts +4 -1
- package/node_modules/@tutti/relay-client/dist/host-control.js +26 -3
- package/node_modules/@tutti/relay-client/dist/index.d.ts +1 -0
- package/node_modules/@tutti/relay-client/dist/index.js +1 -0
- package/node_modules/@tutti/relay-client/dist/project-authority.d.ts +70 -0
- package/node_modules/@tutti/relay-client/dist/project-authority.js +139 -0
- package/node_modules/@tutti/shared/dist/ids/index.d.ts +7 -0
- package/node_modules/@tutti/shared/dist/ids/index.js +7 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/managed-account.d.ts +34 -0
- package/node_modules/@tutti/shared/dist/schemas/api/managed-account.js +40 -0
- package/node_modules/@tutti/shared/dist/schemas/api/provider-config.d.ts +7 -4
- package/node_modules/@tutti/shared/dist/schemas/api/provider-config.js +5 -0
- package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +3 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/creation-validation.d.ts +26 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/creation-validation.js +40 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +5 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/index.js +5 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/managed-account.d.ts +12 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/managed-account.js +13 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/managed-models.d.ts +17 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/managed-models.js +17 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/managed-runtime.d.ts +113 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/managed-runtime.js +84 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/project-authority.d.ts +114 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/project-authority.js +102 -0
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-B6lCCHBq.js → homepage-motion-scene-WfLyzcCG.js} +1 -1
- package/web/assets/{index-DsYi7AhN.css → index-ATsYiBZs.css} +1 -1
- package/web/assets/index-o9YCtzEd.js +70 -0
- package/web/index.html +2 -2
- package/web/assets/index-DepsYMLl.js +0 -70
|
@@ -14,9 +14,11 @@ import { cleanupStaleCodexAppServerTempRoots } from "../../providers/openai/app-
|
|
|
14
14
|
import { CodexAppServerInvocationCoordinator } from "../../providers/openai/app-server/invocation-coordinator.js";
|
|
15
15
|
import { resolveProjectOpenAiReadOnlyChatAssistantModel } from "../../providers/openai/chat-assistant.js";
|
|
16
16
|
import { HostPlatformOutboundCoordinator, HostPlatformOutboundWorker, } from "../../platform-outbound/index.js";
|
|
17
|
-
import { configureProjectOpenAiProvider, discoverProjectOpenAiModels,
|
|
17
|
+
import { configureProjectOpenAiProvider, discoverProjectOpenAiModels, updateProjectOpenAiDefaultModel, } from "../../providers/openai/index.js";
|
|
18
18
|
import { readProviderUsageProjection, recordProviderUsageEvent, } from "../../provider-usage/index.js";
|
|
19
19
|
import { resolveProjectOpenAiRunPipelineRunner } from "../../run-pipeline/openai.js";
|
|
20
|
+
import { createManagedChatModel, createManagedRunPipeline, createManagedWorkflowRunner, } from "../managed-access/runners.js";
|
|
21
|
+
import { createNativeManagedHostRuntime, readProjectModelAccess, } from "../managed-access/project-provider.js";
|
|
20
22
|
import { openHostProjectStore } from "../../store/index.js";
|
|
21
23
|
import { resolvePackagedWebStaticRoot } from "../http/static-web.js";
|
|
22
24
|
import { AGENT_CONTEXT_BASE_PATH, AgentContextTokenRegistry, } from "../http/routes/agent-context.js";
|
|
@@ -224,9 +226,10 @@ export async function startForegroundHostServer(options) {
|
|
|
224
226
|
now,
|
|
225
227
|
});
|
|
226
228
|
const workspaceEvents = new WorkspaceEventBus({ now });
|
|
227
|
-
const getProviderConfig = () =>
|
|
229
|
+
const getProviderConfig = () => readProjectModelAccess({
|
|
228
230
|
tuttiHome: options.preparation.tutti_home,
|
|
229
231
|
projectId: options.preparation.project_id,
|
|
232
|
+
relayUrl: options.preparation.relay_url,
|
|
230
233
|
});
|
|
231
234
|
const recordUsage = (usage) => {
|
|
232
235
|
recordProviderUsageEvent(store.db, {
|
|
@@ -293,6 +296,14 @@ export async function startForegroundHostServer(options) {
|
|
|
293
296
|
logFilePath,
|
|
294
297
|
now,
|
|
295
298
|
});
|
|
299
|
+
const managedRuntime = createNativeManagedHostRuntime({
|
|
300
|
+
tuttiHome: options.preparation.tutti_home,
|
|
301
|
+
projectId: options.preparation.project_id,
|
|
302
|
+
relayUrl: options.preparation.relay_url,
|
|
303
|
+
workspaceRoot: options.preparation.workspace_root,
|
|
304
|
+
surface: options.preparation.machine_local.binding.surface_profile.kind,
|
|
305
|
+
observe: (event) => controlPlaneLogger.info({ ...event, component: "managed-model-access" }, "Managed model access"),
|
|
306
|
+
});
|
|
296
307
|
const platformOutboundLogger = createHostRuntimeLogger({
|
|
297
308
|
component: "platform-outbound",
|
|
298
309
|
logFilePath,
|
|
@@ -397,27 +408,37 @@ export async function startForegroundHostServer(options) {
|
|
|
397
408
|
readMainlineStatus: () => ({
|
|
398
409
|
mainline_clean: readHostRepoProjection(options.preparation.workspace_root).mainline_clean,
|
|
399
410
|
}),
|
|
400
|
-
resolveRunPipeline: () =>
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
411
|
+
resolveRunPipeline: () => {
|
|
412
|
+
const runtimeOptions = {
|
|
413
|
+
tuttiHome: options.preparation.tutti_home,
|
|
414
|
+
projectId: options.preparation.project_id,
|
|
415
|
+
workspaceRoot: options.preparation.workspace_root,
|
|
416
|
+
agentContext: agentContextRuntime,
|
|
417
|
+
approvalManager,
|
|
418
|
+
logger: runPipelineLogger,
|
|
419
|
+
recordUsage,
|
|
420
|
+
invocationCoordinator: codexInvocationCoordinator,
|
|
421
|
+
checkProcessCoordinator,
|
|
422
|
+
onLifecycleIncident: runLifecycleIncident,
|
|
423
|
+
};
|
|
424
|
+
return managedRuntime === null
|
|
425
|
+
? resolveProjectOpenAiRunPipelineRunner(runtimeOptions)
|
|
426
|
+
: { kind: "ready", runner: createManagedRunPipeline(managedRuntime, runtimeOptions) };
|
|
427
|
+
},
|
|
428
|
+
resolveWorkflowRunner: () => {
|
|
429
|
+
const runtimeOptions = {
|
|
430
|
+
tuttiHome: options.preparation.tutti_home,
|
|
431
|
+
projectId: options.preparation.project_id,
|
|
432
|
+
workspaceRoot: options.preparation.workspace_root,
|
|
433
|
+
agentContext: agentContextRuntime,
|
|
434
|
+
recordUsage,
|
|
435
|
+
invocationCoordinator: codexInvocationCoordinator,
|
|
436
|
+
onLifecycleIncident: procedureLifecycleIncident,
|
|
437
|
+
};
|
|
438
|
+
return managedRuntime === null
|
|
439
|
+
? resolveProjectOpenAiProcedureWorkflowRunner(runtimeOptions)
|
|
440
|
+
: { kind: "ready", runner: createManagedWorkflowRunner(managedRuntime, runtimeOptions) };
|
|
441
|
+
},
|
|
421
442
|
});
|
|
422
443
|
const chatAssistant = new ReadOnlyChatAssistant({
|
|
423
444
|
logger: createHostRuntimeLogger({ component: "chat-assistant", logFilePath, now }),
|
|
@@ -427,7 +448,7 @@ export async function startForegroundHostServer(options) {
|
|
|
427
448
|
workspaceRoot: options.preparation.workspace_root,
|
|
428
449
|
model: {
|
|
429
450
|
answer: async (input) => {
|
|
430
|
-
const
|
|
451
|
+
const runtimeOptions = {
|
|
431
452
|
tuttiHome: options.preparation.tutti_home,
|
|
432
453
|
projectId: options.preparation.project_id,
|
|
433
454
|
workspaceRoot: options.preparation.workspace_root,
|
|
@@ -435,7 +456,13 @@ export async function startForegroundHostServer(options) {
|
|
|
435
456
|
recordUsage,
|
|
436
457
|
invocationCoordinator: codexInvocationCoordinator,
|
|
437
458
|
onLifecycleIncident: chatLifecycleIncident,
|
|
438
|
-
}
|
|
459
|
+
};
|
|
460
|
+
const resolution = managedRuntime === null
|
|
461
|
+
? resolveProjectOpenAiReadOnlyChatAssistantModel(runtimeOptions)
|
|
462
|
+
: {
|
|
463
|
+
kind: "ready",
|
|
464
|
+
model: createManagedChatModel(managedRuntime, runtimeOptions),
|
|
465
|
+
};
|
|
439
466
|
if (resolution.kind !== "ready") {
|
|
440
467
|
throw new Error("Provider is not configured.");
|
|
441
468
|
}
|
|
@@ -488,6 +515,9 @@ export async function startForegroundHostServer(options) {
|
|
|
488
515
|
providerConfig: {
|
|
489
516
|
get: getProviderConfig,
|
|
490
517
|
configure: async (input) => {
|
|
518
|
+
if (managedRuntime !== null) {
|
|
519
|
+
throw new HostLocalProviderConfigureError("managed_model_fixed", "Managed project access cannot be replaced through BYOK setup.");
|
|
520
|
+
}
|
|
491
521
|
const result = await configureProjectOpenAiProvider({
|
|
492
522
|
tuttiHome: options.preparation.tutti_home,
|
|
493
523
|
projectId: options.preparation.project_id,
|
|
@@ -597,11 +627,18 @@ export async function startForegroundHostServer(options) {
|
|
|
597
627
|
chatAssistant,
|
|
598
628
|
tuttiHome: options.preparation.tutti_home,
|
|
599
629
|
getProviderConfig,
|
|
600
|
-
providerModel:
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
630
|
+
providerModel: managedRuntime === null
|
|
631
|
+
? createHostProviderModelService({
|
|
632
|
+
tuttiHome: options.preparation.tutti_home,
|
|
633
|
+
projectId: options.preparation.project_id,
|
|
634
|
+
now,
|
|
635
|
+
})
|
|
636
|
+
: {
|
|
637
|
+
discover: () => Promise.resolve({ status: "unavailable", reason: "managed_model_fixed" }),
|
|
638
|
+
update: () => Promise.resolve({
|
|
639
|
+
disposition: { kind: "provider_unavailable", reason: "managed_model_fixed" },
|
|
640
|
+
}),
|
|
641
|
+
},
|
|
605
642
|
getProviderUsage: () => readProviderUsageProjection(store.db),
|
|
606
643
|
updateProjectDisplayName: (displayName) => {
|
|
607
644
|
updateRuntimeProjectDisplayName(displayName);
|
|
@@ -613,6 +650,7 @@ export async function startForegroundHostServer(options) {
|
|
|
613
650
|
capabilities: {
|
|
614
651
|
clarification: true,
|
|
615
652
|
repo_viewer: true,
|
|
653
|
+
provider_model_update: managedRuntime === null,
|
|
616
654
|
},
|
|
617
655
|
getRepo: () => readHostRepoProjection(options.preparation.workspace_root),
|
|
618
656
|
workspaceOps: {
|
|
@@ -657,10 +695,20 @@ export async function startForegroundHostServer(options) {
|
|
|
657
695
|
store.close();
|
|
658
696
|
throw error;
|
|
659
697
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
698
|
+
let recoveryStarted = false;
|
|
699
|
+
const startRecovery = () => {
|
|
700
|
+
if (recoveryStarted)
|
|
701
|
+
return;
|
|
702
|
+
recoveryStarted = true;
|
|
703
|
+
controlPlane.runStartupRecovery({
|
|
704
|
+
workspaceRoot: options.preparation.workspace_root,
|
|
705
|
+
recoverInProgressCommands: true,
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
// Managed creation / reauthorization activates during Relay registration. Do
|
|
709
|
+
// not recover a stored task against a not-yet-activated native delegation.
|
|
710
|
+
if (managedRuntime === null)
|
|
711
|
+
startRecovery();
|
|
664
712
|
let resolveClosed;
|
|
665
713
|
let rejectClosed;
|
|
666
714
|
const closed = new Promise((resolve, reject) => {
|
|
@@ -708,6 +756,7 @@ export async function startForegroundHostServer(options) {
|
|
|
708
756
|
};
|
|
709
757
|
return {
|
|
710
758
|
app,
|
|
759
|
+
startRecovery,
|
|
711
760
|
store,
|
|
712
761
|
log_file_path: logFilePath,
|
|
713
762
|
runtime_endpoint: runtimeEndpoint,
|
|
@@ -115,7 +115,7 @@ export function renderFeishuCompletionPage(options) {
|
|
|
115
115
|
].join("\n");
|
|
116
116
|
}
|
|
117
117
|
async function ensureProviderConfigured(preparation) {
|
|
118
|
-
if (preparation.provider_status === "configured") {
|
|
118
|
+
if (preparation.provider_status === "configured" || preparation.provider_status === "managed") {
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
121
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ProjectSurface } from "@tutti/shared/domain";
|
|
2
2
|
import { type ProjectId } from "@tutti/shared/ids";
|
|
3
|
-
import {
|
|
3
|
+
import type { ProviderConfigProjection } from "@tutti/shared/schemas/api";
|
|
4
4
|
import { type GitBootstrapResult, type LaunchConfirmationHandler } from "./git-bootstrap.js";
|
|
5
5
|
import { type MachineProjectBindingResult } from "./machine-local.js";
|
|
6
6
|
import { type ProjectIdentityReadResult } from "./project-identity.js";
|
|
@@ -15,8 +15,12 @@ export type LaunchPreparationOptions = {
|
|
|
15
15
|
projectDisplayName?: string;
|
|
16
16
|
projectDescription?: string;
|
|
17
17
|
projectSurface?: ProjectSurface;
|
|
18
|
+
reservedIdentity?: {
|
|
19
|
+
projectId: ProjectId;
|
|
20
|
+
hostRegistrationSecret: string;
|
|
21
|
+
};
|
|
18
22
|
};
|
|
19
|
-
export type LaunchProviderStatus =
|
|
23
|
+
export type LaunchProviderStatus = ProviderConfigProjection["status"];
|
|
20
24
|
export type LaunchPreparationResult = {
|
|
21
25
|
status: "prepared";
|
|
22
26
|
workspace_root: string;
|
|
@@ -26,7 +30,7 @@ export type LaunchPreparationResult = {
|
|
|
26
30
|
relay_url: string;
|
|
27
31
|
local_diagnostic_endpoint: string;
|
|
28
32
|
provider_status: LaunchProviderStatus;
|
|
29
|
-
provider_config:
|
|
33
|
+
provider_config: ProviderConfigProjection;
|
|
30
34
|
project_identity_created: boolean;
|
|
31
35
|
git: GitBootstrapResult;
|
|
32
36
|
machine_local: MachineProjectBindingResult;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { basename, resolve } from "node:path";
|
|
2
2
|
import { createProjectId } from "@tutti/shared/ids";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveTuttiHome } from "../../providers/openai/index.js";
|
|
4
|
+
import { readProjectModelAccess } from "../managed-access/project-provider.js";
|
|
4
5
|
import { LaunchError } from "./errors.js";
|
|
5
6
|
import { assertSafeProjectRoot, ensureGitBootstrap, } from "./git-bootstrap.js";
|
|
6
|
-
import { ensureMachineProjectBinding } from "./machine-local.js";
|
|
7
|
+
import { assertReservedMachineProjectIdentity, ensureMachineProjectBinding, } from "./machine-local.js";
|
|
7
8
|
import { readProjectIdentity, readProjectDisplayName, writeProjectDescription, writeProjectDisplayName, writeProjectIdentity, } from "./project-identity.js";
|
|
8
9
|
const DEFAULT_RELAY_URL = "https://tutti.now";
|
|
9
10
|
const DEFAULT_HOST_ENDPOINT_HOST = "127.0.0.1";
|
|
@@ -50,7 +51,21 @@ export async function prepareLaunchProject(options) {
|
|
|
50
51
|
const relayUrl = context.relay_url;
|
|
51
52
|
const localDiagnosticEndpoint = context.local_diagnostic_endpoint.base_url;
|
|
52
53
|
const existingIdentity = context.existing_identity;
|
|
53
|
-
const projectId = existingIdentity.kind === "present"
|
|
54
|
+
const projectId = existingIdentity.kind === "present"
|
|
55
|
+
? existingIdentity.project_id
|
|
56
|
+
: (options.reservedIdentity?.projectId ?? createProjectId());
|
|
57
|
+
if (options.reservedIdentity !== undefined) {
|
|
58
|
+
if (projectId !== options.reservedIdentity.projectId)
|
|
59
|
+
throw new LaunchError("project_identity_conflict", "This directory belongs to a different project", "Use the directory approved in the native creation request.");
|
|
60
|
+
assertReservedMachineProjectIdentity({
|
|
61
|
+
tuttiHome,
|
|
62
|
+
projectId,
|
|
63
|
+
workspaceRoot,
|
|
64
|
+
relayUrl,
|
|
65
|
+
surface: options.projectSurface ?? "standalone",
|
|
66
|
+
hostSecret: options.reservedIdentity.hostRegistrationSecret,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
54
69
|
const projectIdentityCreated = existingIdentity.kind === "missing";
|
|
55
70
|
const gitOptions = {
|
|
56
71
|
workspaceRoot,
|
|
@@ -89,10 +104,14 @@ export async function prepareLaunchProject(options) {
|
|
|
89
104
|
if (options.projectSurface !== undefined) {
|
|
90
105
|
bindingOptions.surface = options.projectSurface;
|
|
91
106
|
}
|
|
107
|
+
if (options.reservedIdentity !== undefined) {
|
|
108
|
+
bindingOptions.reservedHostSecret = options.reservedIdentity.hostRegistrationSecret;
|
|
109
|
+
}
|
|
92
110
|
const machineLocal = ensureMachineProjectBinding(bindingOptions);
|
|
93
|
-
const providerConfig =
|
|
111
|
+
const providerConfig = readProjectModelAccess({
|
|
94
112
|
tuttiHome,
|
|
95
113
|
projectId,
|
|
114
|
+
relayUrl,
|
|
96
115
|
});
|
|
97
116
|
return {
|
|
98
117
|
status: "prepared",
|
|
@@ -41,6 +41,7 @@ export type EnsureMachineProjectBindingOptions = {
|
|
|
41
41
|
displayNameSnapshot?: string;
|
|
42
42
|
relayUrl?: string;
|
|
43
43
|
surface?: ProjectSurface;
|
|
44
|
+
reservedHostSecret?: string;
|
|
44
45
|
allowWorkspaceRootTakeover?: boolean;
|
|
45
46
|
now?: () => Date;
|
|
46
47
|
};
|
|
@@ -103,5 +104,13 @@ export declare function updateMachineProjectDisplayNameSnapshot(options: {
|
|
|
103
104
|
displayName: string;
|
|
104
105
|
now?: () => Date;
|
|
105
106
|
}): MachineProjectBindingRecord;
|
|
107
|
+
export declare function assertReservedMachineProjectIdentity(options: {
|
|
108
|
+
tuttiHome: string;
|
|
109
|
+
projectId: ProjectId;
|
|
110
|
+
workspaceRoot: string;
|
|
111
|
+
relayUrl: string;
|
|
112
|
+
surface: ProjectSurface;
|
|
113
|
+
hostSecret: string;
|
|
114
|
+
}): void;
|
|
106
115
|
export declare function ensureMachineProjectBinding(options: EnsureMachineProjectBindingOptions): MachineProjectBindingResult;
|
|
107
116
|
//# sourceMappingURL=machine-local.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { createProjectSurfaceProfile, isProjectSurfaceProfile, } from "@tutti/shared/domain";
|
|
5
5
|
import { ID_PREFIXES, isPrefixedId } from "@tutti/shared/ids";
|
|
@@ -286,33 +286,76 @@ export function updateMachineProjectDisplayNameSnapshot(options) {
|
|
|
286
286
|
writePrivateJsonFile(getMachineProjectBindingPath(options.tuttiHome, options.projectId), binding);
|
|
287
287
|
return binding;
|
|
288
288
|
}
|
|
289
|
-
function ensureHostRegistrationSecret(tuttiHome, projectId, now) {
|
|
289
|
+
function ensureHostRegistrationSecret(tuttiHome, projectId, now, reservedSecret) {
|
|
290
290
|
const secretPath = getHostRegistrationSecretPath(tuttiHome, projectId);
|
|
291
291
|
const ref = createHostRegistrationSecretRef(projectId);
|
|
292
292
|
if (existsSync(secretPath)) {
|
|
293
|
+
const existing = readHostRegistrationSecret(tuttiHome, projectId);
|
|
294
|
+
if (reservedSecret !== undefined && existing.secret !== reservedSecret)
|
|
295
|
+
throw reservedIdentityConflict();
|
|
293
296
|
return {
|
|
294
297
|
ref,
|
|
295
298
|
created: false,
|
|
296
|
-
createdAt:
|
|
299
|
+
createdAt: existing.created_at,
|
|
297
300
|
};
|
|
298
301
|
}
|
|
299
302
|
const timestamp = now().toISOString();
|
|
300
303
|
const secretFile = {
|
|
301
304
|
kind: "host_registration_secret_v1",
|
|
302
305
|
project_id: projectId,
|
|
303
|
-
secret: createHostRegistrationSecret(),
|
|
306
|
+
secret: reservedSecret ?? createHostRegistrationSecret(),
|
|
304
307
|
created_at: timestamp,
|
|
305
308
|
updated_at: timestamp,
|
|
306
309
|
};
|
|
307
|
-
|
|
310
|
+
// Publish a complete file exclusively; racing creation must never replace a proof
|
|
311
|
+
// already reserved or registered by another process.
|
|
312
|
+
ensurePrivateDirectory(dirname(secretPath));
|
|
313
|
+
const temporary = `${secretPath}.${randomBytes(12).toString("hex")}.tmp`;
|
|
314
|
+
const fd = openSync(temporary, "wx", 0o600);
|
|
315
|
+
try {
|
|
316
|
+
writeFileSync(fd, `${JSON.stringify(secretFile)}\n`, "utf8");
|
|
317
|
+
fsyncSync(fd);
|
|
318
|
+
try {
|
|
319
|
+
linkSync(temporary, secretPath);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (error.code !== "EEXIST")
|
|
323
|
+
throw error;
|
|
324
|
+
const existing = readHostRegistrationSecret(tuttiHome, projectId);
|
|
325
|
+
if (reservedSecret !== undefined && existing.secret !== reservedSecret)
|
|
326
|
+
throw reservedIdentityConflict();
|
|
327
|
+
return { ref, created: false, createdAt: existing.created_at };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
closeSync(fd);
|
|
332
|
+
unlinkSync(temporary);
|
|
333
|
+
}
|
|
308
334
|
return { ref, created: true, createdAt: timestamp };
|
|
309
335
|
}
|
|
336
|
+
function reservedIdentityConflict() {
|
|
337
|
+
return new LaunchError("project_identity_conflict", "The native creation reservation conflicts with this project's local identity", "Keep the existing project unchanged and reconcile its native creation record.");
|
|
338
|
+
}
|
|
339
|
+
// Read-only preflight, before any Git bootstrap, adoption or credential write.
|
|
340
|
+
export function assertReservedMachineProjectIdentity(options) {
|
|
341
|
+
if (!isPrefixedId(options.projectId, ID_PREFIXES.project) ||
|
|
342
|
+
!/^[A-Za-z0-9_-]{43}$/u.test(options.hostSecret))
|
|
343
|
+
throw reservedIdentityConflict();
|
|
344
|
+
const binding = readMachineProjectBinding(options.tuttiHome, options.projectId);
|
|
345
|
+
if (binding !== null &&
|
|
346
|
+
(binding.workspace_root !== resolve(options.workspaceRoot) ||
|
|
347
|
+
binding.surface_profile.kind !== options.surface ||
|
|
348
|
+
binding.relay_url !== options.relayUrl))
|
|
349
|
+
throw reservedIdentityConflict();
|
|
350
|
+
if (existsSync(getHostRegistrationSecretPath(options.tuttiHome, options.projectId)) &&
|
|
351
|
+
readHostRegistrationSecret(options.tuttiHome, options.projectId).secret !== options.hostSecret)
|
|
352
|
+
throw reservedIdentityConflict();
|
|
353
|
+
}
|
|
310
354
|
export function ensureMachineProjectBinding(options) {
|
|
311
355
|
const now = options.now ?? (() => new Date());
|
|
312
356
|
const workspaceRoot = resolve(options.workspaceRoot);
|
|
313
357
|
const localStoreRoot = getProjectLocalStoreRoot(options.tuttiHome, options.projectId);
|
|
314
358
|
const bindingPath = getMachineProjectBindingPath(options.tuttiHome, options.projectId);
|
|
315
|
-
const secret = ensureHostRegistrationSecret(options.tuttiHome, options.projectId, now);
|
|
316
359
|
const existing = readMachineProjectBinding(options.tuttiHome, options.projectId);
|
|
317
360
|
const requestedSurface = options.surface ?? existing?.surface_profile.kind ?? "standalone";
|
|
318
361
|
if (existing !== null && existing.surface_profile.kind !== requestedSurface) {
|
|
@@ -323,6 +366,17 @@ export function ensureMachineProjectBinding(options) {
|
|
|
323
366
|
options.allowWorkspaceRootTakeover !== true) {
|
|
324
367
|
throw new LaunchError("project_identity_conflict", "This project is already bound to a different workspace root on this machine", "Use the future takeover flow, or remove the stale binding only after verifying no host server is running.");
|
|
325
368
|
}
|
|
369
|
+
if (options.reservedHostSecret !== undefined) {
|
|
370
|
+
assertReservedMachineProjectIdentity({
|
|
371
|
+
tuttiHome: options.tuttiHome,
|
|
372
|
+
projectId: options.projectId,
|
|
373
|
+
workspaceRoot,
|
|
374
|
+
relayUrl: options.relayUrl ?? "",
|
|
375
|
+
surface: requestedSurface,
|
|
376
|
+
hostSecret: options.reservedHostSecret,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const secret = ensureHostRegistrationSecret(options.tuttiHome, options.projectId, now, options.reservedHostSecret);
|
|
326
380
|
const binding = {
|
|
327
381
|
project_id: options.projectId,
|
|
328
382
|
surface_profile: createProjectSurfaceProfile(requestedSurface),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ProjectId } from "@tutti/shared/ids";
|
|
2
|
-
import {
|
|
2
|
+
import type { ProviderConfigProjection } from "@tutti/shared/schemas/api";
|
|
3
3
|
import { type MachineProjectBindingRecord, type MachineRuntimeEndpointRecord } from "./machine-local.js";
|
|
4
4
|
export type ExistingProjectContext = {
|
|
5
5
|
workspace_root: string;
|
|
@@ -8,7 +8,7 @@ export type ExistingProjectContext = {
|
|
|
8
8
|
display_name: string;
|
|
9
9
|
binding: MachineProjectBindingRecord | null;
|
|
10
10
|
runtime_endpoint: MachineRuntimeEndpointRecord | null;
|
|
11
|
-
provider_config:
|
|
11
|
+
provider_config: ProviderConfigProjection;
|
|
12
12
|
provider_base_url?: string;
|
|
13
13
|
};
|
|
14
14
|
export type ManagedProjectContext = ExistingProjectContext & {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
import { ID_PREFIXES, isPrefixedId } from "@tutti/shared/ids";
|
|
4
|
-
import {
|
|
4
|
+
import { readProjectOpenAiProviderConfig, resolveTuttiHome } from "../../providers/openai/index.js";
|
|
5
|
+
import { readProjectModelAccess } from "../managed-access/project-provider.js";
|
|
5
6
|
import { LaunchError } from "./errors.js";
|
|
6
7
|
import { assertSafeProjectRoot } from "./git-bootstrap.js";
|
|
7
8
|
import { inspectMachineProjectInventory, inspectMachineProjectInventories, } from "./machine-project-inspector.js";
|
|
@@ -157,9 +158,10 @@ export function resolveExistingProjectContext(options) {
|
|
|
157
158
|
display_name: readProjectDisplayName(workspaceRoot) ?? identity.project_id,
|
|
158
159
|
binding,
|
|
159
160
|
runtime_endpoint: readMachineRuntimeEndpoint(tuttiHome, identity.project_id),
|
|
160
|
-
provider_config:
|
|
161
|
+
provider_config: readProjectModelAccess({
|
|
161
162
|
tuttiHome,
|
|
162
163
|
projectId: identity.project_id,
|
|
164
|
+
relayUrl: binding?.relay_url ?? env.TUTTI_RELAY_URL ?? "https://tutti.now",
|
|
163
165
|
}),
|
|
164
166
|
...(projectConfig.kind === "ok" && projectConfig.config.api_base_url !== undefined
|
|
165
167
|
? { provider_base_url: projectConfig.config.api_base_url }
|
|
@@ -197,9 +199,10 @@ export function resolveManagedProjectContext(options) {
|
|
|
197
199
|
display_name: entry.display_name,
|
|
198
200
|
binding,
|
|
199
201
|
runtime_endpoint: runtimeEndpoint,
|
|
200
|
-
provider_config:
|
|
202
|
+
provider_config: readProjectModelAccess({
|
|
201
203
|
tuttiHome,
|
|
202
204
|
projectId: entry.project_id,
|
|
205
|
+
relayUrl: binding?.relay_url ?? env.TUTTI_RELAY_URL ?? "https://tutti.now",
|
|
203
206
|
}),
|
|
204
207
|
...(projectConfig.kind === "ok" && projectConfig.config.api_base_url !== undefined
|
|
205
208
|
? { provider_base_url: projectConfig.config.api_base_url }
|
|
@@ -4,6 +4,10 @@ import { createIdempotencyKey, } from "@tutti/shared/ids";
|
|
|
4
4
|
import { deleteFeishuOwnerAccountHandoff, readFeishuOwnerAccountHandoff, readHostRegistrationSecret, updateMachineProjectRelayBinding, } from "./machine-local.js";
|
|
5
5
|
import { LaunchError } from "./errors.js";
|
|
6
6
|
import { readProjectDisplayName } from "./project-identity.js";
|
|
7
|
+
import { findNativeManagedProject } from "../managed-access/state.js";
|
|
8
|
+
import { realpathSync } from "node:fs";
|
|
9
|
+
import { ManagedLocalStateError } from "../managed-access/private-store.js";
|
|
10
|
+
import { reconcileNativePreparation } from "../managed-access/transport.js";
|
|
7
11
|
export function relayErrorToLaunchError(error, relayUrl) {
|
|
8
12
|
if (error instanceof LaunchError) {
|
|
9
13
|
return error;
|
|
@@ -25,6 +29,28 @@ export async function registerHostWithRelay(options) {
|
|
|
25
29
|
const hostSecret = readHostRegistrationSecret(options.preparation.tutti_home, options.preparation.project_id);
|
|
26
30
|
const surfaceProfile = options.preparation.machine_local?.binding.surface_profile ??
|
|
27
31
|
{ kind: "standalone", version: 1 };
|
|
32
|
+
const managed = findNativeManagedProject({
|
|
33
|
+
tuttiHome: options.preparation.tutti_home,
|
|
34
|
+
relayUrl: options.preparation.relay_url,
|
|
35
|
+
projectId: options.preparation.project_id,
|
|
36
|
+
});
|
|
37
|
+
if (managed !== null &&
|
|
38
|
+
(managed.journal.workspace_root !== realpathSync(options.preparation.workspace_root) ||
|
|
39
|
+
managed.journal.surface !== surfaceProfile.kind ||
|
|
40
|
+
managed.journal.host_registration_secret !== hostSecret.secret))
|
|
41
|
+
throw new ManagedLocalStateError("managed_registration_mismatch");
|
|
42
|
+
// A lost registration response is reconciled independently before retry. Once
|
|
43
|
+
// activated, ordinary collaboration reconnect does not require browser consent.
|
|
44
|
+
if (managed !== null && managed.journal.state !== "registered") {
|
|
45
|
+
await reconcileNativePreparation({
|
|
46
|
+
state: managed.state,
|
|
47
|
+
requestRef: managed.journal.request_ref,
|
|
48
|
+
fetchImpl: (input, init) => options.fetchImpl(input instanceof Request ? input.url : input, init),
|
|
49
|
+
now: options.now?.() ?? new Date(),
|
|
50
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const pendingManaged = managed !== null && managed.state.read(managed.journal.request_ref).state !== "registered";
|
|
28
54
|
const registrationOptions = {
|
|
29
55
|
relayUrl: options.preparation.relay_url,
|
|
30
56
|
projectId: options.preparation.project_id,
|
|
@@ -50,17 +76,25 @@ export async function registerHostWithRelay(options) {
|
|
|
50
76
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
51
77
|
...(options.now === undefined ? {} : { issuedAt: options.now() }),
|
|
52
78
|
};
|
|
53
|
-
const accountHandoff = surfaceProfile.kind === "feishu"
|
|
79
|
+
const accountHandoff = surfaceProfile.kind === "feishu" && managed === null
|
|
54
80
|
? readFeishuOwnerAccountHandoff(options.preparation.tutti_home, options.preparation.project_id, options.now?.() ?? new Date())
|
|
55
81
|
: null;
|
|
56
82
|
if (accountHandoff !== null) {
|
|
57
83
|
registrationOptions.accountHandoffToken = accountHandoff.token;
|
|
58
84
|
}
|
|
85
|
+
if (pendingManaged && managed !== null) {
|
|
86
|
+
registrationOptions.managedBootstrap = managed.state.bootstrap(managed.journal.request_ref, options.now?.() ?? new Date());
|
|
87
|
+
}
|
|
59
88
|
const response = await options.registerRelayHostConnection(registrationOptions);
|
|
60
89
|
if (response.disposition.kind !== "registered" || response.result === undefined) {
|
|
61
90
|
throw new LaunchError("relay_registration_failed", "Relay host registration did not complete", "Retry `tutti launch` with a new registration command.");
|
|
62
91
|
}
|
|
63
92
|
const result = response.result;
|
|
93
|
+
if (pendingManaged && managed !== null) {
|
|
94
|
+
if (result.managed_authority === undefined ||
|
|
95
|
+
result.managed_authority.relay_project_ref !== result.relay_project.relay_project_ref)
|
|
96
|
+
throw new ManagedLocalStateError("managed_registration_mismatch");
|
|
97
|
+
}
|
|
64
98
|
if ((result.relay_project.surface_profile?.kind ?? "standalone") !== surfaceProfile.kind) {
|
|
65
99
|
throw new LaunchError("project_surface_conflict", "Relay returned a different project collaboration surface", "Keep the Host stopped and inspect the machine project binding and Relay project metadata.");
|
|
66
100
|
}
|
|
@@ -69,6 +103,9 @@ export async function registerHostWithRelay(options) {
|
|
|
69
103
|
(result.join_token?.visibility !== "visible_once" || result.join_token.join_url === undefined)) {
|
|
70
104
|
throw new LaunchError("relay_registration_failed", "Relay did not return a visible join URL for this launch", "Retry `tutti launch` so the host can rotate a fresh visible-once join URL.");
|
|
71
105
|
}
|
|
106
|
+
if (pendingManaged && managed !== null && result.managed_authority !== undefined) {
|
|
107
|
+
managed.state.registered(managed.journal.request_ref, result.managed_authority);
|
|
108
|
+
}
|
|
72
109
|
updateMachineProjectRelayBinding({
|
|
73
110
|
tuttiHome: options.preparation.tutti_home,
|
|
74
111
|
projectId: options.preparation.project_id,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FastifyInstance } from "fastify";
|
|
2
|
-
import type
|
|
2
|
+
import { type ProviderConfigProjection } from "@tutti/shared/schemas/api";
|
|
3
3
|
import type { ProjectId } from "@tutti/shared/ids";
|
|
4
4
|
import { type FeishuSetupProjection } from "@tutti/shared/schemas/internal";
|
|
5
5
|
export type HostLocalControlOptions = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { ProviderConfigProjectionSchema, } from "@tutti/shared/schemas/api";
|
|
2
3
|
import { FeishuSetupProjectionSchema, } from "@tutti/shared/schemas/internal";
|
|
3
4
|
class HostLocalControlUnauthorizedError extends Error {
|
|
4
5
|
statusCode = 401;
|
|
@@ -90,52 +91,7 @@ const HostLocalProjectResponseSchema = {
|
|
|
90
91
|
display_name: { type: "string", minLength: 1, maxLength: 80 },
|
|
91
92
|
},
|
|
92
93
|
};
|
|
93
|
-
const HostLocalProviderConfigResponseSchema =
|
|
94
|
-
oneOf: [
|
|
95
|
-
{
|
|
96
|
-
type: "object",
|
|
97
|
-
required: ["status"],
|
|
98
|
-
additionalProperties: false,
|
|
99
|
-
properties: {
|
|
100
|
-
status: { type: "string", enum: ["not_configured"] },
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
type: "object",
|
|
105
|
-
required: [
|
|
106
|
-
"status",
|
|
107
|
-
"provider",
|
|
108
|
-
"auth_kind",
|
|
109
|
-
"redacted_key",
|
|
110
|
-
"default_model",
|
|
111
|
-
"validated_at",
|
|
112
|
-
],
|
|
113
|
-
additionalProperties: false,
|
|
114
|
-
properties: {
|
|
115
|
-
status: { type: "string", enum: ["configured"] },
|
|
116
|
-
provider: { type: "string", enum: ["openai"] },
|
|
117
|
-
auth_kind: { type: "string", enum: ["api_key"] },
|
|
118
|
-
redacted_key: { type: "string" },
|
|
119
|
-
default_model: { type: "string" },
|
|
120
|
-
validated_at: { type: "string" },
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
type: "object",
|
|
125
|
-
required: ["status"],
|
|
126
|
-
additionalProperties: false,
|
|
127
|
-
properties: {
|
|
128
|
-
status: { type: "string", enum: ["invalid"] },
|
|
129
|
-
provider: { type: "string", enum: ["openai"] },
|
|
130
|
-
auth_kind: { type: "string", enum: ["api_key"] },
|
|
131
|
-
redacted_key: { type: "string" },
|
|
132
|
-
default_model: { type: "string" },
|
|
133
|
-
validated_at: { type: "string" },
|
|
134
|
-
invalid_reason_code: { type: "string" },
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
};
|
|
94
|
+
const HostLocalProviderConfigResponseSchema = ProviderConfigProjectionSchema;
|
|
139
95
|
const HostLocalLaunchStatusResponseSchema = {
|
|
140
96
|
type: "object",
|
|
141
97
|
additionalProperties: false,
|