@xfey/tutti 0.1.104 → 0.1.106

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 (64) hide show
  1. package/README.md +2 -2
  2. package/dist/approvals/decision-command.d.ts +124 -0
  3. package/dist/approvals/decision-command.js +20 -0
  4. package/dist/approvals/index.d.ts +1 -0
  5. package/dist/approvals/index.js +1 -0
  6. package/dist/approvals/manager.d.ts +3 -1
  7. package/dist/approvals/manager.js +9 -0
  8. package/dist/collaboration-ingestion/index.d.ts +3 -0
  9. package/dist/collaboration-ingestion/index.js +3 -0
  10. package/dist/collaboration-ingestion/managed-reference-snapshots.d.ts +27 -0
  11. package/dist/collaboration-ingestion/managed-reference-snapshots.js +45 -0
  12. package/dist/collaboration-ingestion/managed-reference-sources.d.ts +20 -0
  13. package/dist/collaboration-ingestion/managed-reference-sources.js +250 -0
  14. package/dist/collaboration-ingestion/reference-attachments.d.ts +22 -0
  15. package/dist/collaboration-ingestion/reference-attachments.js +132 -0
  16. package/dist/collaboration-ingestion/reference-files.d.ts +3 -2
  17. package/dist/collaboration-ingestion/reference-files.js +1 -0
  18. package/dist/platform-outbound/coordinator.d.ts +5 -2
  19. package/dist/platform-outbound/coordinator.js +95 -2
  20. package/dist/platform-outbound/repository.d.ts +17 -0
  21. package/dist/platform-outbound/repository.js +190 -5
  22. package/dist/platform-outbound/types.d.ts +13 -3
  23. package/dist/platform-outbound/worker.js +30 -14
  24. package/dist/server-shell/cli/host-lifecycle.js +3 -0
  25. package/dist/server-shell/cli/host-server-runtime.d.ts +4 -1
  26. package/dist/server-shell/cli/host-server-runtime.js +128 -40
  27. package/dist/server-shell/cli/relay-registration.js +3 -0
  28. package/dist/server-shell/http/host-tunnel.d.ts +4 -1
  29. package/dist/server-shell/http/host-tunnel.js +174 -3
  30. package/dist/server-shell/http/routes/project-api/approval-routes.js +13 -20
  31. package/dist/server-shell/http/routes/project-api/staged-uploads.d.ts +6 -0
  32. package/dist/server-shell/http/routes/project-api/staged-uploads.js +9 -2
  33. package/dist/server-shell/local-console/project-service.d.ts +2 -0
  34. package/dist/server-shell/local-console/project-service.js +37 -0
  35. package/dist/server-shell/local-console/server.js +4 -0
  36. package/dist/workspace-ops/index.d.ts +2 -2
  37. package/dist/workspace-ops/index.js +1 -1
  38. package/dist/workspace-ops/reference-files.d.ts +14 -1
  39. package/dist/workspace-ops/reference-files.js +167 -2
  40. package/dist/workspace-ops/types.d.ts +24 -0
  41. package/migrations/0018_platform_outbound_events.sql +111 -0
  42. package/migrations/0019_platform_outbound_card_content.sql +76 -0
  43. package/migrations/0020_managed_reference_sources.sql +34 -0
  44. package/migrations/README.md +4 -1
  45. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +5 -2
  46. package/node_modules/@tutti/relay-client/dist/host-control.js +3 -0
  47. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +2 -2
  48. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  49. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +34 -0
  50. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +37 -0
  51. package/node_modules/@tutti/shared/dist/schemas/internal/feishu-user-credentials.d.ts +22 -0
  52. package/node_modules/@tutti/shared/dist/schemas/internal/feishu-user-credentials.js +37 -0
  53. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +1 -0
  54. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +1 -0
  55. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +1005 -49
  56. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +434 -14
  57. package/package.json +1 -1
  58. package/prompts/skills/read-references/README.md +1 -1
  59. package/prompts/skills/read-references/SKILL.md +4 -2
  60. package/web/assets/{homepage-motion-scene-DEVrQ9aj.js → homepage-motion-scene-tfFEdV9Y.js} +1 -1
  61. package/web/assets/index-B7ZGIvOv.js +69 -0
  62. package/web/assets/{index-6i9Uk7fS.css → index-C2Mmj25S.css} +1 -1
  63. package/web/index.html +2 -2
  64. package/web/assets/index-Lxtst2sl.js +0 -69
@@ -1,6 +1,6 @@
1
1
  import type { FastifyInstance } from "fastify";
2
2
  import { type ExecutionStatusProjection } from "@tutti/shared/schemas/api";
3
- import type { IntegrationDeliveryDisposition, TrustedConversationSourceIngestionCommand } from "@tutti/shared/schemas/internal";
3
+ import type { IntegrationApprovalDecisionDisposition, IntegrationDeliveryDisposition, IntegrationManagedReferenceDisposition, TrustedApprovalDecisionCommand, TrustedConversationSourceIngestionCommand, TrustedManagedReferenceIngestionCommand, TrustedReferenceAttachmentIngestionCommand } from "@tutti/shared/schemas/internal";
4
4
  import { type HostProjectStore } from "../../store/index.js";
5
5
  import type { createHostServer } from "../http/create-server.js";
6
6
  import type { HostLocalLaunchStatus } from "../http/routes/local-control.js";
@@ -19,6 +19,9 @@ export type HostServerHandle = {
19
19
  runtime_endpoint: MachineRuntimeEndpointRecord;
20
20
  trusted_relay_metadata_store: TrustedRelaySessionMetadataStore;
21
21
  integration_delivery: (command: TrustedConversationSourceIngestionCommand) => IntegrationDeliveryDisposition;
22
+ integration_approval_decision: (command: TrustedApprovalDecisionCommand) => IntegrationApprovalDecisionDisposition;
23
+ integration_reference_attachment: (command: TrustedReferenceAttachmentIngestionCommand) => Promise<IntegrationDeliveryDisposition>;
24
+ integration_managed_reference: (command: TrustedManagedReferenceIngestionCommand) => Promise<IntegrationManagedReferenceDisposition>;
22
25
  attachRelayConnectionManager: (manager: HostRelayConnectionManager) => void;
23
26
  closed: Promise<void>;
24
27
  close: () => Promise<void>;
@@ -2,11 +2,11 @@ import { basename, join } from "node:path";
2
2
  import { MAX_PROVIDER_MODEL_NAME_LENGTH, } from "@tutti/shared/schemas/api";
3
3
  import { serializeRuntimeLogLine } from "@tutti/shared/utils";
4
4
  import { completeRelayUpload, deliverRelayPlatformOutbound, RelayHostControlClientError, resolveRelayUpload, } from "@tutti/relay-client";
5
- import { ApprovalRequestManager } from "../../approvals/index.js";
5
+ import { ApprovalRequestManager, executeApprovalDecisionCommand } from "../../approvals/index.js";
6
6
  import { ArtifactPreviewManager } from "../../artifacts/index.js";
7
7
  import { ReadOnlyChatAssistant } from "../../chat-assistant/index.js";
8
8
  import { CheckProcessCoordinator } from "../../checks/index.js";
9
- import { ingestTrustedConversationSource } from "../../collaboration-ingestion/index.js";
9
+ import { ingestTrustedConversationSource, ingestTrustedManagedReference, ingestTrustedReferenceAttachment, } from "../../collaboration-ingestion/index.js";
10
10
  import { ensureInitialProjectWelcomeMessage } from "../../collaboration-state/index.js";
11
11
  import { Phase5ControlPlane } from "../../control-plane/index.js";
12
12
  import { resolveProjectOpenAiProcedureWorkflowRunner } from "../../control-plane/workflows/index.js";
@@ -21,6 +21,7 @@ import { openHostProjectStore } from "../../store/index.js";
21
21
  import { resolvePackagedWebStaticRoot } from "../http/static-web.js";
22
22
  import { AGENT_CONTEXT_BASE_PATH, AgentContextTokenRegistry, } from "../http/routes/agent-context.js";
23
23
  import { readHostRepoProjection } from "../http/routes/project-api.js";
24
+ import { abortDownloadedStagedUpload, cleanupDownloadedStagedUpload, downloadStagedUploadWithResolver, } from "../http/routes/project-api/staged-uploads.js";
24
25
  import { WorkspaceEventBus } from "../http/workspace-events.js";
25
26
  import { createTrustedRelaySessionMetadataStore, } from "../session/relay-session-context.js";
26
27
  import { LaunchError } from "./errors.js";
@@ -308,9 +309,60 @@ export async function startForegroundHostServer(options) {
308
309
  },
309
310
  });
310
311
  controlPlaneLogger.info(staleTempCleanup, "Codex app-server stale temp cleanup completed");
312
+ const platformOutboundWorker = options.preparation.machine_local.binding.surface_profile.kind === "feishu"
313
+ ? new HostPlatformOutboundWorker(store, {
314
+ deliver: async (item) => {
315
+ const connectionState = relayConnectionManager?.getState();
316
+ if (connectionState?.status !== "connected") {
317
+ throw new RelayHostControlClientError("relay_unavailable", "Relay is not connected for platform outbound handoff", true);
318
+ }
319
+ const hostSecret = readHostRegistrationSecret(options.preparation.tutti_home, options.preparation.project_id);
320
+ const relayProjectRef = connectionState.connection.relay_project_ref;
321
+ return await deliverRelayPlatformOutbound({
322
+ relayUrl: options.preparation.relay_url,
323
+ projectId: options.preparation.project_id,
324
+ relayProjectRef,
325
+ hostConnectionRef: connectionState.connection.host_connection_ref,
326
+ hostRegistrationSecret: hostSecret.secret,
327
+ intent: {
328
+ protocol_version: 1,
329
+ intent_ref: item.intent.intent_ref,
330
+ relay_project_ref: relayProjectRef,
331
+ installation: {
332
+ platform: item.intent.platform,
333
+ installation_ref: item.intent.installation_ref,
334
+ },
335
+ binding_ref: item.intent.binding_ref,
336
+ ...(item.intent.canonical_message_id === null
337
+ ? {}
338
+ : { canonical_message_ref: item.intent.canonical_message_id }),
339
+ ...(item.intent.trigger_message_id === null
340
+ ? {}
341
+ : { trigger_message_ref: item.intent.trigger_message_id }),
342
+ content: item.content,
343
+ event: {
344
+ kind: item.intent.event_kind,
345
+ event_key: item.intent.event_key,
346
+ subject: {
347
+ kind: item.intent.subject_kind,
348
+ ref: item.intent.subject_ref,
349
+ },
350
+ target: { kind: item.intent.target_kind },
351
+ },
352
+ created_at: item.intent.created_at,
353
+ attempt: item.intent.attempt_count,
354
+ },
355
+ });
356
+ },
357
+ }, platformOutboundLogger, { now })
358
+ : undefined;
359
+ const platformOutboundCoordinator = platformOutboundWorker === undefined
360
+ ? undefined
361
+ : new HostPlatformOutboundCoordinator(store, platformOutboundWorker, platformOutboundLogger, now);
311
362
  const approvalManager = new ApprovalRequestManager({
312
363
  events: workspaceEvents,
313
364
  now,
365
+ onPending: (request) => platformOutboundCoordinator?.captureApprovalRequest(request),
314
366
  });
315
367
  const codexInvocationCoordinator = new CodexAppServerInvocationCoordinator();
316
368
  const checkProcessCoordinator = new CheckProcessCoordinator();
@@ -364,43 +416,6 @@ export async function startForegroundHostServer(options) {
364
416
  onLifecycleIncident: procedureLifecycleIncident,
365
417
  }),
366
418
  });
367
- const platformOutboundWorker = options.preparation.machine_local.binding.surface_profile.kind === "feishu"
368
- ? new HostPlatformOutboundWorker(store, {
369
- deliver: async (item) => {
370
- const connectionState = relayConnectionManager?.getState();
371
- if (connectionState?.status !== "connected") {
372
- throw new RelayHostControlClientError("relay_unavailable", "Relay is not connected for platform outbound handoff", true);
373
- }
374
- const hostSecret = readHostRegistrationSecret(options.preparation.tutti_home, options.preparation.project_id);
375
- const relayProjectRef = connectionState.connection.relay_project_ref;
376
- return await deliverRelayPlatformOutbound({
377
- relayUrl: options.preparation.relay_url,
378
- projectId: options.preparation.project_id,
379
- relayProjectRef,
380
- hostConnectionRef: connectionState.connection.host_connection_ref,
381
- hostRegistrationSecret: hostSecret.secret,
382
- intent: {
383
- protocol_version: 1,
384
- intent_ref: item.intent.intent_ref,
385
- relay_project_ref: relayProjectRef,
386
- installation: {
387
- platform: item.intent.platform,
388
- installation_ref: item.intent.installation_ref,
389
- },
390
- binding_ref: item.intent.binding_ref,
391
- canonical_message_ref: item.intent.canonical_message_id,
392
- trigger_message_ref: item.intent.trigger_message_id,
393
- content: { kind: "text", text: item.body },
394
- created_at: item.intent.created_at,
395
- attempt: item.intent.attempt_count,
396
- },
397
- });
398
- },
399
- }, platformOutboundLogger, { now })
400
- : undefined;
401
- const platformOutboundCoordinator = platformOutboundWorker === undefined
402
- ? undefined
403
- : new HostPlatformOutboundCoordinator(platformOutboundWorker, platformOutboundLogger, now);
404
419
  const chatAssistant = new ReadOnlyChatAssistant({
405
420
  project: chatAssistantProject,
406
421
  store,
@@ -508,9 +523,10 @@ export async function startForegroundHostServer(options) {
508
523
  ...(options.getLaunchStatus === undefined ? {} : { getLaunchStatus: options.getLaunchStatus }),
509
524
  };
510
525
  let app;
526
+ let resolveStagedUpload;
511
527
  try {
512
528
  const webStaticRoot = resolvePackagedWebStaticRoot();
513
- const resolveStagedUpload = createStagedUploadResolver({
529
+ resolveStagedUpload = createStagedUploadResolver({
514
530
  preparation: options.preparation,
515
531
  ...(options.getLaunchStatus === undefined
516
532
  ? {}
@@ -688,6 +704,78 @@ export async function startForegroundHostServer(options) {
688
704
  chatAssistant,
689
705
  now,
690
706
  }),
707
+ integration_reference_attachment: async (command) => await ingestTrustedReferenceAttachment({
708
+ store,
709
+ workspaceRoot: options.preparation.workspace_root,
710
+ events: workspaceEvents,
711
+ command,
712
+ resolveContent: async () => {
713
+ if (resolveStagedUpload === undefined) {
714
+ throw new Error("Staged upload resolver is unavailable");
715
+ }
716
+ const downloaded = await downloadStagedUploadWithResolver({
717
+ resolveStagedUpload,
718
+ expected: command.staged_upload,
719
+ purpose: "reference_file",
720
+ });
721
+ return {
722
+ content: downloaded.content,
723
+ complete: async () => await cleanupDownloadedStagedUpload(downloaded),
724
+ abort: async () => await abortDownloadedStagedUpload(downloaded),
725
+ };
726
+ },
727
+ scratchpadSource: {
728
+ notify: (message) => controlPlane.notifyScratchpadSourceChanged(message),
729
+ },
730
+ referenceSummaries: {
731
+ start: (paths) => controlPlane.startReferenceSummaryRefresh({ paths }),
732
+ },
733
+ now,
734
+ }),
735
+ integration_managed_reference: async (command) => await ingestTrustedManagedReference({
736
+ store,
737
+ workspaceRoot: options.preparation.workspace_root,
738
+ events: workspaceEvents,
739
+ command,
740
+ resolveContent: async () => {
741
+ if (resolveStagedUpload === undefined) {
742
+ throw new Error("Staged upload resolver is unavailable");
743
+ }
744
+ const downloaded = await downloadStagedUploadWithResolver({
745
+ resolveStagedUpload,
746
+ expected: command.staged_upload,
747
+ purpose: "reference_file",
748
+ });
749
+ return {
750
+ content: downloaded.content,
751
+ complete: async () => await cleanupDownloadedStagedUpload(downloaded),
752
+ abort: async () => await abortDownloadedStagedUpload(downloaded),
753
+ };
754
+ },
755
+ referenceSummaries: {
756
+ start: (paths) => controlPlane.startReferenceSummaryRefresh({ paths }),
757
+ },
758
+ now,
759
+ }),
760
+ integration_approval_decision: (command) => {
761
+ try {
762
+ return executeApprovalDecisionCommand({
763
+ db: store.db,
764
+ approvalManager,
765
+ commandId: command.command_id,
766
+ approvalId: command.approval_id,
767
+ decisionId: command.decision_id,
768
+ actor: {
769
+ account_ref: command.actor.account_ref,
770
+ display_name: command.actor.display_name,
771
+ },
772
+ now,
773
+ }).disposition;
774
+ }
775
+ catch {
776
+ return { kind: "invalid", reason_code: "approval_command_rejected" };
777
+ }
778
+ },
691
779
  attachRelayConnectionManager: (manager) => {
692
780
  relayConnectionManager = manager;
693
781
  platformOutboundCoordinator?.start();
@@ -37,7 +37,10 @@ export async function registerHostWithRelay(options) {
37
37
  joinToken: { mode: options.joinTokenMode ?? "rotate" },
38
38
  capabilities: {
39
39
  integration_source_ingestion_v1: true,
40
+ integration_attachment_ingestion_v1: true,
41
+ integration_managed_reference_ingestion_v1: true,
40
42
  integration_outbound_v1: true,
43
+ integration_approval_decision_v1: true,
41
44
  },
42
45
  fetch: options.fetchImpl,
43
46
  ...(options.signal === undefined ? {} : { signal: options.signal }),
@@ -1,6 +1,6 @@
1
1
  import type { FastifyInstance } from "fastify";
2
2
  import type { RelayHostTunnelRequestHandler, RelayHostTunnelStreamRequestHandler } from "@tutti/relay-client";
3
- import type { IntegrationDeliveryDisposition, TrustedConversationSourceIngestionCommand } from "@tutti/shared/schemas/internal";
3
+ import type { IntegrationApprovalDecisionDisposition, IntegrationDeliveryDisposition, IntegrationManagedReferenceDisposition, TrustedApprovalDecisionCommand, TrustedConversationSourceIngestionCommand, TrustedManagedReferenceIngestionCommand, TrustedReferenceAttachmentIngestionCommand } from "@tutti/shared/schemas/internal";
4
4
  import type { ProjectSurface } from "@tutti/shared/domain";
5
5
  import { type TrustedRelaySessionMetadataStore } from "../session/relay-session-context.js";
6
6
  export type HostProjectApiTunnelDispatchOptions = {
@@ -8,6 +8,9 @@ export type HostProjectApiTunnelDispatchOptions = {
8
8
  projectSurface?: ProjectSurface;
9
9
  trustedMetadataStore: TrustedRelaySessionMetadataStore;
10
10
  integrationDelivery?: (command: TrustedConversationSourceIngestionCommand) => IntegrationDeliveryDisposition;
11
+ integrationApprovalDecision?: (command: TrustedApprovalDecisionCommand) => IntegrationApprovalDecisionDisposition;
12
+ integrationReferenceAttachment?: (command: TrustedReferenceAttachmentIngestionCommand) => Promise<IntegrationDeliveryDisposition>;
13
+ integrationManagedReference?: (command: TrustedManagedReferenceIngestionCommand) => Promise<IntegrationManagedReferenceDisposition>;
11
14
  };
12
15
  export type HostProjectApiTunnelStreamDispatchOptions = {
13
16
  baseUrl: string;
@@ -1,5 +1,5 @@
1
- import { isTrustedConversationSourceIngestionCommand } from "@tutti/shared/schemas/internal";
2
- import { TRUSTED_INTEGRATION_INGESTION_PATH } from "../../collaboration-ingestion/index.js";
1
+ import { INTEGRATION_APPROVAL_DECISION_TUNNEL_PATH, INTEGRATION_MANAGED_REFERENCE_TUNNEL_PATH, INTEGRATION_REFERENCE_ATTACHMENT_TUNNEL_PATH, isTrustedApprovalDecisionCommand, isTrustedConversationSourceIngestionCommand, isTrustedManagedReferenceIngestionCommand, isTrustedReferenceAttachmentIngestionCommand, } from "@tutti/shared/schemas/internal";
2
+ import { TRUSTED_INTEGRATION_INGESTION_PATH, TRUSTED_INTEGRATION_REFERENCE_ATTACHMENT_PATH, } from "../../collaboration-ingestion/index.js";
3
3
  import { TRUSTED_RELAY_METADATA_REQUEST_HEADER, } from "../session/relay-session-context.js";
4
4
  function browserMetadataMatchesSurface(projectSurface, request) {
5
5
  return projectSurface === "standalone"
@@ -38,6 +38,30 @@ function integrationResponse(requestId, disposition) {
38
38
  body_base64: Buffer.from(JSON.stringify(body), "utf8").toString("base64"),
39
39
  };
40
40
  }
41
+ function integrationApprovalResponse(requestId, disposition) {
42
+ const body = {
43
+ protocol_version: 1,
44
+ disposition,
45
+ };
46
+ return {
47
+ request_id: requestId,
48
+ status_code: 200,
49
+ headers: { "content-type": "application/json; charset=utf-8" },
50
+ body_base64: Buffer.from(JSON.stringify(body), "utf8").toString("base64"),
51
+ };
52
+ }
53
+ function integrationManagedReferenceResponse(requestId, disposition) {
54
+ const body = {
55
+ protocol_version: 1,
56
+ disposition,
57
+ };
58
+ return {
59
+ request_id: requestId,
60
+ status_code: 200,
61
+ headers: { "content-type": "application/json; charset=utf-8" },
62
+ body_base64: Buffer.from(JSON.stringify(body), "utf8").toString("base64"),
63
+ };
64
+ }
41
65
  function sameInstallation(left, right) {
42
66
  return left.platform === right.platform && left.installation_ref === right.installation_ref;
43
67
  }
@@ -51,6 +75,15 @@ function dispatchIntegrationDelivery(options, request) {
51
75
  if (!("integration_delivery" in request.metadata)) {
52
76
  throw new TypeError("Integration delivery metadata is required");
53
77
  }
78
+ if (request.path === INTEGRATION_APPROVAL_DECISION_TUNNEL_PATH) {
79
+ return dispatchIntegrationApprovalDecision(options, request);
80
+ }
81
+ if (request.path === INTEGRATION_REFERENCE_ATTACHMENT_TUNNEL_PATH) {
82
+ return dispatchIntegrationReferenceAttachment(options, request);
83
+ }
84
+ if (request.path === INTEGRATION_MANAGED_REFERENCE_TUNNEL_PATH) {
85
+ return dispatchIntegrationManagedReference(options, request);
86
+ }
54
87
  if (request.method.toUpperCase() !== "POST" ||
55
88
  request.path !== TRUSTED_INTEGRATION_INGESTION_PATH) {
56
89
  return Promise.resolve(integrationResponse(request.request_id, {
@@ -73,7 +106,8 @@ function dispatchIntegrationDelivery(options, request) {
73
106
  }));
74
107
  }
75
108
  const metadata = request.metadata.integration_delivery;
76
- if (command.relay_project_ref !== metadata.relay_project_ref ||
109
+ if (!("receipt_key" in metadata) ||
110
+ command.relay_project_ref !== metadata.relay_project_ref ||
77
111
  command.binding_ref !== metadata.binding_ref ||
78
112
  command.receipt.receipt_key !== metadata.receipt_key ||
79
113
  command.actor.verified_at !== metadata.verified_at ||
@@ -92,6 +126,143 @@ function dispatchIntegrationDelivery(options, request) {
92
126
  }
93
127
  return Promise.resolve(integrationResponse(request.request_id, options.integrationDelivery(command)));
94
128
  }
129
+ async function dispatchIntegrationReferenceAttachment(options, request) {
130
+ if (!("integration_delivery" in request.metadata)) {
131
+ throw new TypeError("Integration delivery metadata is required");
132
+ }
133
+ if (request.method.toUpperCase() !== "POST" ||
134
+ request.path !== TRUSTED_INTEGRATION_REFERENCE_ATTACHMENT_PATH) {
135
+ return integrationResponse(request.request_id, {
136
+ kind: "invalid",
137
+ reason_code: "internal_route_invalid",
138
+ });
139
+ }
140
+ const payload = decodeRequestBody(request.body_base64);
141
+ let command;
142
+ try {
143
+ command = payload === undefined ? undefined : JSON.parse(payload.toString("utf8"));
144
+ }
145
+ catch {
146
+ command = undefined;
147
+ }
148
+ if (!isTrustedReferenceAttachmentIngestionCommand(command)) {
149
+ return integrationResponse(request.request_id, {
150
+ kind: "invalid",
151
+ reason_code: "trusted_attachment_command_invalid",
152
+ });
153
+ }
154
+ const metadata = request.metadata.integration_delivery;
155
+ if (!("receipt_key" in metadata) ||
156
+ command.relay_project_ref !== metadata.relay_project_ref ||
157
+ command.binding_ref !== metadata.binding_ref ||
158
+ command.receipt.receipt_key !== metadata.receipt_key ||
159
+ command.actor.verified_at !== metadata.verified_at ||
160
+ !sameInstallation(command.receipt.installation, metadata.installation)) {
161
+ return integrationResponse(request.request_id, {
162
+ kind: "invalid",
163
+ reason_code: "trusted_metadata_mismatch",
164
+ });
165
+ }
166
+ if (options.integrationReferenceAttachment === undefined) {
167
+ return integrationResponse(request.request_id, {
168
+ kind: "host_incompatible",
169
+ retryable: true,
170
+ reason_code: "integration_attachment_ingestion_unsupported",
171
+ });
172
+ }
173
+ return integrationResponse(request.request_id, await options.integrationReferenceAttachment(command));
174
+ }
175
+ async function dispatchIntegrationManagedReference(options, request) {
176
+ if (!("integration_delivery" in request.metadata)) {
177
+ throw new TypeError("Integration delivery metadata is required");
178
+ }
179
+ if (request.method.toUpperCase() !== "POST" ||
180
+ request.path !== INTEGRATION_MANAGED_REFERENCE_TUNNEL_PATH) {
181
+ return integrationManagedReferenceResponse(request.request_id, {
182
+ kind: "invalid",
183
+ reason_code: "internal_route_invalid",
184
+ });
185
+ }
186
+ const payload = decodeRequestBody(request.body_base64);
187
+ let command;
188
+ try {
189
+ command = payload === undefined ? undefined : JSON.parse(payload.toString("utf8"));
190
+ }
191
+ catch {
192
+ command = undefined;
193
+ }
194
+ if (!isTrustedManagedReferenceIngestionCommand(command)) {
195
+ return integrationManagedReferenceResponse(request.request_id, {
196
+ kind: "invalid",
197
+ reason_code: "trusted_managed_reference_command_invalid",
198
+ });
199
+ }
200
+ const metadata = request.metadata.integration_delivery;
201
+ if (!("operation_ref" in metadata) ||
202
+ command.relay_project_ref !== metadata.relay_project_ref ||
203
+ command.binding_ref !== metadata.binding_ref ||
204
+ command.operation_ref !== metadata.operation_ref ||
205
+ command.actor.verified_at !== metadata.verified_at ||
206
+ !sameInstallation(command.source.installation, metadata.installation)) {
207
+ return integrationManagedReferenceResponse(request.request_id, {
208
+ kind: "invalid",
209
+ reason_code: "trusted_metadata_mismatch",
210
+ });
211
+ }
212
+ if (options.integrationManagedReference === undefined) {
213
+ return integrationManagedReferenceResponse(request.request_id, {
214
+ kind: "host_incompatible",
215
+ retryable: true,
216
+ reason_code: "integration_managed_reference_ingestion_unsupported",
217
+ });
218
+ }
219
+ return integrationManagedReferenceResponse(request.request_id, await options.integrationManagedReference(command));
220
+ }
221
+ function dispatchIntegrationApprovalDecision(options, request) {
222
+ if (!("integration_delivery" in request.metadata)) {
223
+ throw new TypeError("Integration delivery metadata is required");
224
+ }
225
+ if (request.method.toUpperCase() !== "POST") {
226
+ return Promise.resolve(integrationApprovalResponse(request.request_id, {
227
+ kind: "invalid",
228
+ reason_code: "internal_route_invalid",
229
+ }));
230
+ }
231
+ const payload = decodeRequestBody(request.body_base64);
232
+ let command;
233
+ try {
234
+ command = payload === undefined ? undefined : JSON.parse(payload.toString("utf8"));
235
+ }
236
+ catch {
237
+ command = undefined;
238
+ }
239
+ if (!isTrustedApprovalDecisionCommand(command)) {
240
+ return Promise.resolve(integrationApprovalResponse(request.request_id, {
241
+ kind: "invalid",
242
+ reason_code: "trusted_command_invalid",
243
+ }));
244
+ }
245
+ const metadata = request.metadata.integration_delivery;
246
+ if (!("receipt_key" in metadata) ||
247
+ command.relay_project_ref !== metadata.relay_project_ref ||
248
+ command.binding_ref !== metadata.binding_ref ||
249
+ command.receipt.receipt_key !== metadata.receipt_key ||
250
+ command.actor.verified_at !== metadata.verified_at ||
251
+ !sameInstallation(command.receipt.installation, metadata.installation)) {
252
+ return Promise.resolve(integrationApprovalResponse(request.request_id, {
253
+ kind: "invalid",
254
+ reason_code: "trusted_metadata_mismatch",
255
+ }));
256
+ }
257
+ if (options.integrationApprovalDecision === undefined) {
258
+ return Promise.resolve(integrationApprovalResponse(request.request_id, {
259
+ kind: "host_incompatible",
260
+ retryable: true,
261
+ reason_code: "integration_approval_decision_unsupported",
262
+ }));
263
+ }
264
+ return Promise.resolve(integrationApprovalResponse(request.request_id, options.integrationApprovalDecision(command)));
265
+ }
95
266
  function injectHeaders(request) {
96
267
  const headers = {};
97
268
  for (const [name, value] of Object.entries(request.headers)) {
@@ -1,9 +1,9 @@
1
1
  import { ActiveApprovalProjectionSchema, DecideApprovalBodySchema, DecideApprovalResponseSchema, } from "@tutti/shared/schemas/api";
2
- import { executeIdempotentCommand } from "../../../command-idempotency/index.js";
2
+ import { executeApprovalDecisionCommand } from "../../../../approvals/index.js";
3
3
  import { HOST_PROJECT_API_BASE_PATH } from "./constants.js";
4
4
  import { approvalInvalidates } from "./invalidations.js";
5
- import { ApprovalRequestParamsSchema, } from "./schemas.js";
6
- import { requireHostProjectSession, requireProjectStore, } from "./session-requirements.js";
5
+ import { ApprovalRequestParamsSchema } from "./schemas.js";
6
+ import { requireHostProjectSession, requireProjectStore } from "./session-requirements.js";
7
7
  function readActiveApproval(options) {
8
8
  return options.approvalManager?.getActiveApproval() ?? { state: "none" };
9
9
  }
@@ -29,26 +29,19 @@ export function registerApprovalRoutes(app, options) {
29
29
  }, (request) => {
30
30
  const session = requireHostProjectSession(request, options);
31
31
  const store = requireProjectStore(options);
32
- const execution = executeIdempotentCommand({
32
+ const execution = executeApprovalDecisionCommand({
33
33
  db: store.db,
34
- command_id: request.body.command_id,
35
- command_name: "approval.decide",
36
- target_ref: request.params.approvalId,
37
- payload: {
38
- decision_id: request.body.payload.decision_id,
34
+ ...(options.approvalManager === undefined
35
+ ? {}
36
+ : { approvalManager: options.approvalManager }),
37
+ commandId: request.body.command_id,
38
+ approvalId: request.params.approvalId,
39
+ decisionId: request.body.payload.decision_id,
40
+ actor: {
41
+ account_ref: session.account.account_ref,
42
+ display_name: session.account.display_name,
39
43
  },
40
44
  ...(options.now === undefined ? {} : { now: options.now }),
41
- execute: () => options.approvalManager?.decide({
42
- approvalId: request.params.approvalId,
43
- payload: request.body.payload,
44
- actor: {
45
- account_ref: session.account.account_ref,
46
- display_name: session.account.display_name,
47
- },
48
- }) ?? {
49
- disposition: { kind: "stale_approval" },
50
- result: { approval: { state: "none" } },
51
- },
52
45
  });
53
46
  const response = {
54
47
  command_id: execution.command_id,
@@ -4,6 +4,12 @@ export type DownloadedStagedUpload = {
4
4
  resolved: ResolvedStagedUpload;
5
5
  content: Buffer;
6
6
  };
7
+ export type StagedUploadResolver = NonNullable<HostProjectApiRouteOptions["resolveStagedUpload"]>;
8
+ export declare function downloadStagedUploadWithResolver(options: {
9
+ resolveStagedUpload: StagedUploadResolver;
10
+ expected: StagedUploadReference;
11
+ purpose: RelayUploadPurpose;
12
+ }): Promise<DownloadedStagedUpload>;
7
13
  export declare function downloadStagedUpload(options: {
8
14
  routeOptions: HostProjectApiRouteOptions;
9
15
  expected: StagedUploadReference;
@@ -104,14 +104,14 @@ async function disposeResolvedStagedUpload(upload, status) {
104
104
  // Completion metadata is best-effort after the host has decided the terminal status.
105
105
  }
106
106
  }
107
- export async function downloadStagedUpload(options) {
107
+ export async function downloadStagedUploadWithResolver(options) {
108
108
  const maxBytes = maxStagedUploadBytesForPurpose(options.purpose);
109
109
  if (options.expected.size_bytes > maxBytes) {
110
110
  throw new HostProjectValidationError(`Upload content cannot exceed ${maxBytes} bytes`);
111
111
  }
112
112
  let resolved;
113
113
  try {
114
- resolved = await requireStagedUploadResolver(options.routeOptions)(options.expected.upload_id);
114
+ resolved = await options.resolveStagedUpload(options.expected.upload_id);
115
115
  }
116
116
  catch {
117
117
  throw new HostProjectValidationError("Upload session could not be resolved");
@@ -140,6 +140,13 @@ export async function downloadStagedUpload(options) {
140
140
  content,
141
141
  };
142
142
  }
143
+ export async function downloadStagedUpload(options) {
144
+ return downloadStagedUploadWithResolver({
145
+ resolveStagedUpload: requireStagedUploadResolver(options.routeOptions),
146
+ expected: options.expected,
147
+ purpose: options.purpose,
148
+ });
149
+ }
143
150
  export async function cleanupDownloadedStagedUpload(upload) {
144
151
  await disposeResolvedStagedUpload(upload.resolved, "completed");
145
152
  }
@@ -1,5 +1,6 @@
1
1
  import { type ProjectSurface, type ProjectSurfaceProfile } from "@tutti/shared/domain";
2
2
  import { type ProjectId, type RelayProjectRef } from "@tutti/shared/ids";
3
+ import type { FeishuSetupProjection } from "@tutti/shared/schemas/internal";
3
4
  import { type ConfigureProjectOpenAiProviderResult, type OpenAiProviderCredentialValidator } from "../../providers/openai/index.js";
4
5
  import { type FetchLike } from "../cli/host-runtime-endpoint.js";
5
6
  import type { HostLaunchRelaySummary } from "../cli/relay-registration.js";
@@ -73,6 +74,7 @@ export declare class LocalConsoleProjectService {
73
74
  now?: () => Date;
74
75
  });
75
76
  listProjects(context: LocalConsoleInvocationContext): Promise<LocalConsoleProject[]>;
77
+ readFeishuSetup(projectId: string, context: LocalConsoleInvocationContext): Promise<FeishuSetupProjection>;
76
78
  discoverModels(input: {
77
79
  baseUrl: string;
78
80
  apiKey: string;
@@ -228,6 +228,43 @@ export class LocalConsoleProjectService {
228
228
  }
229
229
  return project;
230
230
  }
231
+ async readFeishuSetup(projectId, context) {
232
+ const project = await this.#readProject(projectId, context);
233
+ if (project.surface_profile.kind !== "feishu") {
234
+ throw new LocalConsoleProjectError("project_surface_mismatch", "Tutti Workspace projects do not use a Feishu connection.");
235
+ }
236
+ let endpoint;
237
+ try {
238
+ endpoint = resolveManagedProjectContext({
239
+ target: projectId,
240
+ ...this.#operationOptions(context),
241
+ }).runtime_endpoint;
242
+ }
243
+ catch {
244
+ throw new LocalConsoleProjectError("feishu_setup_unavailable", "The Feishu connection state is temporarily unavailable.", 503);
245
+ }
246
+ if (endpoint === null) {
247
+ throw new LocalConsoleProjectError("host_not_running", "Resume this project before viewing its Feishu connection.", 409);
248
+ }
249
+ let status;
250
+ try {
251
+ status = await readHostLocalLaunchStatus({ endpoint, fetchImpl: this.#fetchImpl });
252
+ }
253
+ catch {
254
+ throw new LocalConsoleProjectError("feishu_setup_unavailable", "The running Host did not return its Feishu connection state.", 503);
255
+ }
256
+ if (status.project_id !== project.project_id || status.relay?.feishu_setup === undefined) {
257
+ throw new LocalConsoleProjectError("feishu_setup_unavailable", "The running Host did not return its Feishu connection state.", 503);
258
+ }
259
+ const setup = status.relay.feishu_setup;
260
+ if (setup.state === "pairing_required" && setup.visibility === "visible_once") {
261
+ const expiresAt = Date.parse(setup.expires_at);
262
+ if (!Number.isFinite(expiresAt) || expiresAt <= this.#now().getTime()) {
263
+ return { state: "pairing_required", visibility: "not_recoverable" };
264
+ }
265
+ }
266
+ return setup;
267
+ }
231
268
  async #startManagedHost(options) {
232
269
  const startup = spawnDetachedHost({
233
270
  tuttiHome: options.tuttiHome,
@@ -351,6 +351,10 @@ export function createLocalConsoleServer(options) {
351
351
  const session = requireSession(request);
352
352
  return { projects: await projects.listProjects(session.context) };
353
353
  });
354
+ app.get(`${LOCAL_CONSOLE_API_BASE}/projects/:projectId/feishu-setup`, async (request) => {
355
+ const session = requireSession(request);
356
+ return await projects.readFeishuSetup(request.params.projectId, session.context);
357
+ });
354
358
  app.get(`${LOCAL_CONSOLE_API_BASE}/heartbeat`, (request) => {
355
359
  requireSession(request);
356
360
  return { status: "ok" };