@xfey/tutti 0.1.107 → 0.1.108

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 +1 -0
  2. package/dist/collaboration-ingestion/external-source.js +10 -1
  3. package/dist/control-plane/task-source-context-types.d.ts +2 -1
  4. package/dist/control-plane/task-source-context.js +7 -0
  5. package/dist/providers/openai/app-server/read-only-procedure.js +3 -0
  6. package/dist/providers/openai/app-server/skills.d.ts +4 -1
  7. package/dist/providers/openai/app-server/skills.js +12 -2
  8. package/dist/providers/openai/app-server/workspace-write-run.js +14 -10
  9. package/dist/reference-access/cache.d.ts +50 -0
  10. package/dist/reference-access/cache.js +268 -0
  11. package/dist/reference-access/read-evidence.d.ts +6 -0
  12. package/dist/reference-access/read-evidence.js +15 -0
  13. package/dist/reference-access/service.d.ts +26 -0
  14. package/dist/reference-access/service.js +42 -0
  15. package/dist/server-shell/cli/external-reference-runtime.d.ts +12 -0
  16. package/dist/server-shell/cli/external-reference-runtime.js +75 -0
  17. package/dist/server-shell/cli/host-server-runtime.js +17 -1
  18. package/dist/server-shell/cli/relay-registration.js +1 -0
  19. package/dist/server-shell/http/create-server.d.ts +2 -0
  20. package/dist/server-shell/http/create-server.js +5 -4
  21. package/dist/server-shell/http/routes/agent-context-auth.d.ts +2 -1
  22. package/dist/server-shell/http/routes/agent-context-auth.js +3 -1
  23. package/dist/server-shell/http/routes/agent-context-external-reference-routes.d.ts +4 -0
  24. package/dist/server-shell/http/routes/agent-context-external-reference-routes.js +83 -0
  25. package/dist/server-shell/http/routes/agent-context-shared.d.ts +2 -0
  26. package/dist/server-shell/http/routes/agent-context.js +3 -1
  27. package/dist/workspace-ops/project-workspace.js +6 -6
  28. package/dist/workspace-ops/reference-cache-policy.d.ts +6 -0
  29. package/dist/workspace-ops/reference-cache-policy.js +79 -0
  30. package/migrations/0021_external_reference_reads.sql +11 -0
  31. package/node_modules/@tutti/relay-client/dist/external-references.d.ts +11 -0
  32. package/node_modules/@tutti/relay-client/dist/external-references.js +63 -0
  33. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
  34. package/node_modules/@tutti/relay-client/dist/host-control.js +1 -0
  35. package/node_modules/@tutti/relay-client/dist/index.d.ts +1 -0
  36. package/node_modules/@tutti/relay-client/dist/index.js +1 -0
  37. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +6 -0
  38. package/node_modules/@tutti/shared/dist/schemas/api/external-references.d.ts +23 -0
  39. package/node_modules/@tutti/shared/dist/schemas/api/external-references.js +21 -0
  40. package/node_modules/@tutti/shared/dist/schemas/api/feishu-documents.d.ts +6 -1
  41. package/node_modules/@tutti/shared/dist/schemas/api/feishu-documents.js +16 -3
  42. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +3 -2
  43. package/node_modules/@tutti/shared/dist/schemas/api/index.js +2 -1
  44. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +24 -0
  45. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +2 -0
  46. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +7 -1
  47. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +17 -3
  48. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +6 -0
  49. package/node_modules/@tutti/shared/dist/schemas/internal/external-references.d.ts +109 -0
  50. package/node_modules/@tutti/shared/dist/schemas/internal/external-references.js +70 -0
  51. package/node_modules/@tutti/shared/dist/schemas/internal/feishu-documents.d.ts +11 -0
  52. package/node_modules/@tutti/shared/dist/schemas/internal/feishu-documents.js +5 -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 +36 -0
  56. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +3 -0
  57. package/package.json +1 -1
  58. package/prompts/skills/read-references/SKILL.md +32 -3
  59. package/web/assets/{homepage-motion-scene-CFs5rMWq.js → homepage-motion-scene-B6yPJnY8.js} +1 -1
  60. package/web/assets/{index-BmiLZmaV.js → index-aQnJ5JoI.js} +3 -3
  61. package/web/index.html +1 -1
@@ -0,0 +1,75 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { requestExternalReferences } from "@tutti/relay-client";
4
+ import { isExternalReferenceList, isExternalReferenceReadResult, } from "@tutti/shared/schemas/internal";
5
+ import { ExternalReferenceCache } from "../../reference-access/cache.js";
6
+ import { ExternalReferenceAccess, } from "../../reference-access/service.js";
7
+ import { recordExternalReferenceRead } from "../../reference-access/read-evidence.js";
8
+ import { downloadStagedUploadWithResolver, cleanupDownloadedStagedUpload, } from "../http/routes/project-api/staged-uploads.js";
9
+ import { readHostRegistrationSecret } from "./machine-local.js";
10
+ export function createExternalReferenceRuntime(options) {
11
+ const { preparation } = options;
12
+ async function request(action, invocation, sourceRef) {
13
+ const relay = options.getLaunchStatus().relay;
14
+ if (!relay)
15
+ throw new Error("reference_host_offline");
16
+ const secret = readHostRegistrationSecret(preparation.tutti_home, preparation.project_id);
17
+ return requestExternalReferences({
18
+ relayUrl: preparation.relay_url,
19
+ body: {
20
+ project_identity: { source: "git_config", project_id: preparation.project_id },
21
+ host_auth: { kind: "host_registration_secret_v1", secret: secret.secret },
22
+ relay_project_ref: relay.relay_project_ref,
23
+ host_connection_ref: relay.host_connection_ref,
24
+ request_ref: `eref_${randomUUID().replaceAll("-", "")}`,
25
+ invocation_ref: invocation.reference_invocation_ref,
26
+ action,
27
+ ...(sourceRef === undefined ? {} : { source_ref: sourceRef }),
28
+ },
29
+ });
30
+ }
31
+ return new ExternalReferenceAccess({
32
+ createCache: () => new ExternalReferenceCache({
33
+ workspaceRoot: preparation.workspace_root,
34
+ ledgerRoot: join(preparation.tutti_home, "projects", preparation.project_id, "reference-cache"),
35
+ }),
36
+ list: async (invocation) => {
37
+ const result = await request("list", invocation);
38
+ if (!isExternalReferenceList(result))
39
+ throw new Error("reference_list_invalid");
40
+ return result.items;
41
+ },
42
+ read: async (sourceRef, invocation) => {
43
+ const result = await request("read", invocation, sourceRef);
44
+ if (!isExternalReferenceReadResult(result) || result.source.source_ref !== sourceRef)
45
+ throw new Error("reference_read_invalid");
46
+ if (result.size_bytes === 0)
47
+ return {
48
+ content: Buffer.alloc(0),
49
+ revision: result.revision,
50
+ media_type: result.media_type,
51
+ };
52
+ if (!result.upload_id)
53
+ throw new Error("reference_read_invalid");
54
+ const downloaded = await downloadStagedUploadWithResolver({
55
+ resolveStagedUpload: options.resolveStagedUpload,
56
+ purpose: "reference_file",
57
+ expected: {
58
+ upload_id: result.upload_id,
59
+ file_name: `${sourceRef}.bin`,
60
+ size_bytes: result.size_bytes,
61
+ sha256: `sha256:${result.sha256}`,
62
+ media_type: result.media_type,
63
+ },
64
+ });
65
+ await cleanupDownloadedStagedUpload(downloaded);
66
+ return {
67
+ content: downloaded.content,
68
+ revision: result.revision,
69
+ media_type: result.media_type,
70
+ };
71
+ },
72
+ recordRead: (invocation, copy) => recordExternalReferenceRead(options.store.db, invocation, copy),
73
+ });
74
+ }
75
+ //# sourceMappingURL=external-reference-runtime.js.map
@@ -27,6 +27,7 @@ import { createTrustedRelaySessionMetadataStore, } from "../session/relay-sessio
27
27
  import { LaunchError } from "./errors.js";
28
28
  import { HostProjectRuntimeLifecycle } from "./host-project-runtime-lifecycle.js";
29
29
  import { readCliVersion } from "./version.js";
30
+ import { createExternalReferenceRuntime } from "./external-reference-runtime.js";
30
31
  import { relayStateFromLaunchStatus } from "./host-relay-status.js";
31
32
  import { appendHostLogLine, readHostRegistrationSecret, createMachineRuntimeEndpointToken, deleteMachineRuntimeEndpoint, ensureHostLogFile, getHostLogFilePath, updateMachineProjectDisplayNameSnapshot, writeMachineRuntimeEndpoint, } from "./machine-local.js";
32
33
  import { readProjectDescription, readProjectDisplayName, writeProjectDescription, writeProjectDisplayName, } from "./project-identity.js";
@@ -128,6 +129,7 @@ function createAgentContextRuntime(options) {
128
129
  baseUrl: new URL(AGENT_CONTEXT_BASE_PATH, options.baseUrl).toString(),
129
130
  issueToken: (input) => {
130
131
  const token = options.registry.issueToken({
132
+ ...(input.activityRef === undefined ? {} : { activityRef: input.activityRef }),
131
133
  projectId: options.projectId,
132
134
  selection: input.selectionName,
133
135
  scopes: input.scopes,
@@ -533,7 +535,21 @@ export async function startForegroundHostServer(options) {
533
535
  : { getLaunchStatus: options.getLaunchStatus }),
534
536
  });
535
537
  app = options.createServer({
536
- agentContext: { tokenRegistry: agentContextTokenRegistry },
538
+ agentContext: {
539
+ tokenRegistry: agentContextTokenRegistry,
540
+ ...(options.preparation.machine_local.binding.surface_profile.kind === "feishu" &&
541
+ options.getLaunchStatus !== undefined &&
542
+ resolveStagedUpload !== undefined
543
+ ? {
544
+ externalReferences: createExternalReferenceRuntime({
545
+ preparation: options.preparation,
546
+ store,
547
+ getLaunchStatus: options.getLaunchStatus,
548
+ resolveStagedUpload,
549
+ }),
550
+ }
551
+ : {}),
552
+ },
537
553
  serviceVersion: HOST_SERVER_VERSION,
538
554
  health: {
539
555
  readiness: {
@@ -37,6 +37,7 @@ export async function registerHostWithRelay(options) {
37
37
  joinToken: { mode: options.joinTokenMode ?? "rotate" },
38
38
  capabilities: {
39
39
  integration_source_ingestion_v1: true,
40
+ integration_external_references_v1: true,
40
41
  integration_attachment_ingestion_v1: true,
41
42
  integration_managed_reference_ingestion_v1: true,
42
43
  integration_outbound_v1: true,
@@ -3,9 +3,11 @@ import { type HostLocalControlOptions } from "./routes/local-control.js";
3
3
  import { type HostProjectApiRouteOptions } from "./routes/project-api.js";
4
4
  import { AgentContextTokenRegistry } from "./routes/agent-context.js";
5
5
  import { type HostWebStaticOptions } from "./static-web.js";
6
+ import type { ExternalReferenceAccess } from "../../reference-access/service.js";
6
7
  export type HostServerOptions = {
7
8
  agentContext?: false | {
8
9
  tokenRegistry?: AgentContextTokenRegistry;
10
+ externalReferences?: ExternalReferenceAccess;
9
11
  };
10
12
  health?: {
11
13
  readiness?: {
@@ -3,7 +3,7 @@ import { createRedactedLoggerOptions } from "./logger.js";
3
3
  import { registerHostHealthRoute } from "./routes/health.js";
4
4
  import { registerHostLocalControlRoutes, } from "./routes/local-control.js";
5
5
  import { registerHostProjectApiRoutes, } from "./routes/project-api.js";
6
- import { AgentContextTokenRegistry, registerAgentContextRoutes, } from "./routes/agent-context.js";
6
+ import { AgentContextTokenRegistry, registerAgentContextRoutes } from "./routes/agent-context.js";
7
7
  import { registerHostWebStaticRoutes } from "./static-web.js";
8
8
  import { registerApiErrorHandler } from "./validation.js";
9
9
  export function createHostServer(options = {}) {
@@ -12,9 +12,7 @@ export function createHostServer(options = {}) {
12
12
  : fastify({ logger: createRedactedLoggerOptions("tutti-host", options.logger) });
13
13
  registerApiErrorHandler(app);
14
14
  registerHostHealthRoute(app, {
15
- ...(options.health?.readiness === undefined
16
- ? {}
17
- : { readiness: options.health.readiness }),
15
+ ...(options.health?.readiness === undefined ? {} : { readiness: options.health.readiness }),
18
16
  now: options.now ?? (() => new Date()),
19
17
  serviceVersion: options.serviceVersion ?? "0.0.0",
20
18
  });
@@ -31,6 +29,9 @@ export function createHostServer(options = {}) {
31
29
  }
32
30
  if (options.agentContext !== false) {
33
31
  registerAgentContextRoutes(app, {
32
+ ...(options.agentContext?.externalReferences === undefined
33
+ ? {}
34
+ : { externalReferences: options.agentContext.externalReferences }),
34
35
  project: projectApiOptions.project,
35
36
  ...(projectApiOptions.store === undefined ? {} : { store: projectApiOptions.store }),
36
37
  ...(projectApiOptions.workspaceOps === undefined
@@ -3,6 +3,7 @@ import type { ActivityRef, ProjectId } from "@tutti/shared/ids";
3
3
  import type { CodexAppServerContextApiScope, CodexAppServerSkillSelectionName } from "../../../providers/openai/app-server/skills.js";
4
4
  export declare const DEFAULT_AGENT_CONTEXT_TOKEN_TTL_MS: number;
5
5
  export type AgentContextTokenRecord = {
6
+ reference_invocation_ref: string;
6
7
  token: string;
7
8
  project_id: ProjectId;
8
9
  activity_ref?: ActivityRef;
@@ -37,5 +38,5 @@ export declare function authorizeAgentContextRequest(request: FastifyRequest, op
37
38
  project_id: ProjectId;
38
39
  };
39
40
  tokenRegistry: AgentContextTokenRegistry;
40
- }, requiredScope: CodexAppServerContextApiScope): void;
41
+ }, requiredScope: CodexAppServerContextApiScope): AgentContextTokenRecord;
41
42
  //# sourceMappingURL=agent-context-auth.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { createEventRef } from "@tutti/shared/ids";
2
3
  export const DEFAULT_AGENT_CONTEXT_TOKEN_TTL_MS = 30 * 60 * 1000;
3
4
  export class AgentContextTokenRegistry {
4
5
  options;
@@ -11,6 +12,7 @@ export class AgentContextTokenRegistry {
11
12
  const ttlMs = input.ttlMs ?? this.options.ttlMs ?? DEFAULT_AGENT_CONTEXT_TOKEN_TTL_MS;
12
13
  const token = this.options.tokenFactory?.() ?? `agctx_${randomBytes(32).toString("base64url")}`;
13
14
  const record = {
15
+ reference_invocation_ref: createEventRef(),
14
16
  token,
15
17
  project_id: input.projectId,
16
18
  ...(input.activityRef === undefined ? {} : { activity_ref: input.activityRef }),
@@ -64,7 +66,7 @@ function bearerToken(request) {
64
66
  return match[1].trim();
65
67
  }
66
68
  export function authorizeAgentContextRequest(request, options, requiredScope) {
67
- options.tokenRegistry.validate({
69
+ return options.tokenRegistry.validate({
68
70
  token: bearerToken(request),
69
71
  projectId: options.project.project_id,
70
72
  requiredScope,
@@ -0,0 +1,4 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { AgentContextRouteOptions } from "./agent-context-shared.js";
3
+ export declare function registerAgentContextExternalReferenceRoutes(app: FastifyInstance, options: AgentContextRouteOptions): void;
4
+ //# sourceMappingURL=agent-context-external-reference-routes.d.ts.map
@@ -0,0 +1,83 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { redactText } from "@tutti/shared/utils";
3
+ import { ExternalReferenceTransportError } from "@tutti/relay-client";
4
+ import { ReferenceCacheError } from "../../../reference-access/cache.js";
5
+ import { WorkspaceOpsError } from "../../../workspace-ops/errors.js";
6
+ import { authorizeAgentContextRequest } from "./agent-context-auth.js";
7
+ import { AGENT_CONTEXT_BASE_PATH } from "./agent-context-shared.js";
8
+ export function registerAgentContextExternalReferenceRoutes(app, options) {
9
+ for (const action of ["list", "read"])
10
+ app.get(`${AGENT_CONTEXT_BASE_PATH}/references/external${action === "read" ? "/file" : ""}`, {
11
+ schema: {
12
+ querystring: action === "read"
13
+ ? Type.Object({ source_ref: Type.String({ pattern: "^external_ref_[a-f0-9]{32}$" }) }, { additionalProperties: false })
14
+ : Type.Object({}, { additionalProperties: false }),
15
+ },
16
+ }, async (request, reply) => {
17
+ reply.header("cache-control", "no-store");
18
+ const invocation = authorizeAgentContextRequest(request, options, "references:external");
19
+ const service = options.externalReferences;
20
+ if (!service)
21
+ return { kind: "unavailable", reason: "external_references_not_enabled" };
22
+ const sourceRef = request.query.source_ref;
23
+ if (action === "read" &&
24
+ (sourceRef === undefined || !/^external_ref_[a-f0-9]{32}$/u.test(sourceRef)))
25
+ return { kind: "unavailable", reason: "reference_source_invalid" };
26
+ try {
27
+ if (action === "list")
28
+ return { items: await service.list(invocation) };
29
+ const result = await service.read(invocation, sourceRef);
30
+ const { copy } = result;
31
+ request.log.info({
32
+ stage: "external_reference_read",
33
+ invocation_ref: invocation.reference_invocation_ref,
34
+ activity_ref: invocation.activity_ref,
35
+ source_ref: copy.source_ref,
36
+ revision: copy.revision,
37
+ sha256: copy.sha256,
38
+ cache_hit: result.cache_hit,
39
+ size_bytes: copy.size_bytes,
40
+ }, "External Reference working copy ready");
41
+ let text;
42
+ if (!result.content.includes(0)) {
43
+ try {
44
+ text = new TextDecoder("utf-8", { fatal: true }).decode(result.content);
45
+ }
46
+ catch {
47
+ /* Binary resources are available as a controlled local working copy. */
48
+ }
49
+ }
50
+ const limit = 128 * 1024;
51
+ const safeText = text === undefined ? undefined : Buffer.from(redactText(text), "utf8");
52
+ return {
53
+ kind: text === undefined ? "local_file" : "text",
54
+ ...copy,
55
+ cache_hit: result.cache_hit,
56
+ ...(safeText === undefined
57
+ ? {}
58
+ : {
59
+ text: safeText.subarray(0, limit).toString("utf8"),
60
+ truncated: safeText.byteLength > limit,
61
+ }),
62
+ };
63
+ }
64
+ catch (error) {
65
+ const reason = error instanceof ReferenceCacheError
66
+ ? error.code
67
+ : error instanceof ExternalReferenceTransportError
68
+ ? error.reasonCode
69
+ : error instanceof WorkspaceOpsError
70
+ ? "reference_cache_unsafe"
71
+ : "reference_read_unavailable";
72
+ request.log.warn({
73
+ stage: "external_reference_read",
74
+ invocation_ref: invocation.reference_invocation_ref,
75
+ activity_ref: invocation.activity_ref,
76
+ ...(sourceRef === undefined ? {} : { source_ref: sourceRef }),
77
+ reason_code: reason,
78
+ }, "External Reference read unavailable");
79
+ return { kind: "unavailable", reason };
80
+ }
81
+ });
82
+ }
83
+ //# sourceMappingURL=agent-context-external-reference-routes.js.map
@@ -1,6 +1,7 @@
1
1
  import type { HostProjectStore } from "../../../store/index.js";
2
2
  import type { HostProjectApiRouteOptions } from "./project-api.js";
3
3
  import type { AgentContextTokenRegistry } from "./agent-context-auth.js";
4
+ import type { ExternalReferenceAccess } from "../../../reference-access/service.js";
4
5
  export declare const AGENT_CONTEXT_BASE_PATH: "/internal/agent-context";
5
6
  export declare const MESSAGE_LIMIT_DEFAULT = 50;
6
7
  export declare const MESSAGE_LIMIT_MAX = 100;
@@ -27,6 +28,7 @@ export declare const USER_SKILL_MD_DEFAULT_MAX_CHARS = 20000;
27
28
  export declare const USER_SKILL_MD_MAX_CHARS = 100000;
28
29
  export type AgentContextRouteOptions = Pick<HostProjectApiRouteOptions, "project" | "store" | "workspaceOps" | "tuttiHome" | "now"> & {
29
30
  tokenRegistry: AgentContextTokenRegistry;
31
+ externalReferences?: ExternalReferenceAccess;
30
32
  };
31
33
  export declare function requireProjectStore(options: AgentContextRouteOptions): HostProjectStore;
32
34
  export declare function requireWorkspaceRoot(options: AgentContextRouteOptions): string;
@@ -1,8 +1,9 @@
1
1
  import { registerAgentContextMessageRoutes } from "./agent-context-messages-routes.js";
2
2
  import { registerAgentContextProjectDocRoutes } from "./agent-context-project-doc-routes.js";
3
3
  import { registerAgentContextReferenceRoutes } from "./agent-context-reference-routes.js";
4
+ import { registerAgentContextExternalReferenceRoutes } from "./agent-context-external-reference-routes.js";
4
5
  import { registerAgentContextSkillRoutes } from "./agent-context-skills-routes.js";
5
- import { AGENT_CONTEXT_BASE_PATH, } from "./agent-context-shared.js";
6
+ import { AGENT_CONTEXT_BASE_PATH } from "./agent-context-shared.js";
6
7
  import { registerAgentContextWorkspaceRoutes } from "./agent-context-workspace-routes.js";
7
8
  export { DEFAULT_AGENT_CONTEXT_TOKEN_TTL_MS, AgentContextTokenRegistry, } from "./agent-context-auth.js";
8
9
  export { AGENT_CONTEXT_BASE_PATH };
@@ -11,6 +12,7 @@ export function registerAgentContextRoutes(app, options) {
11
12
  registerAgentContextProjectDocRoutes(app, options);
12
13
  registerAgentContextWorkspaceRoutes(app, options);
13
14
  registerAgentContextReferenceRoutes(app, options);
15
+ registerAgentContextExternalReferenceRoutes(app, options);
14
16
  registerAgentContextSkillRoutes(app, options);
15
17
  }
16
18
  //# sourceMappingURL=agent-context.js.map
@@ -2,6 +2,7 @@ import { classifyRepositoryPathMetadata } from "@tutti/shared/utils";
2
2
  import { WorkspaceOpsError } from "./errors.js";
3
3
  import { runGit, runGitRaw, runGitText, stagedChangesExist, summarizeGitFailure, tryGitText, } from "./git.js";
4
4
  import { requireMainlineBranch } from "./mainline.js";
5
+ import { assertReferenceCacheIsolation } from "./reference-cache-policy.js";
5
6
  const MAX_RUN_DIFF_SUMMARY_BYTES = 32 * 1024;
6
7
  const ACCEPTED_TAG_PREFIX = "tutti/accepted/";
7
8
  function truncateText(text, maxBytes) {
@@ -48,6 +49,7 @@ function requireGitCommandSuccess(args, workspaceRoot) {
48
49
  return result;
49
50
  }
50
51
  export function collectProjectSourceDiff(workspaceRoot) {
52
+ assertReferenceCacheIsolation(workspaceRoot);
51
53
  const status = requireGitCommandSuccess(["status", "--porcelain=v1", "-z"], workspaceRoot);
52
54
  const changedPaths = parsePorcelainChangedPaths(status.stdout);
53
55
  const summaryParts = [];
@@ -80,12 +82,8 @@ function acceptedTagName(workspaceRoot, activityRef) {
80
82
  return tagName;
81
83
  }
82
84
  function nearestAcceptedCommit(workspaceRoot) {
83
- const refs = requireGitCommandSuccess([
84
- "for-each-ref",
85
- "--format=%(objectname) %(refname:short)",
86
- `refs/tags/${ACCEPTED_TAG_PREFIX}`,
87
- ], workspaceRoot).stdout
88
- .toString("utf8")
85
+ const refs = requireGitCommandSuccess(["for-each-ref", "--format=%(objectname) %(refname:short)", `refs/tags/${ACCEPTED_TAG_PREFIX}`], workspaceRoot)
86
+ .stdout.toString("utf8")
89
87
  .trim();
90
88
  if (refs.length === 0) {
91
89
  return null;
@@ -209,7 +207,9 @@ export function createAcceptedCheckpoint(options) {
209
207
  };
210
208
  }
211
209
  const inspection = inspectProjectWorkspace(options.workspaceRoot);
210
+ assertReferenceCacheIsolation(options.workspaceRoot);
212
211
  runGit(["add", "-A"], options.workspaceRoot);
212
+ assertReferenceCacheIsolation(options.workspaceRoot);
213
213
  if (!stagedChangesExist(options.workspaceRoot)) {
214
214
  throw new WorkspaceOpsError("validation_failed", "Accepted checkpoint has no staged source changes", { reason_code: "accepted_checkpoint_candidate_missing" });
215
215
  }
@@ -0,0 +1,6 @@
1
+ export declare const REFERENCE_CACHE_PATH = "docs/reference/.tutti-cache";
2
+ /** Only the exact managed namespace is eligible. Never adopt an existing directory. */
3
+ export declare function initializeReferenceCacheDirectory(workspaceRoot: string): string;
4
+ /** Fail closed on force-added cache or ignore drift; never unstage or delete user changes. */
5
+ export declare function assertReferenceCacheIsolation(workspaceRoot: string, required?: boolean): void;
6
+ //# sourceMappingURL=reference-cache-policy.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { WorkspaceOpsError } from "./errors.js";
4
+ import { runGit, runGitRaw, runGitText, tryGitText } from "./git.js";
5
+ export const REFERENCE_CACHE_PATH = "docs/reference/.tutti-cache";
6
+ const CONFIG_KEY = "tutti.reference-cache-path";
7
+ function reject() {
8
+ throw new WorkspaceOpsError("conflict", "Reference cache isolation is unavailable", {
9
+ reason_code: "reference_cache_unsafe",
10
+ });
11
+ }
12
+ /** Only the exact managed namespace is eligible. Never adopt an existing directory. */
13
+ export function initializeReferenceCacheDirectory(workspaceRoot) {
14
+ const root = realpathSync(workspaceRoot);
15
+ const configured = tryGitText(["config", "--local", "--get", CONFIG_KEY], root);
16
+ if (configured !== null && configured !== REFERENCE_CACHE_PATH)
17
+ reject();
18
+ let current = root;
19
+ for (const part of REFERENCE_CACHE_PATH.split("/")) {
20
+ current = join(current, part);
21
+ const stat = lstatSync(current, { throwIfNoEntry: false });
22
+ if (stat !== undefined) {
23
+ if (!stat.isDirectory() || stat.isSymbolicLink())
24
+ reject();
25
+ if (current === join(root, REFERENCE_CACHE_PATH) && configured === null)
26
+ reject();
27
+ }
28
+ }
29
+ if (runGitText(["ls-files", "--", REFERENCE_CACHE_PATH], root) !== "")
30
+ reject();
31
+ // Git-local exclusion preserves the project's own .gitignore and unrelated index state.
32
+ const gitPath = runGitText(["rev-parse", "--git-path", "info/exclude"], root);
33
+ const exclude = isAbsolute(gitPath) ? gitPath : resolve(root, gitPath);
34
+ const gitDirectory = realpathSync(dirname(dirname(exclude)));
35
+ const info = join(gitDirectory, "info");
36
+ const infoStat = lstatSync(info, { throwIfNoEntry: false });
37
+ if (infoStat !== undefined && (!infoStat.isDirectory() || infoStat.isSymbolicLink()))
38
+ reject();
39
+ mkdirSync(info, { recursive: true });
40
+ const excludeStat = lstatSync(exclude, { throwIfNoEntry: false });
41
+ if (excludeStat !== undefined && (!excludeStat.isFile() || excludeStat.isSymbolicLink()))
42
+ reject();
43
+ const previous = excludeStat === undefined ? "" : readFileSync(exclude, "utf8");
44
+ const rule = `/${REFERENCE_CACHE_PATH}/`;
45
+ if (!previous.split(/\r?\n/u).includes(rule)) {
46
+ writeFileSync(exclude, `${previous}${previous.endsWith("\n") ? "" : "\n"}${rule}\n`, {
47
+ flag: excludeStat === undefined ? "wx" : "w",
48
+ mode: 0o600,
49
+ });
50
+ }
51
+ runGit(["config", "--local", CONFIG_KEY, REFERENCE_CACHE_PATH], root);
52
+ assertReferenceCacheIsolation(root);
53
+ mkdirSync(join(root, REFERENCE_CACHE_PATH), { recursive: true, mode: 0o700 });
54
+ return join(root, REFERENCE_CACHE_PATH);
55
+ }
56
+ /** Fail closed on force-added cache or ignore drift; never unstage or delete user changes. */
57
+ export function assertReferenceCacheIsolation(workspaceRoot, required = false) {
58
+ const configured = tryGitText(["config", "--local", "--get", CONFIG_KEY], workspaceRoot);
59
+ if (configured === null) {
60
+ if (required || lstatSync(join(workspaceRoot, REFERENCE_CACHE_PATH), { throwIfNoEntry: false }))
61
+ reject();
62
+ return;
63
+ }
64
+ if (configured !== REFERENCE_CACHE_PATH)
65
+ reject();
66
+ if (runGitText(["ls-files", "--", REFERENCE_CACHE_PATH], workspaceRoot) !== "")
67
+ reject();
68
+ const ignored = runGitRaw(["check-ignore", "--no-index", "-q", `${REFERENCE_CACHE_PATH}/isolation-probe`], workspaceRoot);
69
+ if (ignored.status !== 0)
70
+ reject();
71
+ let current = realpathSync(workspaceRoot);
72
+ for (const part of REFERENCE_CACHE_PATH.split("/")) {
73
+ current = join(current, part);
74
+ const stat = lstatSync(current, { throwIfNoEntry: false });
75
+ if (stat !== undefined && (!stat.isDirectory() || stat.isSymbolicLink()))
76
+ reject();
77
+ }
78
+ }
79
+ //# sourceMappingURL=reference-cache-policy.js.map
@@ -0,0 +1,11 @@
1
+ CREATE TABLE external_reference_reads (
2
+ invocation_ref TEXT NOT NULL,
3
+ activity_ref TEXT,
4
+ source_ref TEXT NOT NULL,
5
+ revision TEXT NOT NULL,
6
+ content_sha256 TEXT NOT NULL,
7
+ size_bytes INTEGER NOT NULL CHECK (size_bytes >= 0),
8
+ checked_at TEXT NOT NULL,
9
+ PRIMARY KEY (invocation_ref, source_ref)
10
+ );
11
+ CREATE INDEX external_reference_reads_activity ON external_reference_reads(activity_ref);
@@ -0,0 +1,11 @@
1
+ import { type ExternalReferenceHostRequest } from "@tutti/shared/schemas/internal";
2
+ export declare class ExternalReferenceTransportError extends Error {
3
+ readonly reasonCode: string;
4
+ constructor(reasonCode: string);
5
+ }
6
+ export declare function requestExternalReferences(options: {
7
+ relayUrl: string;
8
+ body: ExternalReferenceHostRequest;
9
+ fetchImpl?: typeof fetch;
10
+ }): Promise<unknown>;
11
+ //# sourceMappingURL=external-references.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { EXTERNAL_REFERENCE_HOST_PATH, isExternalReferenceList, isExternalReferenceReadResult, } from "@tutti/shared/schemas/internal";
2
+ export class ExternalReferenceTransportError extends Error {
3
+ reasonCode;
4
+ constructor(reasonCode) {
5
+ super(reasonCode);
6
+ this.reasonCode = reasonCode;
7
+ this.name = "ExternalReferenceTransportError";
8
+ }
9
+ }
10
+ export async function requestExternalReferences(options) {
11
+ try {
12
+ const response = await (options.fetchImpl ?? fetch)(new URL(EXTERNAL_REFERENCE_HOST_PATH, options.relayUrl), {
13
+ method: "POST",
14
+ redirect: "error",
15
+ headers: { "content-type": "application/json" },
16
+ body: JSON.stringify(options.body),
17
+ signal: AbortSignal.timeout(90_000),
18
+ });
19
+ const reader = response.body?.getReader();
20
+ if (!reader)
21
+ throw new ExternalReferenceTransportError("reference_transport_invalid_response");
22
+ const chunks = [];
23
+ let size = 0;
24
+ try {
25
+ for (;;) {
26
+ const next = await reader.read();
27
+ if (next.done)
28
+ break;
29
+ if (!(next.value instanceof Uint8Array))
30
+ throw new ExternalReferenceTransportError("reference_transport_invalid_response");
31
+ size += next.value.byteLength;
32
+ if (size > 512 * 1024)
33
+ throw new ExternalReferenceTransportError("reference_transport_invalid_response");
34
+ chunks.push(next.value);
35
+ }
36
+ }
37
+ finally {
38
+ await reader.cancel().catch(() => undefined);
39
+ reader.releaseLock();
40
+ }
41
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
42
+ if (!response.ok) {
43
+ const reason = typeof value === "object" && value !== null && "reason_code" in value
44
+ ? value.reason_code
45
+ : null;
46
+ throw new ExternalReferenceTransportError(typeof reason === "string" &&
47
+ /^(reference|document_wiki|feishu|attachment)_[a-z_]{1,70}$/u.test(reason)
48
+ ? reason
49
+ : "reference_transport_unavailable");
50
+ }
51
+ if (options.body.action === "list"
52
+ ? !isExternalReferenceList(value)
53
+ : !isExternalReferenceReadResult(value))
54
+ throw new ExternalReferenceTransportError("reference_transport_invalid_response");
55
+ return value;
56
+ }
57
+ catch (error) {
58
+ throw error instanceof ExternalReferenceTransportError
59
+ ? error
60
+ : new ExternalReferenceTransportError("reference_transport_unavailable");
61
+ }
62
+ }
63
+ //# sourceMappingURL=external-references.js.map
@@ -44,6 +44,7 @@ export type RegisterRelayHostConnectionPayload = {
44
44
  browser_secret_write: false;
45
45
  repo_viewer: boolean;
46
46
  integration_source_ingestion_v1?: boolean;
47
+ integration_external_references_v1?: boolean;
47
48
  integration_attachment_ingestion_v1?: boolean;
48
49
  integration_managed_reference_ingestion_v1?: boolean;
49
50
  integration_outbound_v1?: boolean;
@@ -57,6 +57,7 @@ function createRegisterPayload(options) {
57
57
  browser_secret_write: false,
58
58
  repo_viewer: capabilities.repo_viewer ?? false,
59
59
  integration_source_ingestion_v1: capabilities.integration_source_ingestion_v1 ?? false,
60
+ integration_external_references_v1: capabilities.integration_external_references_v1 ?? false,
60
61
  integration_attachment_ingestion_v1: capabilities.integration_attachment_ingestion_v1 ?? false,
61
62
  integration_managed_reference_ingestion_v1: capabilities.integration_managed_reference_ingestion_v1 ?? false,
62
63
  integration_outbound_v1: capabilities.integration_outbound_v1 ?? false,
@@ -1,3 +1,4 @@
1
1
  export * from "./host-control.js";
2
2
  export * from "./tunnel.js";
3
+ export * from "./external-references.js";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
1
  export * from "./host-control.js";
2
2
  export * from "./tunnel.js";
3
+ export * from "./external-references.js";
3
4
  //# sourceMappingURL=index.js.map
@@ -507,6 +507,12 @@ export declare const SendClarificationRoundMessageResultSchema: import("@sinclai
507
507
  message_kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"user_text">, import("@sinclair/typebox").TLiteral<"agent_text">, import("@sinclair/typebox").TLiteral<"system_notice">, import("@sinclair/typebox").TLiteral<"clarification_request">, import("@sinclair/typebox").TLiteral<"clarification_card">]>;
508
508
  body: import("@sinclair/typebox").TString;
509
509
  refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
510
+ external_references: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
511
+ source_ref: import("@sinclair/typebox").TString;
512
+ kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"feishu_docx">, import("@sinclair/typebox").TLiteral<"feishu_file">, import("@sinclair/typebox").TLiteral<"feishu_image">]>;
513
+ name: import("@sinclair/typebox").TString;
514
+ created_at: import("@sinclair/typebox").TString;
515
+ }>>>;
510
516
  task_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
511
517
  activity_ref: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
512
518
  workflow_ref: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -0,0 +1,23 @@
1
+ import { type Static } from "@sinclair/typebox";
2
+ export declare const ExternalReferenceRefSchema: import("@sinclair/typebox").TString;
3
+ export declare const ExternalReferenceDescriptorSchema: import("@sinclair/typebox").TObject<{
4
+ source_ref: import("@sinclair/typebox").TString;
5
+ kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"feishu_docx">, import("@sinclair/typebox").TLiteral<"feishu_file">, import("@sinclair/typebox").TLiteral<"feishu_image">]>;
6
+ name: import("@sinclair/typebox").TString;
7
+ created_at: import("@sinclair/typebox").TString;
8
+ }>;
9
+ export declare const ExternalReferenceListSchema: import("@sinclair/typebox").TObject<{
10
+ can_register: import("@sinclair/typebox").TBoolean;
11
+ items: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
12
+ source_ref: import("@sinclair/typebox").TString;
13
+ kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"feishu_docx">, import("@sinclair/typebox").TLiteral<"feishu_file">, import("@sinclair/typebox").TLiteral<"feishu_image">]>;
14
+ name: import("@sinclair/typebox").TString;
15
+ created_at: import("@sinclair/typebox").TString;
16
+ }>>;
17
+ }>;
18
+ export declare const RegisterExternalReferenceBodySchema: import("@sinclair/typebox").TObject<{
19
+ document_url: import("@sinclair/typebox").TString;
20
+ }>;
21
+ export type ExternalReferenceDescriptor = Static<typeof ExternalReferenceDescriptorSchema>;
22
+ export type ExternalReferenceList = Static<typeof ExternalReferenceListSchema>;
23
+ //# sourceMappingURL=external-references.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { IsoDateTimeStringSchema } from "../domain/index.js";
3
+ export const ExternalReferenceRefSchema = Type.String({ pattern: "^external_ref_[a-f0-9]{32}$" });
4
+ export const ExternalReferenceDescriptorSchema = Type.Object({
5
+ source_ref: ExternalReferenceRefSchema,
6
+ kind: Type.Union([
7
+ Type.Literal("feishu_docx"),
8
+ Type.Literal("feishu_file"),
9
+ Type.Literal("feishu_image"),
10
+ ]),
11
+ name: Type.String({ minLength: 1, maxLength: 240 }),
12
+ created_at: IsoDateTimeStringSchema,
13
+ }, { additionalProperties: false });
14
+ export const ExternalReferenceListSchema = Type.Object({
15
+ can_register: Type.Boolean(),
16
+ items: Type.Array(ExternalReferenceDescriptorSchema, { maxItems: 500 }),
17
+ }, { additionalProperties: false });
18
+ export const RegisterExternalReferenceBodySchema = Type.Object({
19
+ document_url: Type.String({ minLength: 1, maxLength: 2048 }),
20
+ }, { additionalProperties: false });
21
+ //# sourceMappingURL=external-references.js.map