@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.
Files changed (113) 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 +45 -137
  40. package/dist/server-shell/http/routes/project-api/openapi.d.ts +45 -137
  41. package/dist/server-shell/http/routes/project-api/schemas.d.ts +24 -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 +3 -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 +141 -0
  47. package/dist/server-shell/local-console/managed-project-service.js +158 -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 +4 -1
  51. package/dist/server-shell/local-console/project-service.js +62 -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/legacy-records.d.ts +2 -0
  58. package/dist/server-shell/managed-access/legacy-records.js +24 -0
  59. package/dist/server-shell/managed-access/model-control-client.d.ts +65 -0
  60. package/dist/server-shell/managed-access/model-control-client.js +112 -0
  61. package/dist/server-shell/managed-access/private-store.d.ts +19 -0
  62. package/dist/server-shell/managed-access/private-store.js +275 -0
  63. package/dist/server-shell/managed-access/project-provider.d.ts +18 -0
  64. package/dist/server-shell/managed-access/project-provider.js +30 -0
  65. package/dist/server-shell/managed-access/records.d.ts +103 -0
  66. package/dist/server-shell/managed-access/records.js +74 -0
  67. package/dist/server-shell/managed-access/runners.d.ts +11 -0
  68. package/dist/server-shell/managed-access/runners.js +103 -0
  69. package/dist/server-shell/managed-access/runtime-records.d.ts +106 -0
  70. package/dist/server-shell/managed-access/runtime-records.js +65 -0
  71. package/dist/server-shell/managed-access/runtime.d.ts +48 -0
  72. package/dist/server-shell/managed-access/runtime.js +300 -0
  73. package/dist/server-shell/managed-access/state.d.ts +53 -0
  74. package/dist/server-shell/managed-access/state.js +292 -0
  75. package/dist/server-shell/managed-access/transport.d.ts +45 -0
  76. package/dist/server-shell/managed-access/transport.js +39 -0
  77. package/dist/server-shell/managed-access/workspace.d.ts +10 -0
  78. package/dist/server-shell/managed-access/workspace.js +69 -0
  79. package/dist/server-shell/smoke/e2e-client.d.ts +8 -3
  80. package/dist/server-shell/smoke/e2e-client.js +11 -11
  81. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +4 -1
  82. package/node_modules/@tutti/relay-client/dist/host-control.js +26 -3
  83. package/node_modules/@tutti/relay-client/dist/index.d.ts +1 -0
  84. package/node_modules/@tutti/relay-client/dist/index.js +1 -0
  85. package/node_modules/@tutti/relay-client/dist/project-authority.d.ts +70 -0
  86. package/node_modules/@tutti/relay-client/dist/project-authority.js +139 -0
  87. package/node_modules/@tutti/shared/dist/ids/index.d.ts +7 -0
  88. package/node_modules/@tutti/shared/dist/ids/index.js +7 -0
  89. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -0
  90. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -0
  91. package/node_modules/@tutti/shared/dist/schemas/api/managed-account.d.ts +34 -0
  92. package/node_modules/@tutti/shared/dist/schemas/api/managed-account.js +40 -0
  93. package/node_modules/@tutti/shared/dist/schemas/api/provider-config.d.ts +7 -4
  94. package/node_modules/@tutti/shared/dist/schemas/api/provider-config.js +5 -0
  95. package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +3 -0
  96. package/node_modules/@tutti/shared/dist/schemas/internal/creation-validation.d.ts +26 -0
  97. package/node_modules/@tutti/shared/dist/schemas/internal/creation-validation.js +40 -0
  98. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +5 -0
  99. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +5 -0
  100. package/node_modules/@tutti/shared/dist/schemas/internal/managed-account.d.ts +12 -0
  101. package/node_modules/@tutti/shared/dist/schemas/internal/managed-account.js +13 -0
  102. package/node_modules/@tutti/shared/dist/schemas/internal/managed-models.d.ts +17 -0
  103. package/node_modules/@tutti/shared/dist/schemas/internal/managed-models.js +17 -0
  104. package/node_modules/@tutti/shared/dist/schemas/internal/managed-runtime.d.ts +113 -0
  105. package/node_modules/@tutti/shared/dist/schemas/internal/managed-runtime.js +84 -0
  106. package/node_modules/@tutti/shared/dist/schemas/internal/project-authority.d.ts +114 -0
  107. package/node_modules/@tutti/shared/dist/schemas/internal/project-authority.js +102 -0
  108. package/package.json +1 -1
  109. package/web/assets/{homepage-motion-scene-B6lCCHBq.js → homepage-motion-scene-WfLyzcCG.js} +1 -1
  110. package/web/assets/{index-DsYi7AhN.css → index-ATsYiBZs.css} +1 -1
  111. package/web/assets/index-o9YCtzEd.js +70 -0
  112. package/web/index.html +2 -2
  113. 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, readOpenAiProviderConfigProjection, updateProjectOpenAiDefaultModel, } from "../../providers/openai/index.js";
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 = () => readOpenAiProviderConfigProjection({
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: () => resolveProjectOpenAiRunPipelineRunner({
401
- tuttiHome: options.preparation.tutti_home,
402
- projectId: options.preparation.project_id,
403
- workspaceRoot: options.preparation.workspace_root,
404
- agentContext: agentContextRuntime,
405
- approvalManager,
406
- logger: runPipelineLogger,
407
- recordUsage,
408
- invocationCoordinator: codexInvocationCoordinator,
409
- checkProcessCoordinator,
410
- onLifecycleIncident: runLifecycleIncident,
411
- }),
412
- resolveWorkflowRunner: () => resolveProjectOpenAiProcedureWorkflowRunner({
413
- tuttiHome: options.preparation.tutti_home,
414
- projectId: options.preparation.project_id,
415
- workspaceRoot: options.preparation.workspace_root,
416
- agentContext: agentContextRuntime,
417
- recordUsage,
418
- invocationCoordinator: codexInvocationCoordinator,
419
- onLifecycleIncident: procedureLifecycleIncident,
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 resolution = resolveProjectOpenAiReadOnlyChatAssistantModel({
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: createHostProviderModelService({
601
- tuttiHome: options.preparation.tutti_home,
602
- projectId: options.preparation.project_id,
603
- now,
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
- controlPlane.runStartupRecovery({
661
- workspaceRoot: options.preparation.workspace_root,
662
- recoverInProgressCommands: true,
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 { type OpenAiProviderConfigProjection } from "../../providers/openai/index.js";
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 = OpenAiProviderConfigProjection["status"];
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: OpenAiProviderConfigProjection;
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 { readOpenAiProviderConfigProjection, resolveTuttiHome, } from "../../providers/openai/index.js";
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" ? existingIdentity.project_id : createProjectId();
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 = readOpenAiProviderConfigProjection({
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: readHostRegistrationSecret(tuttiHome, projectId).created_at,
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
- writePrivateJsonFile(secretPath, secretFile);
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 { type OpenAiProviderConfigProjection } from "../../providers/openai/index.js";
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: OpenAiProviderConfigProjection;
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 { readOpenAiProviderConfigProjection, readProjectOpenAiProviderConfig, resolveTuttiHome, } from "../../providers/openai/index.js";
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: readOpenAiProviderConfigProjection({
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: readOpenAiProviderConfigProjection({
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 { ProviderConfigProjection } from "@tutti/shared/schemas/api";
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,