@xfey/tutti 0.1.119 → 0.1.120

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.
Files changed (110) hide show
  1. package/dist/approvals/decision-command.d.ts +2 -2
  2. package/dist/collaboration-state/clarification-snapshot.d.ts +9 -9
  3. package/dist/collaboration-state/task-compile-context.d.ts +1 -1
  4. package/dist/control-plane/workflows/openai.d.ts +3 -4
  5. package/dist/control-plane/workflows/openai.js +3 -2
  6. package/dist/providers/openai/app-server/invocation-coordinator.d.ts +1 -0
  7. package/dist/providers/openai/app-server/invocation-coordinator.js +4 -1
  8. package/dist/providers/openai/app-server/invocation-deadline.d.ts +3 -0
  9. package/dist/providers/openai/app-server/invocation-deadline.js +15 -0
  10. package/dist/providers/openai/app-server/read-only-procedure.d.ts +4 -2
  11. package/dist/providers/openai/app-server/read-only-procedure.js +16 -0
  12. package/dist/providers/openai/app-server/workspace-write-run.d.ts +4 -2
  13. package/dist/providers/openai/app-server/workspace-write-run.js +16 -0
  14. package/dist/providers/openai/chat-assistant.d.ts +3 -4
  15. package/dist/providers/openai/chat-assistant.js +2 -1
  16. package/dist/providers/openai/execution-access.d.ts +17 -0
  17. package/dist/providers/openai/execution-access.js +13 -0
  18. package/dist/providers/openai/sdk-procedure-runner.d.ts +2 -2
  19. package/dist/providers/openai/sdk-procedure-runner.js +17 -0
  20. package/dist/run-pipeline/artifact-applicability.d.ts +3 -4
  21. package/dist/run-pipeline/artifact-applicability.js +2 -1
  22. package/dist/run-pipeline/openai.d.ts +3 -4
  23. package/dist/run-pipeline/openai.js +3 -2
  24. package/dist/run-pipeline/task-run-invocation.js +6 -12
  25. package/dist/server-shell/cli/cli.js +3 -0
  26. package/dist/server-shell/cli/host-lifecycle.js +1 -0
  27. package/dist/server-shell/cli/host-server-runtime.d.ts +1 -0
  28. package/dist/server-shell/cli/host-server-runtime.js +83 -34
  29. package/dist/server-shell/cli/launch-command.js +1 -1
  30. package/dist/server-shell/cli/launch.d.ts +7 -3
  31. package/dist/server-shell/cli/launch.js +23 -4
  32. package/dist/server-shell/cli/machine-local.d.ts +9 -0
  33. package/dist/server-shell/cli/machine-local.js +60 -6
  34. package/dist/server-shell/cli/project-resolver.d.ts +2 -2
  35. package/dist/server-shell/cli/project-resolver.js +6 -3
  36. package/dist/server-shell/cli/relay-registration.js +38 -1
  37. package/dist/server-shell/http/routes/local-control.d.ts +1 -1
  38. package/dist/server-shell/http/routes/local-control.js +2 -46
  39. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +47 -137
  40. package/dist/server-shell/http/routes/project-api/openapi.d.ts +47 -137
  41. package/dist/server-shell/http/routes/project-api/schemas.d.ts +25 -136
  42. package/dist/server-shell/http/routes/project-api/schemas.js +4 -58
  43. package/dist/server-shell/http/routes/project-api/types.d.ts +4 -0
  44. package/dist/server-shell/local-console/managed-project-routes.d.ts +9 -0
  45. package/dist/server-shell/local-console/managed-project-routes.js +120 -0
  46. package/dist/server-shell/local-console/managed-project-service.d.ts +151 -0
  47. package/dist/server-shell/local-console/managed-project-service.js +163 -0
  48. package/dist/server-shell/local-console/operation-coordinator.d.ts +1 -1
  49. package/dist/server-shell/local-console/package-update-hosts.js +2 -1
  50. package/dist/server-shell/local-console/project-service.d.ts +5 -1
  51. package/dist/server-shell/local-console/project-service.js +63 -14
  52. package/dist/server-shell/local-console/server.js +22 -2
  53. package/dist/server-shell/managed-access/execution-binding.d.ts +9 -0
  54. package/dist/server-shell/managed-access/execution-binding.js +22 -0
  55. package/dist/server-shell/managed-access/existing-project.d.ts +15 -0
  56. package/dist/server-shell/managed-access/existing-project.js +41 -0
  57. package/dist/server-shell/managed-access/model-control-client.d.ts +66 -0
  58. package/dist/server-shell/managed-access/model-control-client.js +114 -0
  59. package/dist/server-shell/managed-access/private-store.d.ts +19 -0
  60. package/dist/server-shell/managed-access/private-store.js +275 -0
  61. package/dist/server-shell/managed-access/project-provider.d.ts +18 -0
  62. package/dist/server-shell/managed-access/project-provider.js +30 -0
  63. package/dist/server-shell/managed-access/records.d.ts +108 -0
  64. package/dist/server-shell/managed-access/records.js +73 -0
  65. package/dist/server-shell/managed-access/runners.d.ts +11 -0
  66. package/dist/server-shell/managed-access/runners.js +103 -0
  67. package/dist/server-shell/managed-access/runtime-records.d.ts +110 -0
  68. package/dist/server-shell/managed-access/runtime-records.js +65 -0
  69. package/dist/server-shell/managed-access/runtime.d.ts +48 -0
  70. package/dist/server-shell/managed-access/runtime.js +301 -0
  71. package/dist/server-shell/managed-access/state.d.ts +54 -0
  72. package/dist/server-shell/managed-access/state.js +287 -0
  73. package/dist/server-shell/managed-access/transport.d.ts +48 -0
  74. package/dist/server-shell/managed-access/transport.js +38 -0
  75. package/dist/server-shell/managed-access/workspace.d.ts +10 -0
  76. package/dist/server-shell/managed-access/workspace.js +69 -0
  77. package/dist/server-shell/smoke/e2e-client.d.ts +8 -3
  78. package/dist/server-shell/smoke/e2e-client.js +11 -11
  79. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +4 -1
  80. package/node_modules/@tutti/relay-client/dist/host-control.js +26 -3
  81. package/node_modules/@tutti/relay-client/dist/index.d.ts +1 -0
  82. package/node_modules/@tutti/relay-client/dist/index.js +1 -0
  83. package/node_modules/@tutti/relay-client/dist/project-authority.d.ts +75 -0
  84. package/node_modules/@tutti/relay-client/dist/project-authority.js +140 -0
  85. package/node_modules/@tutti/shared/dist/ids/index.d.ts +7 -0
  86. package/node_modules/@tutti/shared/dist/ids/index.js +7 -0
  87. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -0
  88. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -0
  89. package/node_modules/@tutti/shared/dist/schemas/api/managed-account.d.ts +35 -0
  90. package/node_modules/@tutti/shared/dist/schemas/api/managed-account.js +41 -0
  91. package/node_modules/@tutti/shared/dist/schemas/api/provider-config.d.ts +8 -4
  92. package/node_modules/@tutti/shared/dist/schemas/api/provider-config.js +6 -0
  93. package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +4 -0
  94. package/node_modules/@tutti/shared/dist/schemas/internal/creation-validation.d.ts +28 -0
  95. package/node_modules/@tutti/shared/dist/schemas/internal/creation-validation.js +42 -0
  96. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +5 -0
  97. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +5 -0
  98. package/node_modules/@tutti/shared/dist/schemas/internal/managed-account.d.ts +12 -0
  99. package/node_modules/@tutti/shared/dist/schemas/internal/managed-account.js +13 -0
  100. package/node_modules/@tutti/shared/dist/schemas/internal/managed-models.d.ts +17 -0
  101. package/node_modules/@tutti/shared/dist/schemas/internal/managed-models.js +17 -0
  102. package/node_modules/@tutti/shared/dist/schemas/internal/managed-runtime.d.ts +116 -0
  103. package/node_modules/@tutti/shared/dist/schemas/internal/managed-runtime.js +87 -0
  104. package/node_modules/@tutti/shared/dist/schemas/internal/project-authority.d.ts +120 -0
  105. package/node_modules/@tutti/shared/dist/schemas/internal/project-authority.js +110 -0
  106. package/package.json +1 -1
  107. package/web/assets/{homepage-motion-scene-B6lCCHBq.js → homepage-motion-scene-DyMFsYmT.js} +1 -1
  108. package/web/assets/index-DSlecDXC.js +70 -0
  109. package/web/index.html +1 -1
  110. package/web/assets/index-DepsYMLl.js +0 -70
@@ -0,0 +1,163 @@
1
+ import { Value } from "@sinclair/typebox/value";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { IdempotencyKeySchema, ProjectIdSchema } from "@tutti/shared/schemas/domain";
4
+ import { ManagedTierSchema } from "@tutti/shared/schemas/internal";
5
+ import { resolveLaunchLocalContext } from "../cli/launch.js";
6
+ import { NativeManagedState } from "../managed-access/state.js";
7
+ import { findNativeManagedProject } from "../managed-access/state.js";
8
+ import { readExistingManagedTarget } from "../managed-access/existing-project.js";
9
+ import { ManagedLocalStateError } from "../managed-access/private-store.js";
10
+ import { reconcileNativePreparation, validateNativePreparation, } from "../managed-access/transport.js";
11
+ import { createLocalConsoleOperationEnvironment, } from "./invocation-context.js";
12
+ export const ManagedDraftInputSchema = Type.Object({
13
+ request_ref: IdempotencyKeySchema,
14
+ workspace_path: Type.String({ minLength: 1, maxLength: 4096 }),
15
+ display_name: Type.String({ minLength: 1, maxLength: 80 }),
16
+ description: Type.Optional(Type.String({ maxLength: 160 })),
17
+ surface: Type.Union([Type.Literal("standalone"), Type.Literal("feishu")]),
18
+ tier: ManagedTierSchema,
19
+ existing_project_id: Type.Optional(ProjectIdSchema),
20
+ }, { additionalProperties: false });
21
+ function projection(row) {
22
+ return {
23
+ request_ref: row.request_ref,
24
+ project_id: row.project_id,
25
+ display_name: row.display_name,
26
+ workspace_path: row.workspace_root,
27
+ ...(row.description === undefined ? {} : { description: row.description }),
28
+ surface: row.surface,
29
+ tier: row.tier,
30
+ ...(row.existing_relay_project_ref === undefined
31
+ ? {}
32
+ : { existing_project_id: row.project_id }),
33
+ state: row.state,
34
+ expires_at: row.expires_at,
35
+ };
36
+ }
37
+ // Native orchestration only. No project SQLite, Git writes, model key or browser account authority.
38
+ export class LocalConsoleManagedProjectService {
39
+ options;
40
+ constructor(options) {
41
+ this.options = options;
42
+ }
43
+ environment(context) {
44
+ return createLocalConsoleOperationEnvironment({
45
+ serviceEnvironment: this.options.serviceEnvironment ?? process.env,
46
+ invocationEnvironment: context.environment,
47
+ tuttiHome: this.options.tuttiHome,
48
+ });
49
+ }
50
+ state(context) {
51
+ const env = this.environment(context);
52
+ return new NativeManagedState({
53
+ tuttiHome: this.options.tuttiHome,
54
+ relayUrl: env.TUTTI_RELAY_URL ?? "https://tutti.now",
55
+ });
56
+ }
57
+ prepare(input, context) {
58
+ return this.options.operations.runProjectOperation(() => {
59
+ if (!Value.Check(ManagedDraftInputSchema, input) || !input.display_name.trim())
60
+ throw new ManagedLocalStateError("managed_creation_input_invalid");
61
+ const local = resolveLaunchLocalContext({
62
+ workspacePath: input.workspace_path,
63
+ env: this.environment(context),
64
+ cwd: context.source === "terminal" ? context.currentDirectory : this.options.tuttiHome,
65
+ });
66
+ if (local.existing_identity.kind !== "missing" && input.existing_project_id === undefined)
67
+ throw new ManagedLocalStateError("managed_existing_project_requires_opt_in");
68
+ const state = this.state(context);
69
+ let existing;
70
+ if (input.existing_project_id !== undefined) {
71
+ const target = readExistingManagedTarget({
72
+ tuttiHome: this.options.tuttiHome,
73
+ relayOrigin: state.origin,
74
+ projectId: input.existing_project_id,
75
+ workspaceRoot: local.workspace_root,
76
+ surface: input.surface,
77
+ });
78
+ const previous = findNativeManagedProject({
79
+ tuttiHome: this.options.tuttiHome,
80
+ relayUrl: state.origin,
81
+ projectId: input.existing_project_id,
82
+ });
83
+ if (previous !== null &&
84
+ (previous.journal.state !== "registered" || previous.journal.tier !== input.tier))
85
+ throw new ManagedLocalStateError("managed_reauthorization_conflict");
86
+ existing = {
87
+ ...target,
88
+ ...(previous === null ? {} : { previousRequestRef: previous.journal.request_ref }),
89
+ };
90
+ }
91
+ const row = state.createDraft({
92
+ requestRef: input.request_ref,
93
+ workspaceRoot: local.workspace_root,
94
+ displayName: input.display_name.trim(),
95
+ ...(input.description === undefined ? {} : { description: input.description.trim() }),
96
+ surface: input.surface,
97
+ tier: input.tier,
98
+ ...(existing === undefined ? {} : { projectId: input.existing_project_id, existing }),
99
+ now: this.options.now?.() ?? new Date(),
100
+ });
101
+ return {
102
+ draft: projection(row),
103
+ relay_origin: state.origin,
104
+ preparation: state.preparationBody(row.request_ref),
105
+ };
106
+ });
107
+ }
108
+ read(requestRef, context) {
109
+ const state = this.state(context);
110
+ return {
111
+ draft: projection(state.read(requestRef)),
112
+ relay_origin: state.origin,
113
+ preparation: state.preparationBody(requestRef),
114
+ };
115
+ }
116
+ async reconcile(requestRef, context) {
117
+ return this.options.operations.runProjectOperation(async () => {
118
+ const state = this.state(context);
119
+ await reconcileNativePreparation({
120
+ state,
121
+ requestRef,
122
+ now: this.options.now?.() ?? new Date(),
123
+ ...(this.options.fetchImpl === undefined ? {} : { fetchImpl: this.options.fetchImpl }),
124
+ });
125
+ return this.read(requestRef, context);
126
+ });
127
+ }
128
+ async validate(requestRef, context) {
129
+ return this.options.operations.runProjectOperation(async () => {
130
+ const state = this.state(context);
131
+ await reconcileNativePreparation({
132
+ state,
133
+ requestRef,
134
+ now: this.options.now?.() ?? new Date(),
135
+ ...(this.options.fetchImpl === undefined ? {} : { fetchImpl: this.options.fetchImpl }),
136
+ });
137
+ const validation = await validateNativePreparation({
138
+ state,
139
+ requestRef,
140
+ ...(this.options.fetchImpl === undefined ? {} : { fetchImpl: this.options.fetchImpl }),
141
+ });
142
+ return { ...this.read(requestRef, context), validation };
143
+ });
144
+ }
145
+ async launch(requestRef, context) {
146
+ return this.options.operations.runProjectOperation(async () => {
147
+ const projects = this.options.projects;
148
+ if (projects === undefined)
149
+ throw new ManagedLocalStateError("managed_launch_unavailable");
150
+ const state = this.state(context);
151
+ // Re-check current Relay authority immediately before any workspace mutation.
152
+ await reconcileNativePreparation({
153
+ state,
154
+ requestRef,
155
+ now: this.options.now?.() ?? new Date(),
156
+ ...(this.options.fetchImpl === undefined ? {} : { fetchImpl: this.options.fetchImpl }),
157
+ });
158
+ state.bootstrap(requestRef, this.options.now?.() ?? new Date());
159
+ return projects.launchAuthorizedProject(state, requestRef, context);
160
+ });
161
+ }
162
+ }
163
+ //# sourceMappingURL=managed-project-service.js.map
@@ -10,7 +10,7 @@ export type LocalConsoleOperationState = "idle" | "project_busy" | "package_upda
10
10
  export declare class LocalConsoleOperationCoordinator {
11
11
  #private;
12
12
  state(): LocalConsoleOperationState;
13
- runProjectOperation<T>(operation: () => Promise<T>): Promise<T>;
13
+ runProjectOperation<T>(operation: () => T | Promise<T>): Promise<T>;
14
14
  tryBeginPackageUpdate(): LocalConsolePackageUpdateOperationLease | null;
15
15
  }
16
16
  //# sourceMappingURL=operation-coordinator.d.ts.map
@@ -3,6 +3,7 @@ import { readHostLocalPackageUpdateReadiness, requestHostLocalIdleShutdown, } fr
3
3
  import { createRuntimeEndpointProbe, waitForHostShutdown } from "../cli/host-runtime-endpoint.js";
4
4
  import { spawnDetachedHost, waitForManagedHostReady } from "../cli/managed-host.js";
5
5
  import { resolveManagedProjectContext } from "../cli/project-resolver.js";
6
+ import { hasProjectModelConfiguration } from "../managed-access/project-provider.js";
6
7
  const PACKAGE_UPDATE_HOST_SHUTDOWN_WAIT_MS = 5_000;
7
8
  export async function stopPackageUpdateHost(options) {
8
9
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -65,7 +66,7 @@ export async function restorePackageUpdateHosts(options) {
65
66
  target: projectId,
66
67
  env: { ...options.env, TUTTI_HOME: options.tuttiHome },
67
68
  });
68
- if (!project.workspace_exists || project.provider_config.status !== "configured") {
69
+ if (!project.workspace_exists || !hasProjectModelConfiguration(project.provider_config)) {
69
70
  failed.push(projectId);
70
71
  continue;
71
72
  }
@@ -1,6 +1,8 @@
1
1
  import { type ProjectSurface, type ProjectSurfaceProfile } from "@tutti/shared/domain";
2
2
  import { type ProjectId, type RelayProjectRef } from "@tutti/shared/ids";
3
3
  import type { FeishuSetupProjection } from "@tutti/shared/schemas/internal";
4
+ import type { ProviderConfigProjection } from "@tutti/shared/schemas/api";
5
+ import type { NativeManagedState } from "../managed-access/state.js";
4
6
  import { type ConfigureProjectOpenAiProviderResult, type OpenAiProviderCredentialValidator } from "../../providers/openai/index.js";
5
7
  import { type FetchLike } from "../cli/host-runtime-endpoint.js";
6
8
  import type { HostLaunchRelaySummary } from "../cli/relay-registration.js";
@@ -13,8 +15,9 @@ export type LocalConsoleProject = {
13
15
  display_name: string;
14
16
  workspace_path: string;
15
17
  status: RuntimeProjectRow["status"];
16
- provider_status: "configured" | "not_configured" | "invalid";
18
+ provider_status: ProviderConfigProjection["status"];
17
19
  provider_model?: string;
20
+ provider_tier?: "light" | "high" | "max";
18
21
  relay_project_ref?: RelayProjectRef;
19
22
  open_url?: string;
20
23
  relay_connection_status?: string;
@@ -91,6 +94,7 @@ export declare class LocalConsoleProjectService {
91
94
  provider?: LocalConsoleProviderInput;
92
95
  surface?: ProjectSurface;
93
96
  }, context: LocalConsoleInvocationContext, accountHandoff?: LocalConsoleAccountHandoff): Promise<LocalConsoleLaunchResult>;
97
+ launchAuthorizedProject(state: NativeManagedState, requestRef: string, context: LocalConsoleInvocationContext): Promise<LocalConsoleLaunchResult>;
94
98
  openProject(projectId: string, context: LocalConsoleInvocationContext): Promise<LocalConsoleOpenResult>;
95
99
  refreshInvite(projectId: string, context: LocalConsoleInvocationContext): Promise<{
96
100
  join_url: string;
@@ -3,7 +3,9 @@ import { resolve } from "node:path";
3
3
  import { readRelayProjectMembershipState } from "@tutti/relay-client";
4
4
  import { createProjectSurfaceProfile, } from "@tutti/shared/domain";
5
5
  import { relayProjectRouteSegment } from "@tutti/shared/ids";
6
- import { configureProjectOpenAiProvider, discoverOpenAiModels, isOpenAiApiKey, readOpenAiProviderConfigProjection, validateOpenAiCredential, } from "../../providers/openai/index.js";
6
+ import { hasProjectModelConfiguration, readProjectModelAccess, } from "../managed-access/project-provider.js";
7
+ import { materializeManagedWorkspace } from "../managed-access/workspace.js";
8
+ import { configureProjectOpenAiProvider, discoverOpenAiModels, isOpenAiApiKey, validateOpenAiCredential, } from "../../providers/openai/index.js";
7
9
  import { formatCliErrorReason, LaunchError } from "../cli/errors.js";
8
10
  import { createRuntimeEndpointProbe, waitForHostShutdown, } from "../cli/host-runtime-endpoint.js";
9
11
  import { prepareLaunchProject, resolveLaunchLocalContext, resolveRelayUrl } from "../cli/launch.js";
@@ -207,18 +209,28 @@ export class LocalConsoleProjectService {
207
209
  // Damaged legacy metadata remains visible without an invented creation date.
208
210
  }
209
211
  }
210
- const provider = readOpenAiProviderConfigProjection({
212
+ const provider = readProjectModelAccess({
211
213
  tuttiHome: this.#tuttiHome,
212
214
  projectId: row.project_id,
215
+ relayUrl: binding?.relay_url ?? resolveRelayUrl(operation.env),
213
216
  });
214
- const providerModel = provider.status === "not_configured" ? undefined : provider.default_model;
217
+ const providerModel = provider.status === "not_configured"
218
+ ? undefined
219
+ : provider.status === "managed"
220
+ ? `Tutti ${provider.tier}`
221
+ : provider.default_model;
215
222
  const activityAt = readProjectActivityAt(this.#tuttiHome, row, binding);
216
- return projectFromRuntimeRow(row, provider.status, providerModel, {
217
- ...(activityAt === undefined ? {} : { activityAt }),
218
- ...(createdAt === undefined ? {} : { createdAt }),
219
- relayUrl: binding?.relay_url ?? relayUrlFromJoinUrl(row.join_url) ?? resolveRelayUrl(operation.env),
220
- surfaceProfile: binding?.surface_profile ?? createProjectSurfaceProfile(),
221
- });
223
+ return {
224
+ ...projectFromRuntimeRow(row, provider.status, providerModel, {
225
+ ...(activityAt === undefined ? {} : { activityAt }),
226
+ ...(createdAt === undefined ? {} : { createdAt }),
227
+ relayUrl: binding?.relay_url ??
228
+ relayUrlFromJoinUrl(row.join_url) ??
229
+ resolveRelayUrl(operation.env),
230
+ surfaceProfile: binding?.surface_profile ?? createProjectSurfaceProfile(),
231
+ }),
232
+ ...(provider.status === "managed" ? { provider_tier: provider.tier } : {}),
233
+ };
222
234
  });
223
235
  }
224
236
  async #readProject(projectId, context) {
@@ -325,12 +337,14 @@ export class LocalConsoleProjectService {
325
337
  #requireStoredLaunchProvider(workspacePath, operation) {
326
338
  const context = resolveLaunchLocalContext({ workspacePath, ...operation });
327
339
  const provider = context.existing_identity.kind === "present"
328
- ? readOpenAiProviderConfigProjection({
340
+ ? readProjectModelAccess({
329
341
  tuttiHome: context.tutti_home,
330
342
  projectId: context.existing_identity.project_id,
343
+ relayUrl: readMachineProjectBinding(context.tutti_home, context.existing_identity.project_id)
344
+ ?.relay_url ?? resolveRelayUrl(operation.env),
331
345
  })
332
346
  : { status: "not_configured" };
333
- if (provider.status !== "configured") {
347
+ if (!hasProjectModelConfiguration(provider)) {
334
348
  throw new LocalConsoleProjectError("provider_configuration_required", "This project does not have a valid saved Provider. Configure it in the launch form.");
335
349
  }
336
350
  }
@@ -359,6 +373,18 @@ export class LocalConsoleProjectService {
359
373
  }
360
374
  async #launchProject(input, context, accountHandoff) {
361
375
  const operation = this.#operationOptions(context);
376
+ if (input.provider !== undefined) {
377
+ const local = resolveLaunchLocalContext({ workspacePath: input.workspacePath, ...operation });
378
+ if (local.existing_identity.kind === "present" &&
379
+ readProjectModelAccess({
380
+ tuttiHome: local.tutti_home,
381
+ projectId: local.existing_identity.project_id,
382
+ relayUrl: readMachineProjectBinding(local.tutti_home, local.existing_identity.project_id)
383
+ ?.relay_url ?? resolveRelayUrl(operation.env),
384
+ }).status === "managed") {
385
+ throw new LocalConsoleProjectError("managed_model_fixed", "Managed projects cannot be changed through BYOK setup.", 409);
386
+ }
387
+ }
362
388
  if (input.provider === undefined) {
363
389
  try {
364
390
  this.#requireStoredLaunchProvider(input.workspacePath, operation);
@@ -388,6 +414,9 @@ export class LocalConsoleProjectService {
388
414
  catch (error) {
389
415
  throw this.#normalizeError(error);
390
416
  }
417
+ if (provider !== undefined && preparation.provider_status === "managed") {
418
+ throw new LocalConsoleProjectError("managed_model_fixed", "Managed projects cannot be changed through BYOK setup.", 409);
419
+ }
391
420
  if (provider !== undefined) {
392
421
  const configured = await configureProjectOpenAiProvider({
393
422
  tuttiHome: preparation.tutti_home,
@@ -401,10 +430,11 @@ export class LocalConsoleProjectService {
401
430
  throw new LocalConsoleProjectError(configured.reason, localConsoleProviderValidationMessage(configured));
402
431
  }
403
432
  }
404
- else if (preparation.provider_status !== "configured") {
433
+ else if (!hasProjectModelConfiguration(preparation.provider_config)) {
405
434
  throw new LocalConsoleProjectError("provider_configuration_required", "This project does not have a valid saved Provider. Configure it in the launch form.");
406
435
  }
407
436
  if (preparation.machine_local.binding.surface_profile.kind === "feishu" &&
437
+ preparation.provider_status !== "managed" &&
408
438
  accountHandoff !== undefined) {
409
439
  writeFeishuOwnerAccountHandoff({
410
440
  tuttiHome: preparation.tutti_home,
@@ -414,12 +444,31 @@ export class LocalConsoleProjectService {
414
444
  now: this.#now(),
415
445
  });
416
446
  }
447
+ return this.#finishLaunch(preparation, context, operation.env);
448
+ }
449
+ // Called only by the native authority service while holding the Console operation
450
+ // guard. It accepts a private, verified journal, never a browser-provided receipt.
451
+ async launchAuthorizedProject(state, requestRef, context) {
452
+ const operation = this.#operationOptions(context);
453
+ const preparation = await materializeManagedWorkspace({
454
+ state,
455
+ requestRef,
456
+ tuttiHome: this.#tuttiHome,
457
+ environment: operation.env,
458
+ now: this.#now,
459
+ });
460
+ return this.#finishLaunch(preparation, context, {
461
+ ...operation.env,
462
+ TUTTI_RELAY_URL: state.origin,
463
+ });
464
+ }
465
+ async #finishLaunch(preparation, context, environment) {
417
466
  try {
418
467
  const ready = await this.#startManagedHost({
419
468
  workspaceRoot: preparation.workspace_root,
420
469
  tuttiHome: preparation.tutti_home,
421
470
  projectId: preparation.project_id,
422
- env: operation.env,
471
+ env: environment,
423
472
  });
424
473
  if (preparation.machine_local.binding.surface_profile.kind === "standalone" &&
425
474
  ready.join_url === undefined) {
@@ -480,7 +529,7 @@ export class LocalConsoleProjectService {
480
529
  }
481
530
  if (!managed.workspace_exists ||
482
531
  managed.runtime_endpoint === null ||
483
- managed.provider_config.status !== "configured") {
532
+ !hasProjectModelConfiguration(managed.provider_config)) {
484
533
  return { project: current, hostUpdate: "deferred_unavailable" };
485
534
  }
486
535
  let shutdownDisposition;
@@ -1,7 +1,10 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
+ import { LocalConsoleManagedProjectService } from "./managed-project-service.js";
3
+ import { registerLocalConsoleManagedProjectRoutes } from "./managed-project-routes.js";
2
4
  import { existsSync, statSync } from "node:fs";
3
5
  import { resolve } from "node:path";
4
6
  import fastify from "fastify";
7
+ import { createIdempotencyKey } from "@tutti/shared/ids";
5
8
  import { resolveTuttiHome } from "../../providers/openai/index.js";
6
9
  import { readCliVersion } from "../cli/version.js";
7
10
  import { registerHostWebStaticRoutes, resolvePackagedWebStaticRoot } from "../http/static-web.js";
@@ -66,7 +69,11 @@ function requireLoopbackHost(request) {
66
69
  }
67
70
  }
68
71
  export function createLocalConsoleServer(options) {
69
- const app = fastify({ logger: false, bodyLimit: 64 * 1024 });
72
+ const app = fastify({
73
+ logger: false,
74
+ bodyLimit: 64 * 1024,
75
+ genReqId: () => createIdempotencyKey(),
76
+ });
70
77
  const tuttiHome = options.tuttiHome ?? resolveTuttiHome(options.env?.TUTTI_HOME, options.cwd);
71
78
  const sessions = new LocalConsoleSessionRegistry({
72
79
  ...(options.now === undefined ? {} : { now: options.now }),
@@ -119,6 +126,7 @@ export function createLocalConsoleServer(options) {
119
126
  }
120
127
  });
121
128
  app.addHook("onRequest", (request, reply, done) => {
129
+ reply.header("x-tutti-request-id", request.id);
122
130
  try {
123
131
  requireLoopbackHost(request);
124
132
  void reply
@@ -135,7 +143,7 @@ export function createLocalConsoleServer(options) {
135
143
  done(error);
136
144
  }
137
145
  });
138
- app.setErrorHandler((error, _request, reply) => {
146
+ app.setErrorHandler((error, request, reply) => {
139
147
  const normalized = error instanceof LocalConsoleHttpError ||
140
148
  error instanceof LocalConsoleProjectError ||
141
149
  error instanceof LocalConsoleOperationConflictError
@@ -148,6 +156,7 @@ export function createLocalConsoleServer(options) {
148
156
  code: normalized.code,
149
157
  message: normalized.message,
150
158
  retryable: normalized.statusCode >= 500,
159
+ request_id: request.id,
151
160
  },
152
161
  });
153
162
  });
@@ -173,6 +182,17 @@ export function createLocalConsoleServer(options) {
173
182
  options.onActivity?.();
174
183
  return session;
175
184
  }
185
+ registerLocalConsoleManagedProjectRoutes(app, {
186
+ tuttiHome,
187
+ requireSession,
188
+ service: new LocalConsoleManagedProjectService({
189
+ tuttiHome,
190
+ operations,
191
+ projects,
192
+ ...(options.env === undefined ? {} : { serviceEnvironment: options.env }),
193
+ ...(options.now === undefined ? {} : { now: options.now }),
194
+ }),
195
+ });
176
196
  app.get(`${LOCAL_CONSOLE_API_BASE}/health`, () => ({
177
197
  status: "ready",
178
198
  protocol_version: 2,
@@ -0,0 +1,9 @@
1
+ import type { OpenAiCredentialLease, OpenAiExecutionConfig } from "../../providers/openai/execution-access.js";
2
+ import type { NativeManagedRuntime } from "./runtime.js";
3
+ import type { ModelSessionOwner } from "./runtime-records.js";
4
+ export type ManagedExecutionRuntime = Pick<NativeManagedRuntime, "selection" | "run">;
5
+ export declare function bindManagedExecution(runtime: ManagedExecutionRuntime, owner: ModelSessionOwner): Promise<{
6
+ config: OpenAiExecutionConfig;
7
+ credentialLease: OpenAiCredentialLease;
8
+ }>;
9
+ //# sourceMappingURL=execution-binding.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { ManagedLocalStateError } from "./private-store.js";
2
+ export async function bindManagedExecution(runtime, owner) {
3
+ // This happens before provider factories open any durable Thread. Retaining
4
+ // this selection through retries forbids silent route/model replacement.
5
+ const selected = await runtime.selection(owner);
6
+ const config = {
7
+ default_model: selected.provider.model,
8
+ api_base_url: selected.provider.base_url,
9
+ };
10
+ return {
11
+ config,
12
+ credentialLease: (durationMs, invoke) => runtime.run(owner, Math.ceil(durationMs / 1000), async (access) => {
13
+ if (access.session_ref !== selected.session_ref ||
14
+ access.route_ref !== selected.route_ref ||
15
+ access.provider.model !== selected.provider.model ||
16
+ access.provider.base_url !== selected.provider.base_url)
17
+ throw new ManagedLocalStateError("managed_route_changed");
18
+ return invoke(access.provider.api_key);
19
+ }),
20
+ };
21
+ }
22
+ //# sourceMappingURL=execution-binding.js.map
@@ -0,0 +1,15 @@
1
+ import type { ProjectId } from "@tutti/shared/ids";
2
+ export declare function requireStoppedManagedTarget(tuttiHome: string, projectId: ProjectId): void;
3
+ export declare function readExistingManagedTarget(options: {
4
+ tuttiHome: string;
5
+ relayOrigin: string;
6
+ projectId: ProjectId;
7
+ workspaceRoot: string;
8
+ surface: "standalone" | "feishu";
9
+ requireStopped?: boolean;
10
+ }): {
11
+ relayProjectRef: `relay_project_${string}`;
12
+ workspacePath: string;
13
+ hostSecret: string;
14
+ };
15
+ //# sourceMappingURL=existing-project.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { readHostRegistrationSecret, readMachineProjectBinding, readMachineRuntimeEndpoint, } from "../cli/machine-local.js";
3
+ import { readProjectIdentity } from "../cli/project-identity.js";
4
+ import { ManagedLocalStateError } from "./private-store.js";
5
+ import { managedRelayOrigin } from "./state.js";
6
+ // A provider cutover must never mutate the authority used by a live Host. A
7
+ // process we cannot prove stopped is rejected; network health failure is not proof.
8
+ export function requireStoppedManagedTarget(tuttiHome, projectId) {
9
+ const endpoint = readMachineRuntimeEndpoint(tuttiHome, projectId);
10
+ if (endpoint === null)
11
+ return;
12
+ try {
13
+ process.kill(endpoint.pid, 0);
14
+ }
15
+ catch (error) {
16
+ if (error.code === "ESRCH")
17
+ return;
18
+ }
19
+ throw new ManagedLocalStateError("managed_project_must_be_stopped");
20
+ }
21
+ export function readExistingManagedTarget(options) {
22
+ if (options.requireStopped !== false)
23
+ requireStoppedManagedTarget(options.tuttiHome, options.projectId);
24
+ const binding = readMachineProjectBinding(options.tuttiHome, options.projectId);
25
+ const identity = readProjectIdentity(options.workspaceRoot);
26
+ if (binding === null ||
27
+ binding.relay_project_ref === undefined ||
28
+ binding.relay_url === undefined ||
29
+ managedRelayOrigin(binding.relay_url) !== options.relayOrigin ||
30
+ realpathSync(binding.workspace_root) !== realpathSync(options.workspaceRoot) ||
31
+ binding.surface_profile.kind !== options.surface ||
32
+ identity.kind !== "present" ||
33
+ identity.project_id !== options.projectId)
34
+ throw new ManagedLocalStateError("managed_existing_project_mismatch");
35
+ return {
36
+ relayProjectRef: binding.relay_project_ref,
37
+ workspacePath: binding.workspace_root,
38
+ hostSecret: readHostRegistrationSecret(options.tuttiHome, options.projectId).secret,
39
+ };
40
+ }
41
+ //# sourceMappingURL=existing-project.js.map
@@ -0,0 +1,66 @@
1
+ import { ManagedLocalStateError } from "./private-store.js";
2
+ export declare class ManagedControlError extends ManagedLocalStateError {
3
+ readonly status?: number | undefined;
4
+ readonly requestId?: string | undefined;
5
+ constructor(code: string, status?: number | undefined, requestId?: string | undefined);
6
+ }
7
+ export declare class NativeModelControlClient {
8
+ private readonly options;
9
+ constructor(options: {
10
+ authorize: () => Promise<{
11
+ authorization: string;
12
+ control_origin: string;
13
+ }>;
14
+ fetchImpl?: typeof fetch;
15
+ });
16
+ private request;
17
+ pin(sessionRef: string, routeRef?: string): Promise<{
18
+ disposition: "created" | "existing";
19
+ tier: "light" | "high" | "max";
20
+ session_ref: string;
21
+ provider: {
22
+ model: string;
23
+ base_url: string;
24
+ };
25
+ route_ref: string;
26
+ }>;
27
+ issue(sessionRef: string, requestRef: string): Promise<{
28
+ disposition: "issued";
29
+ provider: {
30
+ api_key: string;
31
+ model: string;
32
+ base_url: string;
33
+ };
34
+ route_ref: string;
35
+ credential: {
36
+ state: "active" | "revoked" | "expired" | "creating" | "revoking";
37
+ session_ref: string;
38
+ expires_at: number;
39
+ credential_ref: string;
40
+ device_ref: string;
41
+ quota_cap: number;
42
+ };
43
+ } | {
44
+ disposition: "already_issued";
45
+ credential: {
46
+ state: "active" | "revoked" | "expired" | "creating" | "revoking";
47
+ session_ref: string;
48
+ expires_at: number;
49
+ credential_ref: string;
50
+ device_ref: string;
51
+ quota_cap: number;
52
+ };
53
+ }>;
54
+ revoke(credentialRef: string): Promise<{
55
+ disposition: "revoked";
56
+ credential: {
57
+ state: "active" | "revoked" | "expired" | "creating" | "revoking";
58
+ session_ref: string;
59
+ expires_at: number;
60
+ credential_ref: string;
61
+ device_ref: string;
62
+ quota_cap: number;
63
+ };
64
+ }>;
65
+ }
66
+ //# sourceMappingURL=model-control-client.d.ts.map