@xfey/tutti 0.1.105 → 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 (61) 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/workspace-ops/index.d.ts +2 -2
  34. package/dist/workspace-ops/index.js +1 -1
  35. package/dist/workspace-ops/reference-files.d.ts +14 -1
  36. package/dist/workspace-ops/reference-files.js +167 -2
  37. package/dist/workspace-ops/types.d.ts +24 -0
  38. package/migrations/0018_platform_outbound_events.sql +111 -0
  39. package/migrations/0019_platform_outbound_card_content.sql +76 -0
  40. package/migrations/0020_managed_reference_sources.sql +34 -0
  41. package/migrations/README.md +4 -1
  42. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +5 -2
  43. package/node_modules/@tutti/relay-client/dist/host-control.js +3 -0
  44. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +2 -2
  45. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  46. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +34 -0
  47. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +37 -0
  48. package/node_modules/@tutti/shared/dist/schemas/internal/feishu-user-credentials.d.ts +22 -0
  49. package/node_modules/@tutti/shared/dist/schemas/internal/feishu-user-credentials.js +37 -0
  50. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +1 -0
  51. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +1 -0
  52. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +1005 -49
  53. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +434 -14
  54. package/package.json +1 -1
  55. package/prompts/skills/read-references/README.md +1 -1
  56. package/prompts/skills/read-references/SKILL.md +4 -2
  57. package/web/assets/{homepage-motion-scene-BnRTgItL.js → homepage-motion-scene-tfFEdV9Y.js} +1 -1
  58. package/web/assets/index-B7ZGIvOv.js +69 -0
  59. package/web/assets/{index-CTMOjCgU.css → index-C2Mmj25S.css} +1 -1
  60. package/web/index.html +2 -2
  61. package/web/assets/index-CmzEr4-V.js +0 -69
package/README.md CHANGED
@@ -35,7 +35,7 @@
35
35
 
36
36
  - 已具备 Fastify host shell、后台 host manager CLI 套件、用户级 desktop integration / strict `tutti://projects` bridge、带 `service status / stop / restart`、卸载前 `shutdown`、并发启动锁、版本替换、strict terminal / desktop browser context、当前 runtime version bootstrap projection、idle / package update cleanup、全机readiness、exact npm global install、跨版本worker / recovery与私有日志的机器级 Local Console、Dev Runner、Host Project API、SSE、SQLite store、collaboration-state、workspace read-state、project timeline observability helper、Control Plane、Procedure / Run、workspace-ops、checks、Relay staged upload finalize、user skills 文件服务、artifacts 可视化产物查看层、current-generation Artifact feedback message / model formatter、Owner-only provider model discovery / update 和 OpenAI / Codex provider 接入。
37
37
  - 新 Host project 的空群聊会由 collaboration-state 幂等写入一条 Tutti 欢迎消息;它不经过 browser message command,不触发 `@tutti`、Chat read-state、Scratchpad 或 Timeline / execution 流程。
38
- - Feishu external source触发的主群聊Chat Assistant回复会在canonical message transaction中建立`platform_outbound_intents`,后台通过当前Host credential交给Relay;`handed_off`只表示Integration durable outbox已接受,不表示飞书最终delivered。
38
+ - Feishu external source触发的主群聊Chat Assistant回复会在canonical message transaction中建立`platform_outbound_intents`;新clarification / approval和Task terminal也会以稳定event identity进入同一Host durable worker。后台通过当前Host credential交给Relay,`handed_off`只表示Integration durable outbox已接受,不表示飞书最终delivered。
39
39
  - Host server 与 Local Console 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、health、host-local control 和 `/local-console/*` API 不被 SPA fallback 吞掉。
40
40
  - Root `pack:dry-run` 会先清理 build output、重新构建,再复制 `apps/web/dist` 到 package-local `web/`,生成临时 staging `@xfey/tutti` artifact,并校验 npm pack 文件列表包含 CLI、migrations、prompts、Web index、bundled `@tutti/shared` / `@tutti/relay-client` 以及它们的外部 runtime dependencies,同时排除源码、source map、日志、SQLite、`.env`、`.tutti-dev` 与测试产物。
41
41
  - Root `smoke:packed-cli` 同样从干净 build output 生成 staging `@xfey/tutti` tarball、在临时目录安装,并验证 SQLite native binding、`tutti --help` / desktop help / `tutti service --help` / `tutti service status` / `tutti shutdown --help` / `tutti launch --help` / `tutti archive --help` / `tutti --version`、desktop assets、migrations、prompts、Web static index、bundled internal dependencies、bundled external runtime dependencies、CLI QR dependency、仓库锁定的 `@openai/codex` binary 可执行性和临时全局安装路径;该 smoke 不需要 Tutti source repo 存在。
@@ -57,7 +57,7 @@
57
57
  - host 注册使用 machine-local `host_registration_secret`;该 secret 明文只可写入受限权限的 machine-local secret store,不写入目标项目 repo、Git local config 项目身份、SQLite 协作真相或日志,`binding.json` 只保存引用。
58
58
  - Relay registration 成功后只把 `relay_project_ref` 写回 machine-local binding;Standalone join URL只在host本机终端或当前host-local control内存状态中展示,不写入binding、runtime endpoint、SQLite协作真相或日志。Feishu project不签发join URL。
59
59
  - 普通 `tutti launch` 默认后台运行 host;Standalone完成页展示join URL,Feishu完成页只确认collaboration surface,然后CLI退出。`tutti launch --foreground` 保留前台诊断模式。后台spawn为每次启动生成唯一`startup_attempt_ref`,源码入口会先把`tsx` loader解析为不受目标project cwd影响的绝对URL;spawn、endpoint等待、Relay readiness、内部启动失败与timeout使用同一ref写入`TUTTI_HOME/logs/host.log`。Host HTTP request log 与 Control Plane / Run Pipeline runtime log也写入该文件,不写入目标项目 repo 或常规 CLI stdout。
60
- - Standalone Host只接受普通`relay_session_context`工作区请求;Feishu Host接受`feishu_platform_session_context` browser请求和固定internal path的`integration_delivery`。三种metadata严格三选一,cross-Surface browser request在进入Fastify业务route前403。Feishu browser只开放Project / Work精确只读allowlist;Host outbound使用独立host-control route,不借用browser transport。无状态`/api/v1/openapi.json`诊断仍可生成contract。
60
+ - Standalone Host只接受普通`relay_session_context`工作区请求;Feishu Host接受`feishu_platform_session_context` browser请求和固定internal path的`integration_delivery`。三种metadata严格三选一,cross-Surface browser request在进入Fastify业务route前403。Feishu browser只经Relay开放Project / Work / References / Artifacts精确query / command allowlist;References复用Host既有列表、Viewer文本与图片route,Artifacts复用既有projection、bodyless lease与raw preview route,不新增Host truth或upload route。Host仍执行相同actor、command idempotency与Control Plane逻辑。Host outbound使用独立host-control route,不借用browser transport。无状态`/api/v1/openapi.json`诊断仍可生成contract。
61
61
  - Reference 和 Skills 上传的最终写入由 Host Project API 完成;Host 只通过 Relay host-control resolve 获取短期 R2 URL 下载 staged object,不接收浏览器 base64 文件正文,也不持久化 presigned URL。
62
62
  - Host Project API的browser session context只信任Relay tunnel metadata中的Surface-matched variant;route handler不读取浏览器身份header,也不接受payload内身份字段。Feishu session在兼容projection中使用`client_session.transport = "feishu_platform_session"`,不代表创建Standalone Relay client session。
63
63
  - Artifact feedback 仍写入现有 `messages` truth并复用command idempotency;Host只在当前ready generation完全匹配时接受,Anchor技术字段仅保留审计ref,模型上下文使用bounded semantic formatter。
@@ -0,0 +1,124 @@
1
+ import type { ApprovalRequestId, IdempotencyKey } from "@tutti/shared/ids";
2
+ import type { SqliteDatabase } from "../store/index.js";
3
+ import type { ApprovalDecisionActor, ApprovalRequestManager } from "./manager.js";
4
+ export declare function executeApprovalDecisionCommand(input: {
5
+ db: SqliteDatabase;
6
+ approvalManager?: ApprovalRequestManager;
7
+ commandId: IdempotencyKey;
8
+ approvalId: ApprovalRequestId;
9
+ decisionId: string;
10
+ actor: ApprovalDecisionActor;
11
+ now?: () => Date;
12
+ }): import("../server-shell/command-idempotency/index.js").ExecuteIdempotentCommandResult<{
13
+ kind: "interrupted";
14
+ retry_with_new_command_id: true;
15
+ } | {
16
+ kind: "accepted";
17
+ } | {
18
+ kind: "stale_approval";
19
+ } | {
20
+ kind: "decision_unavailable";
21
+ }, {
22
+ approval?: {
23
+ state: "none";
24
+ } | {
25
+ request: {
26
+ task_id?: string;
27
+ activity_ref?: string;
28
+ expires_at?: string;
29
+ reason?: string;
30
+ file_change?: {
31
+ paths?: string[];
32
+ grant_root_label?: string;
33
+ };
34
+ permissions?: {
35
+ network?: {
36
+ enabled: boolean;
37
+ };
38
+ filesystem?: {
39
+ read?: string[];
40
+ write?: string[];
41
+ };
42
+ };
43
+ command?: {
44
+ network?: {
45
+ host: string;
46
+ protocol: string;
47
+ };
48
+ cwd_label?: string;
49
+ actions?: {
50
+ detail?: string;
51
+ label: string;
52
+ }[];
53
+ text: string;
54
+ };
55
+ title: string;
56
+ summary: string;
57
+ decisions: {
58
+ kind: "allow_once" | "allow_session" | "deny" | "cancel";
59
+ label: string;
60
+ tone: "neutral" | "primary" | "danger";
61
+ decision_id: string;
62
+ }[];
63
+ approval_id: string;
64
+ request_kind: "command_execution" | "file_change" | "permissions" | "legacy_exec_command" | "legacy_apply_patch";
65
+ requested_at: string;
66
+ };
67
+ state: "pending";
68
+ } | {
69
+ request: {
70
+ task_id?: string;
71
+ activity_ref?: string;
72
+ expires_at?: string;
73
+ reason?: string;
74
+ file_change?: {
75
+ paths?: string[];
76
+ grant_root_label?: string;
77
+ };
78
+ permissions?: {
79
+ network?: {
80
+ enabled: boolean;
81
+ };
82
+ filesystem?: {
83
+ read?: string[];
84
+ write?: string[];
85
+ };
86
+ };
87
+ command?: {
88
+ network?: {
89
+ host: string;
90
+ protocol: string;
91
+ };
92
+ cwd_label?: string;
93
+ actions?: {
94
+ detail?: string;
95
+ label: string;
96
+ }[];
97
+ text: string;
98
+ };
99
+ title: string;
100
+ summary: string;
101
+ decisions: {
102
+ kind: "allow_once" | "allow_session" | "deny" | "cancel";
103
+ label: string;
104
+ tone: "neutral" | "primary" | "danger";
105
+ decision_id: string;
106
+ }[];
107
+ approval_id: string;
108
+ request_kind: "command_execution" | "file_change" | "permissions" | "legacy_exec_command" | "legacy_apply_patch";
109
+ requested_at: string;
110
+ };
111
+ state: "resolved";
112
+ resolution: {
113
+ decided_by?: {
114
+ display_name: string;
115
+ account_ref: string;
116
+ };
117
+ label: string;
118
+ outcome: "accepted" | "declined" | "cancelled" | "expired";
119
+ decision_id: string;
120
+ decided_at: string;
121
+ };
122
+ };
123
+ }>;
124
+ //# sourceMappingURL=decision-command.d.ts.map
@@ -0,0 +1,20 @@
1
+ import { executeIdempotentCommand } from "../server-shell/command-idempotency/index.js";
2
+ export function executeApprovalDecisionCommand(input) {
3
+ return executeIdempotentCommand({
4
+ db: input.db,
5
+ command_id: input.commandId,
6
+ command_name: "approval.decide",
7
+ target_ref: input.approvalId,
8
+ payload: { decision_id: input.decisionId },
9
+ ...(input.now === undefined ? {} : { now: input.now }),
10
+ execute: () => input.approvalManager?.decide({
11
+ approvalId: input.approvalId,
12
+ payload: { decision_id: input.decisionId },
13
+ actor: input.actor,
14
+ }) ?? {
15
+ disposition: { kind: "stale_approval" },
16
+ result: { approval: { state: "none" } },
17
+ },
18
+ });
19
+ }
20
+ //# sourceMappingURL=decision-command.js.map
@@ -1,3 +1,4 @@
1
1
  export { ApprovalRequestManager, type ApprovalDecisionActor, type ApprovalRequestManagerOptions, type AwaitApprovalInput, type DecideApprovalInput, } from "./manager.js";
2
+ export { executeApprovalDecisionCommand } from "./decision-command.js";
2
3
  export { buildApprovalProjection, type ApprovalAppServerResponse, type ApprovalDecisionRuntime, type ApprovalProjectionBuildResult, type ApprovalProjectionContext, } from "./projections.js";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
1
  export { ApprovalRequestManager, } from "./manager.js";
2
+ export { executeApprovalDecisionCommand } from "./decision-command.js";
2
3
  export { buildApprovalProjection, } from "./projections.js";
3
4
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,5 @@
1
1
  import type { ApprovalRequestId, IdempotencyKey } from "@tutti/shared/ids";
2
- import type { ActiveApprovalProjection, DecideApprovalDisposition, DecideApprovalPayload, DecideApprovalResult } from "@tutti/shared/schemas/api";
2
+ import type { ActiveApprovalProjection, ApprovalRequestProjection, DecideApprovalDisposition, DecideApprovalPayload, DecideApprovalResult } from "@tutti/shared/schemas/api";
3
3
  import type { CodexAppServerJsonRpcServerRequest } from "../providers/openai/app-server/json-rpc.js";
4
4
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
5
5
  import { type ApprovalProjectionContext, type ApprovalAppServerResponse } from "./projections.js";
@@ -11,6 +11,7 @@ export type ApprovalRequestManagerOptions = {
11
11
  events?: WorkspaceEventBus;
12
12
  now?: () => Date;
13
13
  resolvedTtlMs?: number;
14
+ onPending?: (request: ApprovalRequestProjection) => void;
14
15
  };
15
16
  export type AwaitApprovalInput = {
16
17
  request: CodexAppServerJsonRpcServerRequest;
@@ -27,6 +28,7 @@ export declare class ApprovalRequestManager {
27
28
  private readonly events;
28
29
  private readonly now;
29
30
  private readonly resolvedTtlMs;
31
+ private readonly onPending;
30
32
  private active;
31
33
  private resolved;
32
34
  private resolvedTimer;
@@ -23,6 +23,7 @@ export class ApprovalRequestManager {
23
23
  events;
24
24
  now;
25
25
  resolvedTtlMs;
26
+ onPending;
26
27
  active = null;
27
28
  resolved = null;
28
29
  resolvedTimer;
@@ -30,6 +31,7 @@ export class ApprovalRequestManager {
30
31
  this.events = options.events;
31
32
  this.now = options.now ?? (() => new Date());
32
33
  this.resolvedTtlMs = options.resolvedTtlMs ?? DEFAULT_RESOLVED_TTL_MS;
34
+ this.onPending = options.onPending;
33
35
  }
34
36
  getActiveApproval() {
35
37
  if (this.active !== null) {
@@ -88,6 +90,13 @@ export class ApprovalRequestManager {
88
90
  resolve,
89
91
  timer,
90
92
  };
93
+ try {
94
+ this.onPending?.(build.request);
95
+ }
96
+ catch {
97
+ // Approval correctness and the app-server response must not depend on outbound notice
98
+ // capture. The platform worker records bounded diagnostics after successful capture.
99
+ }
91
100
  this.publish(this.active.projection);
92
101
  });
93
102
  }
@@ -1,5 +1,8 @@
1
1
  export { ingestTrustedConversationSource, TRUSTED_INTEGRATION_INGESTION_PATH, type IngestTrustedConversationSourceOptions, } from "./external-source.js";
2
2
  export { ingestMainChatMessage, type IngestMainChatMessageOptions, type MainChatMessageMaterialization, } from "./main-chat.js";
3
+ export { ingestManagedReferenceSnapshot, publishManagedReferenceSnapshotChanged, type IngestManagedReferenceSnapshotOptions, } from "./managed-reference-snapshots.js";
4
+ export { ingestTrustedManagedReference, type IngestTrustedManagedReferenceOptions, type ResolvedManagedReferenceContent, } from "./managed-reference-sources.js";
3
5
  export { ingestReferenceFile, REFERENCE_FILE_INGESTION_COMMAND_NAME, REFERENCE_FILE_INGESTION_TARGET_REF, toReferenceFileProjection, type IngestReferenceFileOptions, } from "./reference-files.js";
6
+ export { ingestTrustedReferenceAttachment, TRUSTED_INTEGRATION_REFERENCE_ATTACHMENT_PATH, type IngestTrustedReferenceAttachmentOptions, type ResolvedReferenceAttachmentContent, } from "./reference-attachments.js";
4
7
  export type { ChatAssistantEnqueuer, ReferenceSummaryStarter, ScratchpadSourceNotifier, TrustedCollaborationAuthor, } from "./types.js";
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,7 @@
1
1
  export { ingestTrustedConversationSource, TRUSTED_INTEGRATION_INGESTION_PATH, } from "./external-source.js";
2
2
  export { ingestMainChatMessage, } from "./main-chat.js";
3
+ export { ingestManagedReferenceSnapshot, publishManagedReferenceSnapshotChanged, } from "./managed-reference-snapshots.js";
4
+ export { ingestTrustedManagedReference, } from "./managed-reference-sources.js";
3
5
  export { ingestReferenceFile, REFERENCE_FILE_INGESTION_COMMAND_NAME, REFERENCE_FILE_INGESTION_TARGET_REF, toReferenceFileProjection, } from "./reference-files.js";
6
+ export { ingestTrustedReferenceAttachment, TRUSTED_INTEGRATION_REFERENCE_ATTACHMENT_PATH, } from "./reference-attachments.js";
4
7
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,27 @@
1
+ import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
2
+ import { type WriteManagedReferenceSnapshotResult } from "../workspace-ops/index.js";
3
+ import type { ReferenceSummaryStarter } from "./types.js";
4
+ export type IngestManagedReferenceSnapshotOptions = {
5
+ workspaceRoot: string;
6
+ events: WorkspaceEventBus;
7
+ path: string;
8
+ content: Buffer;
9
+ expectedBlobOid?: string;
10
+ note?: string;
11
+ referenceSummaries?: ReferenceSummaryStarter;
12
+ now?: () => Date;
13
+ };
14
+ export declare function publishManagedReferenceSnapshotChanged(options: {
15
+ events: WorkspaceEventBus;
16
+ path: string;
17
+ referenceSummaries?: ReferenceSummaryStarter;
18
+ }): void;
19
+ /**
20
+ * Applies content that a trusted platform adapter has already normalized.
21
+ *
22
+ * External source identity, revision discovery, download credentials and
23
+ * retries stay in the adapter. This service owns only the Host-side snapshot
24
+ * write, Reference invalidation and summary request.
25
+ */
26
+ export declare function ingestManagedReferenceSnapshot(options: IngestManagedReferenceSnapshotOptions): WriteManagedReferenceSnapshotResult;
27
+ //# sourceMappingURL=managed-reference-snapshots.d.ts.map
@@ -0,0 +1,45 @@
1
+ import { REFERENCE_DIRECTORY_PATH, writeManagedReferenceSnapshot, } from "../workspace-ops/index.js";
2
+ export function publishManagedReferenceSnapshotChanged(options) {
3
+ options.events.publish({
4
+ event_type: "query.invalidate",
5
+ payload: {
6
+ reason: "viewer_changed",
7
+ message: "Reference snapshot changed.",
8
+ },
9
+ invalidates: [
10
+ { kind: "viewer", path: REFERENCE_DIRECTORY_PATH },
11
+ { kind: "execution_status" },
12
+ { kind: "workspace" },
13
+ { kind: "bootstrap" },
14
+ ],
15
+ });
16
+ options.referenceSummaries?.start([options.path]);
17
+ }
18
+ /**
19
+ * Applies content that a trusted platform adapter has already normalized.
20
+ *
21
+ * External source identity, revision discovery, download credentials and
22
+ * retries stay in the adapter. This service owns only the Host-side snapshot
23
+ * write, Reference invalidation and summary request.
24
+ */
25
+ export function ingestManagedReferenceSnapshot(options) {
26
+ const result = writeManagedReferenceSnapshot({
27
+ workspaceRoot: options.workspaceRoot,
28
+ path: options.path,
29
+ content: options.content,
30
+ ...(options.expectedBlobOid === undefined ? {} : { expectedBlobOid: options.expectedBlobOid }),
31
+ ...(options.note === undefined ? {} : { note: options.note }),
32
+ ...(options.now === undefined ? {} : { now: options.now }),
33
+ });
34
+ if (result.disposition !== "unchanged") {
35
+ publishManagedReferenceSnapshotChanged({
36
+ events: options.events,
37
+ path: result.file.path,
38
+ ...(options.referenceSummaries === undefined
39
+ ? {}
40
+ : { referenceSummaries: options.referenceSummaries }),
41
+ });
42
+ }
43
+ return result;
44
+ }
45
+ //# sourceMappingURL=managed-reference-snapshots.js.map
@@ -0,0 +1,20 @@
1
+ import type { IntegrationManagedReferenceDisposition, TrustedManagedReferenceIngestionCommand } from "@tutti/shared/schemas/internal";
2
+ import type { HostProjectStore } from "../store/index.js";
3
+ import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
4
+ import type { ReferenceSummaryStarter } from "./types.js";
5
+ export type ResolvedManagedReferenceContent = {
6
+ content: Buffer;
7
+ complete: () => Promise<void>;
8
+ abort: () => Promise<void>;
9
+ };
10
+ export type IngestTrustedManagedReferenceOptions = {
11
+ store: HostProjectStore;
12
+ workspaceRoot: string;
13
+ events: WorkspaceEventBus;
14
+ command: TrustedManagedReferenceIngestionCommand;
15
+ resolveContent: () => Promise<ResolvedManagedReferenceContent>;
16
+ referenceSummaries?: ReferenceSummaryStarter;
17
+ now?: () => Date;
18
+ };
19
+ export declare function ingestTrustedManagedReference(options: IngestTrustedManagedReferenceOptions): Promise<IntegrationManagedReferenceDisposition>;
20
+ //# sourceMappingURL=managed-reference-sources.d.ts.map
@@ -0,0 +1,250 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isTrustedManagedReferenceIngestionCommand } from "@tutti/shared/schemas/internal";
3
+ import { readManagedReferenceSnapshotState, WorkspaceOpsError, writeManagedReferenceSnapshot, } from "../workspace-ops/index.js";
4
+ import { publishManagedReferenceSnapshotChanged } from "./managed-reference-snapshots.js";
5
+ function readSource(db, sourceRef) {
6
+ return (db
7
+ .prepare("SELECT * FROM managed_reference_sources WHERE source_ref = ?")
8
+ .get(sourceRef) ?? null);
9
+ }
10
+ function sourceIdentityMatches(row, command) {
11
+ return (row.adapter_key === command.source.adapter_key &&
12
+ row.reference_path === command.source.reference_path);
13
+ }
14
+ function duplicateDisposition(row) {
15
+ if (row.last_outcome === null ||
16
+ row.accepted_blob_oid === null ||
17
+ row.accepted_revision_ref === null) {
18
+ return null;
19
+ }
20
+ return {
21
+ kind: "duplicate",
22
+ outcome: row.last_outcome,
23
+ current_blob_oid: row.accepted_blob_oid,
24
+ };
25
+ }
26
+ function completedOperationMatches(row, command) {
27
+ return (sourceIdentityMatches(row, command) &&
28
+ row.accepted_revision_ref === command.remote_revision_ref &&
29
+ row.accepted_content_sha256 === command.content.sha256 &&
30
+ row.last_expected_blob_oid === (command.expected_host_blob_oid ?? null));
31
+ }
32
+ function prepareSource(db, command, nowIso) {
33
+ const existing = readSource(db, command.source.source_ref);
34
+ if (existing !== null) {
35
+ if (!sourceIdentityMatches(existing, command)) {
36
+ return { kind: "conflict", reason_code: "managed_reference_source_conflict" };
37
+ }
38
+ if (existing.last_operation_ref === command.operation_ref) {
39
+ if (!completedOperationMatches(existing, command)) {
40
+ return { kind: "conflict", reason_code: "managed_reference_operation_conflict" };
41
+ }
42
+ return (duplicateDisposition(existing) ?? {
43
+ kind: "host_unavailable",
44
+ retryable: true,
45
+ reason_code: "managed_reference_recovery_pending",
46
+ });
47
+ }
48
+ if (existing.pending_operation_ref === command.operation_ref) {
49
+ if (existing.pending_revision_ref !== command.remote_revision_ref ||
50
+ existing.pending_content_sha256 !== command.content.sha256 ||
51
+ existing.pending_expected_blob_oid !== (command.expected_host_blob_oid ?? null)) {
52
+ return { kind: "conflict", reason_code: "managed_reference_operation_conflict" };
53
+ }
54
+ return { row: existing };
55
+ }
56
+ if (existing.accepted_blob_oid !== (command.expected_host_blob_oid ?? null)) {
57
+ return { kind: "conflict", reason_code: "managed_reference_checkpoint_stale" };
58
+ }
59
+ }
60
+ else if (command.expected_host_blob_oid !== undefined) {
61
+ return { kind: "conflict", reason_code: "managed_reference_source_missing" };
62
+ }
63
+ db.prepare(`
64
+ INSERT INTO managed_reference_sources (
65
+ source_ref, adapter_key, reference_path, pending_operation_ref,
66
+ pending_revision_ref, pending_content_sha256, pending_expected_blob_oid,
67
+ created_at, updated_at
68
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
69
+ ON CONFLICT(source_ref) DO UPDATE SET
70
+ pending_operation_ref = excluded.pending_operation_ref,
71
+ pending_revision_ref = excluded.pending_revision_ref,
72
+ pending_content_sha256 = excluded.pending_content_sha256,
73
+ pending_expected_blob_oid = excluded.pending_expected_blob_oid,
74
+ updated_at = excluded.updated_at
75
+ `).run(command.source.source_ref, command.source.adapter_key, command.source.reference_path, command.operation_ref, command.remote_revision_ref, command.content.sha256, command.expected_host_blob_oid ?? null, nowIso, nowIso);
76
+ return {
77
+ row: readSource(db, command.source.source_ref),
78
+ };
79
+ }
80
+ function finalizeAccepted(options) {
81
+ options.db
82
+ .prepare(`
83
+ UPDATE managed_reference_sources
84
+ SET accepted_revision_ref = ?, accepted_blob_oid = ?, accepted_content_sha256 = ?,
85
+ last_operation_ref = ?, last_expected_blob_oid = ?, last_outcome = ?,
86
+ pending_operation_ref = NULL, pending_revision_ref = NULL,
87
+ pending_content_sha256 = NULL, pending_expected_blob_oid = NULL,
88
+ side_effects_pending = ?, last_checked_at = ?, last_accepted_at = ?, updated_at = ?
89
+ WHERE source_ref = ?
90
+ `)
91
+ .run(options.command.remote_revision_ref, options.blobOid, options.command.content.sha256, options.command.operation_ref, options.command.expected_host_blob_oid ?? null, options.outcome, options.sideEffectsPending ? 1 : 0, options.nowIso, options.nowIso, options.nowIso, options.command.source.source_ref);
92
+ return readSource(options.db, options.command.source.source_ref);
93
+ }
94
+ function clearSideEffectsPending(db, sourceRef, nowIso) {
95
+ db.prepare(`
96
+ UPDATE managed_reference_sources
97
+ SET side_effects_pending = 0, updated_at = ?
98
+ WHERE source_ref = ?
99
+ `).run(nowIso, sourceRef);
100
+ }
101
+ function sha256(content) {
102
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
103
+ }
104
+ function accepted(outcome, blobOid) {
105
+ return { kind: outcome, current_blob_oid: blobOid };
106
+ }
107
+ export async function ingestTrustedManagedReference(options) {
108
+ const command = options.command;
109
+ if (!isTrustedManagedReferenceIngestionCommand(command)) {
110
+ return { kind: "invalid", reason_code: "trusted_managed_reference_command_invalid" };
111
+ }
112
+ const now = options.now ?? (() => new Date());
113
+ const nowIso = now().toISOString();
114
+ const existing = readSource(options.store.db, command.source.source_ref);
115
+ if (existing?.last_operation_ref === command.operation_ref &&
116
+ !completedOperationMatches(existing, command)) {
117
+ return { kind: "conflict", reason_code: "managed_reference_operation_conflict" };
118
+ }
119
+ if (existing?.last_operation_ref === command.operation_ref &&
120
+ existing.side_effects_pending === 1 &&
121
+ existing.accepted_blob_oid !== null) {
122
+ publishManagedReferenceSnapshotChanged({
123
+ events: options.events,
124
+ path: existing.reference_path,
125
+ ...(options.referenceSummaries === undefined
126
+ ? {}
127
+ : { referenceSummaries: options.referenceSummaries }),
128
+ });
129
+ clearSideEffectsPending(options.store.db, existing.source_ref, now().toISOString());
130
+ return duplicateDisposition(existing);
131
+ }
132
+ let current;
133
+ try {
134
+ current = readManagedReferenceSnapshotState({
135
+ workspaceRoot: options.workspaceRoot,
136
+ path: command.source.reference_path,
137
+ now,
138
+ });
139
+ }
140
+ catch (error) {
141
+ return error instanceof WorkspaceOpsError && error.code === "conflict"
142
+ ? { kind: "conflict", reason_code: "managed_reference_workspace_changed" }
143
+ : { kind: "invalid", reason_code: "managed_reference_snapshot_rejected" };
144
+ }
145
+ if (existing === null && current.exists) {
146
+ return { kind: "conflict", reason_code: "managed_reference_path_conflict" };
147
+ }
148
+ const prepared = prepareSource(options.store.db, command, nowIso);
149
+ if (!("row" in prepared)) {
150
+ return prepared;
151
+ }
152
+ if (prepared.row.accepted_blob_oid !== null &&
153
+ (!current.exists ||
154
+ (current.blob_oid !== prepared.row.accepted_blob_oid &&
155
+ current.content_sha256 !== command.content.sha256))) {
156
+ return { kind: "conflict", reason_code: "managed_reference_workspace_changed" };
157
+ }
158
+ if (current.exists && current.content_sha256 === command.content.sha256) {
159
+ const outcome = prepared.row.accepted_blob_oid === null
160
+ ? "created"
161
+ : prepared.row.accepted_content_sha256 === command.content.sha256
162
+ ? "unchanged"
163
+ : "updated";
164
+ finalizeAccepted({
165
+ db: options.store.db,
166
+ command,
167
+ blobOid: current.blob_oid,
168
+ outcome,
169
+ sideEffectsPending: outcome !== "unchanged",
170
+ nowIso,
171
+ });
172
+ if (outcome !== "unchanged") {
173
+ publishManagedReferenceSnapshotChanged({
174
+ events: options.events,
175
+ path: command.source.reference_path,
176
+ ...(options.referenceSummaries === undefined
177
+ ? {}
178
+ : { referenceSummaries: options.referenceSummaries }),
179
+ });
180
+ clearSideEffectsPending(options.store.db, command.source.source_ref, now().toISOString());
181
+ }
182
+ return accepted(outcome, current.blob_oid);
183
+ }
184
+ let staged;
185
+ try {
186
+ staged = await options.resolveContent();
187
+ }
188
+ catch {
189
+ return {
190
+ kind: "host_unavailable",
191
+ retryable: true,
192
+ reason_code: "managed_reference_stage_unavailable",
193
+ };
194
+ }
195
+ try {
196
+ if (staged.content.byteLength !== command.content.size_bytes ||
197
+ sha256(staged.content) !== command.content.sha256) {
198
+ await staged.abort();
199
+ return { kind: "invalid", reason_code: "managed_reference_content_mismatch" };
200
+ }
201
+ const result = writeManagedReferenceSnapshot({
202
+ workspaceRoot: options.workspaceRoot,
203
+ path: command.source.reference_path,
204
+ content: staged.content,
205
+ ...(prepared.row.accepted_blob_oid === null
206
+ ? {}
207
+ : { expectedBlobOid: prepared.row.accepted_blob_oid }),
208
+ note: "Managed Feishu Docx snapshot.",
209
+ now,
210
+ });
211
+ finalizeAccepted({
212
+ db: options.store.db,
213
+ command,
214
+ blobOid: result.file.blob_oid,
215
+ outcome: result.disposition,
216
+ sideEffectsPending: result.disposition !== "unchanged",
217
+ nowIso: now().toISOString(),
218
+ });
219
+ try {
220
+ await staged.complete();
221
+ }
222
+ catch {
223
+ // The accepted Host snapshot is authoritative; staging cleanup is best effort.
224
+ }
225
+ if (result.disposition !== "unchanged") {
226
+ publishManagedReferenceSnapshotChanged({
227
+ events: options.events,
228
+ path: result.file.path,
229
+ ...(options.referenceSummaries === undefined
230
+ ? {}
231
+ : { referenceSummaries: options.referenceSummaries }),
232
+ });
233
+ clearSideEffectsPending(options.store.db, command.source.source_ref, now().toISOString());
234
+ }
235
+ return accepted(result.disposition, result.file.blob_oid);
236
+ }
237
+ catch (error) {
238
+ try {
239
+ await staged.abort();
240
+ }
241
+ catch {
242
+ // Preserve the original stable disposition.
243
+ }
244
+ if (error instanceof WorkspaceOpsError && error.code === "conflict") {
245
+ return { kind: "conflict", reason_code: "managed_reference_workspace_changed" };
246
+ }
247
+ return { kind: "invalid", reason_code: "managed_reference_snapshot_rejected" };
248
+ }
249
+ }
250
+ //# sourceMappingURL=managed-reference-sources.js.map
@@ -0,0 +1,22 @@
1
+ import type { IntegrationDeliveryDisposition, TrustedReferenceAttachmentIngestionCommand } from "@tutti/shared/schemas/internal";
2
+ import type { HostProjectStore } from "../store/index.js";
3
+ import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
4
+ import type { ReferenceSummaryStarter, ScratchpadSourceNotifier } from "./types.js";
5
+ export declare const TRUSTED_INTEGRATION_REFERENCE_ATTACHMENT_PATH = "/internal/integration/v1/reference-attachments";
6
+ export type ResolvedReferenceAttachmentContent = {
7
+ content: Buffer;
8
+ complete: () => Promise<void>;
9
+ abort: () => Promise<void>;
10
+ };
11
+ export type IngestTrustedReferenceAttachmentOptions = {
12
+ store: HostProjectStore;
13
+ workspaceRoot: string;
14
+ events: WorkspaceEventBus;
15
+ command: TrustedReferenceAttachmentIngestionCommand;
16
+ resolveContent: () => Promise<ResolvedReferenceAttachmentContent>;
17
+ scratchpadSource?: ScratchpadSourceNotifier;
18
+ referenceSummaries?: ReferenceSummaryStarter;
19
+ now?: () => Date;
20
+ };
21
+ export declare function ingestTrustedReferenceAttachment(options: IngestTrustedReferenceAttachmentOptions): Promise<IntegrationDeliveryDisposition>;
22
+ //# sourceMappingURL=reference-attachments.d.ts.map