@xfey/tutti 0.1.103 → 0.1.104

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 (106) hide show
  1. package/README.md +9 -4
  2. package/dist/chat-assistant/index.d.ts +6 -1
  3. package/dist/chat-assistant/index.js +22 -12
  4. package/dist/collaboration-ingestion/external-source.d.ts +15 -0
  5. package/dist/collaboration-ingestion/external-source.js +120 -0
  6. package/dist/collaboration-ingestion/index.d.ts +5 -0
  7. package/dist/collaboration-ingestion/index.js +4 -0
  8. package/dist/collaboration-ingestion/invalidations.d.ts +4 -0
  9. package/dist/collaboration-ingestion/invalidations.js +15 -0
  10. package/dist/collaboration-ingestion/main-chat.d.ts +33 -0
  11. package/dist/collaboration-ingestion/main-chat.js +67 -0
  12. package/dist/collaboration-ingestion/reference-files.d.ts +29 -0
  13. package/dist/collaboration-ingestion/reference-files.js +158 -0
  14. package/dist/collaboration-ingestion/types.d.ts +19 -0
  15. package/dist/collaboration-ingestion/types.js +2 -0
  16. package/dist/platform-outbound/coordinator.d.ts +17 -0
  17. package/dist/platform-outbound/coordinator.js +48 -0
  18. package/dist/platform-outbound/index.d.ts +5 -0
  19. package/dist/platform-outbound/index.js +5 -0
  20. package/dist/platform-outbound/repository.d.ts +30 -0
  21. package/dist/platform-outbound/repository.js +108 -0
  22. package/dist/platform-outbound/types.d.ts +38 -0
  23. package/dist/platform-outbound/types.js +2 -0
  24. package/dist/platform-outbound/worker.d.ts +36 -0
  25. package/dist/platform-outbound/worker.js +164 -0
  26. package/dist/server-shell/cli/cli.js +9 -0
  27. package/dist/server-shell/cli/errors.d.ts +1 -1
  28. package/dist/server-shell/cli/host-lifecycle.d.ts +2 -0
  29. package/dist/server-shell/cli/host-lifecycle.js +34 -1
  30. package/dist/server-shell/cli/host-relay-connection-manager.js +22 -11
  31. package/dist/server-shell/cli/host-relay-status.js +1 -0
  32. package/dist/server-shell/cli/host-runtime-endpoint.js +17 -1
  33. package/dist/server-shell/cli/host-server-runtime.d.ts +2 -0
  34. package/dist/server-shell/cli/host-server-runtime.js +88 -14
  35. package/dist/server-shell/cli/launch-command.d.ts +7 -0
  36. package/dist/server-shell/cli/launch-command.js +39 -1
  37. package/dist/server-shell/cli/launch.d.ts +2 -0
  38. package/dist/server-shell/cli/launch.js +3 -0
  39. package/dist/server-shell/cli/machine-local.d.ts +20 -0
  40. package/dist/server-shell/cli/machine-local.js +61 -1
  41. package/dist/server-shell/cli/managed-host.d.ts +16 -1
  42. package/dist/server-shell/cli/managed-host.js +191 -23
  43. package/dist/server-shell/cli/relay-registration.d.ts +4 -1
  44. package/dist/server-shell/cli/relay-registration.js +36 -7
  45. package/dist/server-shell/cli/runtime-commands.d.ts +5 -1
  46. package/dist/server-shell/cli/runtime-commands.js +9 -3
  47. package/dist/server-shell/http/host-tunnel.d.ts +5 -0
  48. package/dist/server-shell/http/host-tunnel.js +109 -0
  49. package/dist/server-shell/http/routes/local-control.d.ts +2 -0
  50. package/dist/server-shell/http/routes/local-control.js +2 -0
  51. package/dist/server-shell/http/routes/project-api/invalidations.d.ts +0 -1
  52. package/dist/server-shell/http/routes/project-api/invalidations.js +0 -3
  53. package/dist/server-shell/http/routes/project-api/messages-routes.js +32 -63
  54. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +8 -4
  55. package/dist/server-shell/http/routes/project-api/openapi.d.ts +8 -4
  56. package/dist/server-shell/http/routes/project-api/reference-files-routes.js +25 -94
  57. package/dist/server-shell/http/routes/project-api/reference-files.d.ts +1 -11
  58. package/dist/server-shell/http/routes/project-api/reference-files.js +2 -87
  59. package/dist/server-shell/http/routes/project-api/schemas.d.ts +8 -4
  60. package/dist/server-shell/http/routes/project-api/schemas.js +8 -3
  61. package/dist/server-shell/http/routes/project-api/session-requirements.js +34 -2
  62. package/dist/server-shell/http/routes/project-api/types.d.ts +3 -1
  63. package/dist/server-shell/http/routes/project-api/workspace-projections.js +8 -2
  64. package/dist/server-shell/local-console/package-update-hosts.js +4 -1
  65. package/dist/server-shell/local-console/project-service.d.ts +11 -2
  66. package/dist/server-shell/local-console/project-service.js +36 -8
  67. package/dist/server-shell/local-console/server.js +6 -1
  68. package/dist/server-shell/local-console/session.d.ts +4 -0
  69. package/dist/server-shell/local-console/session.js +14 -0
  70. package/dist/server-shell/session/relay-session-context.d.ts +12 -1
  71. package/dist/server-shell/session/relay-session-context.js +19 -1
  72. package/migrations/0016_external_source_mappings.sql +141 -0
  73. package/migrations/0017_platform_outbound_intents.sql +31 -0
  74. package/migrations/README.md +3 -1
  75. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +26 -1
  76. package/node_modules/@tutti/relay-client/dist/host-control.js +62 -0
  77. package/node_modules/@tutti/relay-client/dist/tunnel-frames.js +32 -6
  78. package/node_modules/@tutti/relay-client/dist/tunnel-types.d.ts +5 -1
  79. package/node_modules/@tutti/shared/dist/domain/index.d.ts +9 -0
  80. package/node_modules/@tutti/shared/dist/domain/index.js +14 -0
  81. package/node_modules/@tutti/shared/dist/ids/index.d.ts +6 -0
  82. package/node_modules/@tutti/shared/dist/ids/index.js +4 -0
  83. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +4 -0
  84. package/node_modules/@tutti/shared/dist/schemas/api/index.js +2 -0
  85. package/node_modules/@tutti/shared/dist/schemas/api/local-console-account-handoffs.d.ts +13 -0
  86. package/node_modules/@tutti/shared/dist/schemas/api/local-console-account-handoffs.js +16 -0
  87. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +43 -0
  88. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +44 -0
  89. package/node_modules/@tutti/shared/dist/schemas/domain/index.d.ts +7 -0
  90. package/node_modules/@tutti/shared/dist/schemas/domain/index.js +10 -0
  91. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +29 -0
  92. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +24 -1
  93. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +1346 -0
  94. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +516 -0
  95. package/node_modules/@tutti/shared/dist/utils/feishu-diagnostics/index.d.ts +10 -0
  96. package/node_modules/@tutti/shared/dist/utils/feishu-diagnostics/index.js +19 -0
  97. package/node_modules/@tutti/shared/dist/utils/index.d.ts +1 -0
  98. package/node_modules/@tutti/shared/dist/utils/index.js +1 -0
  99. package/node_modules/@tutti/shared/dist/utils/redaction/index.d.ts +1 -1
  100. package/node_modules/@tutti/shared/dist/utils/redaction/index.js +18 -0
  101. package/package.json +1 -1
  102. package/web/assets/{homepage-motion-scene-CPs4EYQq.js → homepage-motion-scene-DEVrQ9aj.js} +1 -1
  103. package/web/assets/{index-CxCTqGFO.css → index-6i9Uk7fS.css} +1 -1
  104. package/web/assets/index-Lxtst2sl.js +69 -0
  105. package/web/index.html +2 -2
  106. package/web/assets/index-D3lwP_UR.js +0 -69
@@ -1,11 +1,14 @@
1
+ import { type ProjectSurface, type ProjectSurfaceProfile } from "@tutti/shared/domain";
1
2
  import { type ProjectId, type RelayProjectRef } from "@tutti/shared/ids";
2
3
  import { type ConfigureProjectOpenAiProviderResult, type OpenAiProviderCredentialValidator } from "../../providers/openai/index.js";
3
4
  import { type FetchLike } from "../cli/host-runtime-endpoint.js";
5
+ import type { HostLaunchRelaySummary } from "../cli/relay-registration.js";
4
6
  import { type RuntimeProjectRow } from "../cli/runtime-commands.js";
5
7
  import { type LocalConsoleInvocationContext } from "./invocation-context.js";
6
8
  import { LocalConsoleOperationCoordinator } from "./operation-coordinator.js";
7
9
  export type LocalConsoleProject = {
8
10
  project_id: ProjectId;
11
+ surface_profile: ProjectSurfaceProfile;
9
12
  display_name: string;
10
13
  workspace_path: string;
11
14
  status: RuntimeProjectRow["status"];
@@ -31,8 +34,13 @@ export type LocalConsoleProjectMetadataInput = {
31
34
  export type LocalConsoleLaunchResult = {
32
35
  project: LocalConsoleProject;
33
36
  project_created: boolean;
34
- join_url: string;
37
+ join_url?: string;
35
38
  join_token_expires_at?: string;
39
+ feishu_setup?: HostLaunchRelaySummary["feishu_setup"];
40
+ };
41
+ export type LocalConsoleAccountHandoff = {
42
+ token: string;
43
+ expires_at: string;
36
44
  };
37
45
  export type LocalConsoleOpenResult = {
38
46
  project: LocalConsoleProject;
@@ -79,7 +87,8 @@ export declare class LocalConsoleProjectService {
79
87
  workspacePath: string;
80
88
  project?: LocalConsoleProjectMetadataInput;
81
89
  provider?: LocalConsoleProviderInput;
82
- }, context: LocalConsoleInvocationContext): Promise<LocalConsoleLaunchResult>;
90
+ surface?: ProjectSurface;
91
+ }, context: LocalConsoleInvocationContext, accountHandoff?: LocalConsoleAccountHandoff): Promise<LocalConsoleLaunchResult>;
83
92
  openProject(projectId: string, context: LocalConsoleInvocationContext): Promise<LocalConsoleOpenResult>;
84
93
  refreshInvite(projectId: string, context: LocalConsoleInvocationContext): Promise<{
85
94
  join_url: string;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { readRelayProjectMembershipState } from "@tutti/relay-client";
4
+ import { createProjectSurfaceProfile, } from "@tutti/shared/domain";
4
5
  import { relayProjectRouteSegment } from "@tutti/shared/ids";
5
6
  import { configureProjectOpenAiProvider, discoverOpenAiModels, isOpenAiApiKey, readOpenAiProviderConfigProjection, validateOpenAiCredential, } from "../../providers/openai/index.js";
6
7
  import { formatCliErrorReason, LaunchError } from "../cli/errors.js";
@@ -8,7 +9,7 @@ import { createRuntimeEndpointProbe, waitForHostShutdown, } from "../cli/host-ru
8
9
  import { prepareLaunchProject, resolveLaunchLocalContext, resolveRelayUrl } from "../cli/launch.js";
9
10
  import { readHostLocalLaunchStatus, requestHostLocalIdleShutdown, rotateHostLocalInvite, } from "../cli/local-control-client.js";
10
11
  import { readFreshVisibleJoinUrl } from "../cli/join-url.js";
11
- import { readHostRegistrationSecret, readMachineProjectBinding, readMachineRuntimeEndpoint, } from "../cli/machine-local.js";
12
+ import { readHostRegistrationSecret, readMachineProjectBinding, readMachineRuntimeEndpoint, writeFeishuOwnerAccountHandoff, } from "../cli/machine-local.js";
12
13
  import { spawnDetachedHost, waitForManagedHostReady } from "../cli/managed-host.js";
13
14
  import { relayStatusFromLocalStatus } from "../cli/host-relay-status.js";
14
15
  import { resolveManagedProjectContext } from "../cli/project-resolver.js";
@@ -88,9 +89,12 @@ function validateProjectMetadata(input) {
88
89
  }
89
90
  function projectFromRuntimeRow(row, providerStatus, providerModel, metadata) {
90
91
  const currentVersion = readCliVersion();
91
- const openUrl = projectOpenUrl(metadata.relayUrl, row.relay_project_ref);
92
+ const openUrl = metadata.surfaceProfile.kind === "standalone"
93
+ ? projectOpenUrl(metadata.relayUrl, row.relay_project_ref)
94
+ : undefined;
92
95
  return {
93
96
  project_id: row.project_id,
97
+ surface_profile: metadata.surfaceProfile,
94
98
  display_name: row.display_name,
95
99
  workspace_path: row.workspace_root,
96
100
  status: row.status,
@@ -213,6 +217,7 @@ export class LocalConsoleProjectService {
213
217
  ...(activityAt === undefined ? {} : { activityAt }),
214
218
  ...(createdAt === undefined ? {} : { createdAt }),
215
219
  relayUrl: binding?.relay_url ?? relayUrlFromJoinUrl(row.join_url) ?? resolveRelayUrl(operation.env),
220
+ surfaceProfile: binding?.surface_profile ?? createProjectSurfaceProfile(),
216
221
  });
217
222
  });
218
223
  }
@@ -224,7 +229,9 @@ export class LocalConsoleProjectService {
224
229
  return project;
225
230
  }
226
231
  async #startManagedHost(options) {
227
- spawnDetachedHost({
232
+ const startup = spawnDetachedHost({
233
+ tuttiHome: options.tuttiHome,
234
+ projectId: options.projectId,
228
235
  workspaceRoot: options.workspaceRoot,
229
236
  env: options.env,
230
237
  inheritProcessEnv: false,
@@ -234,6 +241,7 @@ export class LocalConsoleProjectService {
234
241
  projectId: options.projectId,
235
242
  workspaceRoot: options.workspaceRoot,
236
243
  fetchImpl: this.#fetchImpl,
244
+ startupAttemptRef: startup.startup_attempt_ref,
237
245
  ...(options.inviteMode === undefined ? {} : { inviteMode: options.inviteMode }),
238
246
  });
239
247
  }
@@ -289,7 +297,7 @@ export class LocalConsoleProjectService {
289
297
  throw new LocalConsoleProjectError("provider_configuration_required", "This project does not have a valid saved Provider. Configure it in the launch form.");
290
298
  }
291
299
  }
292
- async launchProject(input, context) {
300
+ async launchProject(input, context, accountHandoff) {
293
301
  const workspacePath = validateWorkspacePath(input.workspacePath);
294
302
  const active = this.#launches.get(workspacePath);
295
303
  if (active !== undefined) {
@@ -304,14 +312,15 @@ export class LocalConsoleProjectService {
304
312
  ...(input.provider === undefined
305
313
  ? {}
306
314
  : { provider: validateProvider(input.provider) }),
307
- }, context))
315
+ ...(input.surface === undefined ? {} : { surface: input.surface }),
316
+ }, context, accountHandoff))
308
317
  .finally(() => {
309
318
  this.#launches.delete(workspacePath);
310
319
  });
311
320
  this.#launches.set(workspacePath, launch);
312
321
  return await launch;
313
322
  }
314
- async #launchProject(input, context) {
323
+ async #launchProject(input, context, accountHandoff) {
315
324
  const operation = this.#operationOptions(context);
316
325
  if (input.provider === undefined) {
317
326
  try {
@@ -335,6 +344,7 @@ export class LocalConsoleProjectService {
335
344
  ? {}
336
345
  : { projectDescription: input.project.description }),
337
346
  }),
347
+ ...(input.surface === undefined ? {} : { projectSurface: input.surface }),
338
348
  ...operation,
339
349
  });
340
350
  }
@@ -357,6 +367,16 @@ export class LocalConsoleProjectService {
357
367
  else if (preparation.provider_status !== "configured") {
358
368
  throw new LocalConsoleProjectError("provider_configuration_required", "This project does not have a valid saved Provider. Configure it in the launch form.");
359
369
  }
370
+ if (preparation.machine_local.binding.surface_profile.kind === "feishu" &&
371
+ accountHandoff !== undefined) {
372
+ writeFeishuOwnerAccountHandoff({
373
+ tuttiHome: preparation.tutti_home,
374
+ projectId: preparation.project_id,
375
+ token: accountHandoff.token,
376
+ expiresAt: accountHandoff.expires_at,
377
+ now: this.#now(),
378
+ });
379
+ }
360
380
  try {
361
381
  const ready = await this.#startManagedHost({
362
382
  workspaceRoot: preparation.workspace_root,
@@ -364,7 +384,8 @@ export class LocalConsoleProjectService {
364
384
  projectId: preparation.project_id,
365
385
  env: operation.env,
366
386
  });
367
- if (ready.join_url === undefined) {
387
+ if (preparation.machine_local.binding.surface_profile.kind === "standalone" &&
388
+ ready.join_url === undefined) {
368
389
  throw new LocalConsoleProjectError("relay_registration_failed", "The host started, but Relay did not return a visible join link.");
369
390
  }
370
391
  const projects = await this.listProjects(context);
@@ -375,10 +396,11 @@ export class LocalConsoleProjectService {
375
396
  return {
376
397
  project,
377
398
  project_created: preparation.project_identity_created,
378
- join_url: ready.join_url,
399
+ ...(ready.join_url === undefined ? {} : { join_url: ready.join_url }),
379
400
  ...(ready.join_token_expires_at === undefined
380
401
  ? {}
381
402
  : { join_token_expires_at: ready.join_token_expires_at }),
403
+ ...(ready.feishu_setup === undefined ? {} : { feishu_setup: ready.feishu_setup }),
382
404
  };
383
405
  }
384
406
  catch (error) {
@@ -388,6 +410,9 @@ export class LocalConsoleProjectService {
388
410
  async openProject(projectId, context) {
389
411
  return await this.#operations.runProjectOperation(async () => {
390
412
  const current = await this.#readProject(projectId, context);
413
+ if (current.surface_profile.kind !== "standalone") {
414
+ throw new LocalConsoleProjectError("project_surface_mismatch", "Feishu collaboration projects do not use the Standalone workspace entry.");
415
+ }
391
416
  if (current.open_url === undefined) {
392
417
  throw new LocalConsoleProjectError("project_open_unavailable", "This project does not have a Relay destination yet.");
393
418
  }
@@ -534,6 +559,9 @@ export class LocalConsoleProjectService {
534
559
  target: projectId,
535
560
  ...this.#operationOptions(context),
536
561
  });
562
+ if (project.binding?.surface_profile.kind !== "standalone") {
563
+ throw new LocalConsoleProjectError("project_surface_mismatch", "Feishu collaboration projects do not use Standalone invite links.");
564
+ }
537
565
  if (project.runtime_endpoint === null) {
538
566
  throw new LocalConsoleProjectError("host_not_running", "Launch this project before creating an invite link.");
539
567
  }
@@ -405,6 +405,10 @@ export function createLocalConsoleServer(options) {
405
405
  model: { type: "string", minLength: 1, maxLength: 240 },
406
406
  },
407
407
  },
408
+ surface: {
409
+ type: "string",
410
+ enum: ["standalone", "feishu"],
411
+ },
408
412
  },
409
413
  },
410
414
  },
@@ -414,7 +418,8 @@ export function createLocalConsoleServer(options) {
414
418
  workspacePath: request.body.workspace_path,
415
419
  ...(request.body.project === undefined ? {} : { project: request.body.project }),
416
420
  ...(request.body.provider === undefined ? {} : { provider: request.body.provider }),
417
- }, session.context);
421
+ ...(request.body.surface === undefined ? {} : { surface: request.body.surface }),
422
+ }, session.context, session.accountHandoff);
418
423
  });
419
424
  app.post(`${LOCAL_CONSOLE_API_BASE}/projects/:projectId/open`, async (request) => {
420
425
  const session = requireSession(request, true);
@@ -13,6 +13,10 @@ export type ExchangedLocalConsoleSession = {
13
13
  export type ReadLocalConsoleSession = {
14
14
  accountReady: boolean;
15
15
  accountDisplayName?: string;
16
+ accountHandoff?: {
17
+ token: string;
18
+ expires_at: string;
19
+ };
16
20
  csrfToken: string;
17
21
  context: LocalConsoleInvocationContext;
18
22
  };
@@ -111,6 +111,12 @@ export class LocalConsoleSessionRegistry {
111
111
  ...(normalizedAccountDisplayName === undefined
112
112
  ? {}
113
113
  : { accountDisplayName: normalizedAccountDisplayName }),
114
+ ...(accessToken.accountReady
115
+ ? {
116
+ accountHandoffToken: token,
117
+ accountHandoffExpiresAt: accessToken.expiresAt,
118
+ }
119
+ : {}),
114
120
  hash: contextHash,
115
121
  expiresAt: session.expiresAt,
116
122
  sessionHash,
@@ -147,6 +153,14 @@ export class LocalConsoleSessionRegistry {
147
153
  ...(context.accountDisplayName === undefined
148
154
  ? {}
149
155
  : { accountDisplayName: context.accountDisplayName }),
156
+ ...(context.accountHandoffToken === undefined || context.accountHandoffExpiresAt === undefined
157
+ ? {}
158
+ : {
159
+ accountHandoff: {
160
+ token: context.accountHandoffToken,
161
+ expires_at: new Date(context.accountHandoffExpiresAt).toISOString(),
162
+ },
163
+ }),
150
164
  csrfToken: session.csrfToken,
151
165
  context: context.context,
152
166
  };
@@ -1,5 +1,6 @@
1
- import { type RelaySessionContext } from "@tutti/shared/schemas/internal";
1
+ import { type FeishuPlatformSessionContext, type RelaySessionContext } from "@tutti/shared/schemas/internal";
2
2
  export declare const RELAY_SESSION_CONTEXT_METADATA_KEY = "relay_session_context";
3
+ export declare const FEISHU_PLATFORM_SESSION_CONTEXT_METADATA_KEY = "feishu_platform_session_context";
3
4
  export declare const TRUSTED_RELAY_METADATA_REQUEST_HEADER = "x-tutti-internal-relay-metadata-id";
4
5
  export type RelaySessionContextReadResult = {
5
6
  kind: "present";
@@ -9,6 +10,14 @@ export type RelaySessionContextReadResult = {
9
10
  } | {
10
11
  kind: "invalid";
11
12
  };
13
+ export type FeishuPlatformSessionContextReadResult = {
14
+ kind: "present";
15
+ context: FeishuPlatformSessionContext;
16
+ } | {
17
+ kind: "missing";
18
+ } | {
19
+ kind: "invalid";
20
+ };
12
21
  export declare class RelaySessionInvalidError extends Error {
13
22
  readonly statusCode = 401;
14
23
  readonly code = "relay_session_invalid";
@@ -22,4 +31,6 @@ export declare class TrustedRelaySessionMetadataStore {
22
31
  export declare function createTrustedRelaySessionMetadataStore(): TrustedRelaySessionMetadataStore;
23
32
  export declare function readRelaySessionContext(metadata: unknown): RelaySessionContextReadResult;
24
33
  export declare function requireRelaySessionContext(metadata: unknown): RelaySessionContext;
34
+ export declare function readFeishuPlatformSessionContext(metadata: unknown): FeishuPlatformSessionContextReadResult;
35
+ export declare function requireFeishuPlatformSessionContext(metadata: unknown): FeishuPlatformSessionContext;
25
36
  //# sourceMappingURL=relay-session-context.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { parseRelaySessionContext } from "@tutti/shared/schemas/internal";
2
+ import { parseFeishuPlatformSessionContext, parseRelaySessionContext, } from "@tutti/shared/schemas/internal";
3
3
  export const RELAY_SESSION_CONTEXT_METADATA_KEY = "relay_session_context";
4
+ export const FEISHU_PLATFORM_SESSION_CONTEXT_METADATA_KEY = "feishu_platform_session_context";
4
5
  export const TRUSTED_RELAY_METADATA_REQUEST_HEADER = "x-tutti-internal-relay-metadata-id";
5
6
  export class RelaySessionInvalidError extends Error {
6
7
  statusCode = 401;
@@ -46,4 +47,21 @@ export function requireRelaySessionContext(metadata) {
46
47
  }
47
48
  return result.context;
48
49
  }
50
+ export function readFeishuPlatformSessionContext(metadata) {
51
+ if (!isRecord(metadata) || !(FEISHU_PLATFORM_SESSION_CONTEXT_METADATA_KEY in metadata)) {
52
+ return { kind: "missing" };
53
+ }
54
+ const context = parseFeishuPlatformSessionContext(metadata[FEISHU_PLATFORM_SESSION_CONTEXT_METADATA_KEY]);
55
+ if (context === null) {
56
+ return { kind: "invalid" };
57
+ }
58
+ return { kind: "present", context };
59
+ }
60
+ export function requireFeishuPlatformSessionContext(metadata) {
61
+ const result = readFeishuPlatformSessionContext(metadata);
62
+ if (result.kind !== "present") {
63
+ throw new RelaySessionInvalidError();
64
+ }
65
+ return result.context;
66
+ }
49
67
  //# sourceMappingURL=relay-session-context.js.map
@@ -0,0 +1,141 @@
1
+ CREATE TABLE external_source_mappings (
2
+ platform TEXT NOT NULL,
3
+ installation_ref TEXT NOT NULL,
4
+ conversation_ref TEXT NOT NULL,
5
+ thread_ref TEXT,
6
+ parent_message_ref TEXT,
7
+ external_message_ref TEXT NOT NULL,
8
+ message_id TEXT NOT NULL UNIQUE,
9
+ binding_ref TEXT NOT NULL,
10
+ receipt_key TEXT NOT NULL UNIQUE,
11
+ external_subject_ref TEXT NOT NULL,
12
+ author_account_ref TEXT NOT NULL,
13
+ author_display_name TEXT NOT NULL,
14
+ external_created_at TEXT NOT NULL,
15
+ accepted_at TEXT NOT NULL,
16
+ initial_revision_ref TEXT NOT NULL,
17
+ initial_observed_at TEXT NOT NULL,
18
+ content_sha256 TEXT NOT NULL CHECK (
19
+ content_sha256 GLOB 'sha256:[0-9a-f]*' AND length(content_sha256) = 71
20
+ ),
21
+ adapter_version TEXT NOT NULL,
22
+ latest_revision_ref TEXT NOT NULL,
23
+ latest_external_updated_at TEXT NOT NULL,
24
+ external_deleted_at TEXT,
25
+ PRIMARY KEY (platform, installation_ref, external_message_ref),
26
+ FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT
27
+ );
28
+
29
+ CREATE INDEX external_source_mappings_conversation_order
30
+ ON external_source_mappings (
31
+ platform,
32
+ installation_ref,
33
+ conversation_ref,
34
+ accepted_at,
35
+ message_id
36
+ );
37
+
38
+ CREATE TABLE external_source_revision_observations (
39
+ platform TEXT NOT NULL,
40
+ installation_ref TEXT NOT NULL,
41
+ external_message_ref TEXT NOT NULL,
42
+ revision_ref TEXT NOT NULL,
43
+ observation_kind TEXT NOT NULL CHECK (
44
+ observation_kind IN ('created', 'edited', 'deleted')
45
+ ),
46
+ content_sha256 TEXT CHECK (
47
+ content_sha256 IS NULL
48
+ OR (content_sha256 GLOB 'sha256:[0-9a-f]*' AND length(content_sha256) = 71)
49
+ ),
50
+ external_updated_at TEXT NOT NULL,
51
+ observed_at TEXT NOT NULL,
52
+ adapter_version TEXT NOT NULL,
53
+ PRIMARY KEY (
54
+ platform,
55
+ installation_ref,
56
+ external_message_ref,
57
+ revision_ref
58
+ ),
59
+ FOREIGN KEY (platform, installation_ref, external_message_ref)
60
+ REFERENCES external_source_mappings (
61
+ platform,
62
+ installation_ref,
63
+ external_message_ref
64
+ )
65
+ ON DELETE RESTRICT,
66
+ CHECK (observation_kind != 'created' OR content_sha256 IS NOT NULL)
67
+ );
68
+
69
+ CREATE INDEX external_source_revision_observations_order
70
+ ON external_source_revision_observations (
71
+ platform,
72
+ installation_ref,
73
+ external_message_ref,
74
+ external_updated_at,
75
+ revision_ref
76
+ );
77
+
78
+ CREATE TRIGGER external_source_mapping_identity_immutable
79
+ BEFORE UPDATE OF
80
+ platform,
81
+ installation_ref,
82
+ conversation_ref,
83
+ thread_ref,
84
+ parent_message_ref,
85
+ external_message_ref,
86
+ message_id,
87
+ binding_ref,
88
+ receipt_key,
89
+ external_subject_ref,
90
+ author_account_ref,
91
+ author_display_name,
92
+ external_created_at,
93
+ accepted_at,
94
+ initial_revision_ref,
95
+ initial_observed_at,
96
+ content_sha256,
97
+ adapter_version
98
+ ON external_source_mappings
99
+ BEGIN
100
+ SELECT RAISE(ABORT, 'external source mapping evidence is immutable');
101
+ END;
102
+
103
+ CREATE TRIGGER external_source_mapping_no_delete
104
+ BEFORE DELETE ON external_source_mappings
105
+ BEGIN
106
+ SELECT RAISE(ABORT, 'external source mapping evidence is immutable');
107
+ END;
108
+
109
+ CREATE TRIGGER external_source_revision_observation_no_update
110
+ BEFORE UPDATE ON external_source_revision_observations
111
+ BEGIN
112
+ SELECT RAISE(ABORT, 'external source revision observations are append-only');
113
+ END;
114
+
115
+ CREATE TRIGGER external_source_revision_observation_no_delete
116
+ BEFORE DELETE ON external_source_revision_observations
117
+ BEGIN
118
+ SELECT RAISE(ABORT, 'external source revision observations are append-only');
119
+ END;
120
+
121
+ CREATE TRIGGER external_source_message_evidence_immutable
122
+ BEFORE UPDATE OF
123
+ scope_kind,
124
+ clarification_round_id,
125
+ author_kind,
126
+ author_ref,
127
+ author_display_name,
128
+ author_avatar_json,
129
+ message_kind,
130
+ body,
131
+ refs_json,
132
+ created_at
133
+ ON messages
134
+ WHEN EXISTS (
135
+ SELECT 1
136
+ FROM external_source_mappings
137
+ WHERE external_source_mappings.message_id = OLD.id
138
+ )
139
+ BEGIN
140
+ SELECT RAISE(ABORT, 'external source message evidence is immutable');
141
+ END;
@@ -0,0 +1,31 @@
1
+ CREATE TABLE platform_outbound_intents (
2
+ intent_ref TEXT PRIMARY KEY CHECK (intent_ref GLOB 'idem_*'),
3
+ platform TEXT NOT NULL CHECK (platform = 'feishu'),
4
+ installation_ref TEXT NOT NULL,
5
+ binding_ref TEXT NOT NULL,
6
+ trigger_message_id TEXT NOT NULL,
7
+ canonical_message_id TEXT NOT NULL UNIQUE,
8
+ disposition TEXT NOT NULL CHECK (
9
+ disposition IN ('pending', 'sending', 'retry_wait', 'handed_off', 'terminal')
10
+ ),
11
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
12
+ next_attempt_at TEXT,
13
+ last_attempt_at TEXT,
14
+ delivery_ref TEXT,
15
+ error_code TEXT,
16
+ created_at TEXT NOT NULL,
17
+ updated_at TEXT NOT NULL,
18
+ handed_off_at TEXT,
19
+ FOREIGN KEY (trigger_message_id) REFERENCES messages(id) ON DELETE RESTRICT,
20
+ FOREIGN KEY (canonical_message_id) REFERENCES messages(id) ON DELETE RESTRICT,
21
+ CHECK (
22
+ (disposition = 'handed_off' AND delivery_ref IS NOT NULL AND handed_off_at IS NOT NULL)
23
+ OR disposition != 'handed_off'
24
+ )
25
+ );
26
+
27
+ CREATE INDEX platform_outbound_intents_due
28
+ ON platform_outbound_intents (disposition, next_attempt_at, created_at, intent_ref);
29
+
30
+ CREATE INDEX platform_outbound_intents_trigger
31
+ ON platform_outbound_intents (trigger_message_id, created_at, intent_ref);
@@ -28,10 +28,12 @@
28
28
  - `0013_scratchpad_receipt_sources.sql`:新增当前 Scratchpad snapshot 与 submitted receipt 的 source message id 引用表,供 Timeline Task Accepted 追溯原始聊天依据。
29
29
  - `0014_scratchpad_task_compile_context.sql`:把 Scratchpad source state 收敛为纯主群聊周期 cursor,新增 durable active Task Compile Context 与完整 source refs,并把 Current Scratchpad / receipt sources 限定为主群聊。该开发阶段 migration 不迁移旧调试数据,已有本地开发数据库需要 reset / recreate。
30
30
  - `0015_control_plane_intents.sql`:新增 Control Plane durable dispatch intent,保存 bounded kind / owner / dedupe / lane、pending-to-running claim lease 与 terminal 状态,为 Clarification、Task Compile、Scratchpad、Reference Summary 和 Run 提供可恢复的 runtime handoff;同时用 immutable pending Task Compile submission row 固化 context capture 前的 workflow / main-chat cursor boundary,不保存 prompt、消息正文、provider output、secret 或 host path。
31
+ - `0016_external_source_mappings.sql`:新增external installation / message identity到既有`messages.id`的一对一mapping和append-only revision observation;mapped message evidence、mapping identity与observations由trigger / FK保护,不保存raw platform payload、credential或短期URL。
32
+ - `0017_platform_outbound_intents.sql`:新增Host-owned platform outbound intent;只引用canonical / trigger message与installation / binding locator,保存handoff attempt和terminal状态,不复制回复正文或保存conversation ref / credential。
31
33
 
32
34
  ## 当前实现状态
33
35
 
34
- - `0001_init.sql` 至 `0015_control_plane_intents.sql` 已存在,代表当前 host project SQLite schema contract。`0014` 明确采用开发数据库重建策略,不提供旧 Scratchpad source/cursor 数据兼容。
36
+ - `0001_init.sql` 至 `0017_platform_outbound_intents.sql` 已存在,代表当前 host project SQLite schema contract。`0014` 明确采用开发数据库重建策略,不提供旧 Scratchpad source/cursor 数据兼容;`0016`–`0017`是additive migration,不要求为既有internal Chat message或agent response回填external mapping / outbound intent。
35
37
  - SQLite adapter、migration runner、checksum 校验、store metadata 初始化、transaction helper 与 command recovery scan helper 已实现。
36
38
  - 协作 repository 与 Control Plane 已实现第一批业务读写;Phase 7 首批已复用 `0001_init.sql` 中的 `tasks.latest_run_result_id`、`task_details.result_json` 与 `run_results` 表完成 Run result / checks / promotion 摘要持久化。Control Plane recovery resolver 与 task-bound continuation / retry 复用现有 schema;新 Run 的 accepted checkpoint 业务 truth 继续写入最终 Run result / task detail result,Git tag / commit trailer 承担 crash recovery evidence。Timeline 默认从 `tasks`、`run_results` 与历史主群聊 `refs.worklist_feedback` 投影任务级审计节点,当前 task terminal Chat feedback 写入 `refs.task_result` 消息;`project_timeline_events` 只保留缺少稳定 record 的轻量可观测性事件和历史 reconcile 兼容记录。project workspace、working diff 与 ignored runtime state 都不进入 SQLite truth。`provider_usage_events` 只保存 token 计数、调用来源、模型名、记录时间和可空 anchor,不保存 provider secret、prompt 或 raw response。store 基础设施本身已经可以创建并迁移 `tutti.sqlite`。
37
39
 
@@ -1,10 +1,13 @@
1
1
  import { type HostConnectionRef, type IdempotencyKey, type ProjectId, type RelayProjectRef, type RelayUploadRef } from "@tutti/shared/ids";
2
+ import type { ProjectSurfaceProfile } from "@tutti/shared/domain";
2
3
  import type { RelayUploadPurpose, Sha256Digest } from "@tutti/shared/schemas/api";
4
+ import { type FeishuSetupProjection, type PlatformOutboundHandoffResponse, type PlatformOutboundTextIntent } from "@tutti/shared/schemas/internal";
3
5
  export type RelayHostControlFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
4
6
  export type RelayProjectProjection = {
5
7
  relay_project_ref: RelayProjectRef;
6
8
  display_name: string;
7
9
  host_status: "online" | "offline" | "unknown";
10
+ surface_profile?: ProjectSurfaceProfile;
8
11
  };
9
12
  export type HostConnectionProjection = {
10
13
  host_connection_ref: HostConnectionRef;
@@ -31,6 +34,7 @@ export type RegisterRelayHostConnectionPayload = {
31
34
  secret: string;
32
35
  };
33
36
  display_name: string;
37
+ surface_profile?: ProjectSurfaceProfile;
34
38
  host: {
35
39
  server_version: string;
36
40
  host_project_api_base_path: "/api/v1";
@@ -39,17 +43,24 @@ export type RegisterRelayHostConnectionPayload = {
39
43
  provider_config_query: boolean;
40
44
  browser_secret_write: false;
41
45
  repo_viewer: boolean;
46
+ integration_source_ingestion_v1?: boolean;
47
+ integration_outbound_v1?: boolean;
42
48
  };
43
49
  };
44
50
  join_token?: {
45
51
  mode: "reuse_active" | "rotate";
46
52
  ttl_seconds?: number;
47
53
  };
54
+ owner_bootstrap?: {
55
+ kind: "local_console_account_handoff_v1";
56
+ account_handoff_token: string;
57
+ };
48
58
  };
49
59
  export type RegisterRelayHostConnectionResult = {
50
60
  relay_project: RelayProjectProjection;
51
61
  host_connection: HostConnectionProjection;
52
- join_token: JoinTokenProjection;
62
+ join_token?: JoinTokenProjection;
63
+ feishu_setup?: FeishuSetupProjection;
53
64
  host_connection_token: string;
54
65
  };
55
66
  export type RegisterRelayHostConnectionResponse = {
@@ -122,18 +133,31 @@ export type ResolveRelayUploadResponse = {
122
133
  export type CompleteRelayUploadResponse = {
123
134
  upload: RelayUploadProjection;
124
135
  };
136
+ export type DeliverRelayPlatformOutboundOptions = {
137
+ relayUrl: string;
138
+ projectId: ProjectId;
139
+ relayProjectRef: RelayProjectRef;
140
+ hostConnectionRef: HostConnectionRef;
141
+ hostRegistrationSecret: string;
142
+ intent: PlatformOutboundTextIntent;
143
+ fetch?: RelayHostControlFetch;
144
+ timeoutMs?: number;
145
+ signal?: AbortSignal;
146
+ };
125
147
  export type RegisterRelayHostConnectionOptions = {
126
148
  relayUrl: string;
127
149
  projectId: ProjectId;
128
150
  hostRegistrationSecret: string;
129
151
  displayName: string;
130
152
  serverVersion: string;
153
+ surfaceProfile?: ProjectSurfaceProfile;
131
154
  commandId?: IdempotencyKey;
132
155
  issuedAt?: Date;
133
156
  joinToken?: {
134
157
  mode: "reuse_active" | "rotate";
135
158
  ttlSeconds?: number;
136
159
  };
160
+ accountHandoffToken?: string;
137
161
  capabilities?: Partial<RegisterRelayHostConnectionPayload["host"]["capabilities"]>;
138
162
  fetch?: RelayHostControlFetch;
139
163
  timeoutMs?: number;
@@ -198,4 +222,5 @@ export declare function archiveRelayProject(options: ArchiveRelayProjectOptions)
198
222
  export declare function readRelayProjectMembershipState(options: ReadRelayProjectMembershipStateOptions): Promise<ReadRelayProjectMembershipStateResponse>;
199
223
  export declare function resolveRelayUpload(options: ResolveRelayUploadOptions): Promise<ResolveRelayUploadResponse>;
200
224
  export declare function completeRelayUpload(options: CompleteRelayUploadOptions): Promise<CompleteRelayUploadResponse>;
225
+ export declare function deliverRelayPlatformOutbound(options: DeliverRelayPlatformOutboundOptions): Promise<PlatformOutboundHandoffResponse>;
201
226
  //# sourceMappingURL=host-control.d.ts.map