@xfey/tutti 0.1.109 → 0.1.110
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.
- package/README.md +4 -2
- package/dist/chat-assistant/diagnostics.d.ts +1 -0
- package/dist/chat-assistant/index.js +18 -17
- package/dist/chat-assistant/reference-freshness-smoke-cli.d.ts +2 -0
- package/dist/chat-assistant/reference-freshness-smoke-cli.js +40 -0
- package/dist/chat-assistant/reference-freshness-smoke.d.ts +23 -0
- package/dist/chat-assistant/reference-freshness-smoke.js +274 -0
- package/dist/collaboration-ingestion/index.d.ts +1 -1
- package/dist/collaboration-ingestion/index.js +1 -1
- package/dist/collaboration-ingestion/managed-reference-sources.d.ts +3 -1
- package/dist/collaboration-ingestion/managed-reference-sources.js +7 -0
- package/dist/collaboration-state/task-compile-context.d.ts +1 -1
- package/dist/reference-access/message-context.d.ts +6 -0
- package/dist/reference-access/message-context.js +11 -0
- package/dist/reference-access/read-evidence.d.ts +2 -0
- package/dist/reference-access/read-evidence.js +8 -0
- package/dist/server-shell/cli/external-reference-runtime.js +1 -0
- package/dist/server-shell/http/routes/agent-context-external-reference-routes.js +1 -0
- package/dist/server-shell/http/routes/agent-context-messages-routes.js +2 -0
- package/dist/server-shell/http/routes/agent-context-reference-routes.js +39 -3
- package/dist/server-shell/http/routes/agent-context-references.d.ts +11 -0
- package/dist/server-shell/http/routes/agent-context-references.js +14 -0
- package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +2 -1
- package/node_modules/@tutti/shared/dist/schemas/api/external-references.d.ts +27 -4
- package/node_modules/@tutti/shared/dist/schemas/api/external-references.js +32 -3
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +8 -4
- package/node_modules/@tutti/shared/dist/schemas/api/messages.js +2 -1
- package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +5 -3
- package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +16 -1
- package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +2 -1
- package/node_modules/@tutti/shared/dist/schemas/internal/external-references.d.ts +15 -2
- package/node_modules/@tutti/shared/dist/schemas/internal/external-references.js +4 -2
- package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +6 -6
- package/package.json +1 -1
- package/prompts/chat-assistant.md +16 -15
- package/prompts/skills/README.md +1 -1
- package/prompts/skills/read-references/README.md +2 -2
- package/prompts/skills/read-references/SKILL.md +24 -37
- package/web/assets/{homepage-motion-scene-B6yPJnY8.js → homepage-motion-scene-rxYjg-zc.js} +1 -1
- package/web/assets/{index-aQnJ5JoI.js → index-BrKWdtKZ.js} +3 -3
- package/web/index.html +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
`apps/server` 是 Tutti 的项目 host service,也是机器级 Local Console 的进程入口。
|
|
6
6
|
|
|
7
|
-
`0.1.
|
|
7
|
+
`0.1.110` 发布候选加入每次 Chat invocation 的外部资料核验规则、本地/外部 provenance、从实际读取证据派生的回答来源线索,以及隔离多轮模型 smoke;配套 F.3 增量支持群资料确认与普通云盘文件。需要同步 Web/Relay/Integration 后再升级 Host;实际发布与真人验收状态见同一[按需资料计划](../../docs/roadmap/feishu-on-demand-reference-plan.md)。历史快照不删除,读取线索不继承新鲜度或权限。
|
|
8
|
+
|
|
9
|
+
已发布的 `0.1.109` 统一验证 Codex `skills/extraRoots/set` / `skills/list` 与实际 `SKILL.md` 文件身份;Chat 的安全阶段日志可通过 invocation ref 关联外部 Reference 读取。候选安装/升级、registry byte match、本机 CLI/Console/测试 Host 更新及更新后 Docx 真人读取闭环已通过;连续更新/同次 pin/附件/失权 live 矩阵仍独立跟踪。证据见[按需资料计划](../../docs/roadmap/feishu-on-demand-reference-plan.md),不以安装版本代替实际读取证据。
|
|
8
10
|
|
|
9
11
|
它负责:
|
|
10
12
|
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
- Host server 与 Local Console 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、health、host-local control 和 `/local-console/*` API 不被 SPA fallback 吞掉。
|
|
44
46
|
- 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` 与测试产物。
|
|
45
47
|
- 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 存在。
|
|
46
|
-
- 对外发布必须把同一 staging package 通过 `npm pack` 生成的已核验 `.tgz` 交给 `npm publish`,不得直接发布 staging 目录;npm 的目录发布路径可能重新收集文件并遗漏 staging 内的 bundled dependencies
|
|
48
|
+
- 对外发布必须把同一 staging package 通过 `npm pack` 生成的已核验 `.tgz` 交给 `npm publish --auth-type=web`,不得直接发布 staging 目录;npm 的目录发布路径可能重新收集文件并遗漏 staging 内的 bundled dependencies。发布后核对 registry metadata 并回下载;若与已完成安装/CLI smoke 的 candidate 逐字节一致,复用候选证据,不重复安装。未被相同候选覆盖的 registry artifact 仍须补齐安装 smoke。
|
|
47
49
|
- `prompts/` 是所有模型-facing prompt、指令模板和 Codex 执行模板的统一落点;长 prompt 不内联在 TypeScript 中。
|
|
48
50
|
- Host Project API只信任Relay tunnel metadata注入的Surface-matched browser context;direct loopback缺少可信context时不作为浏览器工作区入口。
|
|
49
51
|
- `binding.json.surface_profile`是Project Surface的durable authority,legacy binding默认Standalone;已有project不能通过普通launch或Local Console隐式切换Surface。Relay只保存不可变mirror用于边缘gate。
|
|
@@ -6,6 +6,7 @@ export type ChatAssistantLogger = {
|
|
|
6
6
|
type ChatAssistantProgress = ProviderInvocationProgress | {
|
|
7
7
|
stage: "queued" | "inspection_wait" | "model_requested" | "reply_persisted" | "failed" | "skipped";
|
|
8
8
|
reason_code?: "queue_full" | "stopping" | "context_unavailable" | "answer_failed" | "reply_failed";
|
|
9
|
+
reference_source_count?: number;
|
|
9
10
|
};
|
|
10
11
|
export declare function createChatAssistantDiagnostics(options: {
|
|
11
12
|
logger?: ChatAssistantLogger;
|
|
@@ -3,6 +3,8 @@ import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readC
|
|
|
3
3
|
import { renderPromptTemplate } from "../prompt-templates/index.js";
|
|
4
4
|
import { withHostStoreTransaction, } from "../store/index.js";
|
|
5
5
|
import { messageTextForModel, messageTextForTuttiMention } from "../artifact-feedback/index.js";
|
|
6
|
+
import { externalReferenceMessageContext } from "../reference-access/message-context.js";
|
|
7
|
+
import { readExternalReferenceReadSources } from "../reference-access/read-evidence.js";
|
|
6
8
|
const CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT = 3;
|
|
7
9
|
const CHAT_ASSISTANT_ROUND_MESSAGE_LIMIT = 50;
|
|
8
10
|
const CHAT_ASSISTANT_ROUND_MESSAGE_MAX_CHARS = 2_000;
|
|
@@ -51,6 +53,7 @@ function messageToContext(message) {
|
|
|
51
53
|
? {}
|
|
52
54
|
: { author_display_name: authorDisplayName }),
|
|
53
55
|
text: messageTextForModel(message),
|
|
56
|
+
...externalReferenceMessageContext(message.refs),
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
function messageRole(message) {
|
|
@@ -90,6 +93,7 @@ function roundMessagesToContext(messages) {
|
|
|
90
93
|
? {}
|
|
91
94
|
: { author_display_name: authorDisplayName }),
|
|
92
95
|
text,
|
|
96
|
+
...externalReferenceMessageContext(message.refs),
|
|
93
97
|
});
|
|
94
98
|
}
|
|
95
99
|
return result;
|
|
@@ -179,18 +183,12 @@ export class ReadOnlyChatAssistant {
|
|
|
179
183
|
this.idleWaiters.clear();
|
|
180
184
|
}
|
|
181
185
|
buildMainChatContext(triggerMessage) {
|
|
182
|
-
const authorDisplayName = triggerMessage.author.display_name?.trim();
|
|
183
186
|
return {
|
|
184
187
|
mode: "main_chat",
|
|
185
188
|
project: {
|
|
186
189
|
display_name: this.options.project.display_name,
|
|
187
190
|
},
|
|
188
|
-
trigger:
|
|
189
|
-
...(authorDisplayName === undefined || authorDisplayName === ""
|
|
190
|
-
? {}
|
|
191
|
-
: { author_display_name: authorDisplayName }),
|
|
192
|
-
text: messageTextForModel(triggerMessage),
|
|
193
|
-
},
|
|
191
|
+
trigger: messageToContext(triggerMessage),
|
|
194
192
|
recent_messages: readMainChatMessageWindow(this.options.store.db, {
|
|
195
193
|
limit: CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT,
|
|
196
194
|
}).items.map(messageToContext),
|
|
@@ -213,18 +211,12 @@ export class ReadOnlyChatAssistant {
|
|
|
213
211
|
before_id: cardMessage.id,
|
|
214
212
|
limit: CHAT_ASSISTANT_ROUND_MAIN_CHAT_CONTEXT_LIMIT,
|
|
215
213
|
}).map(messageToContext);
|
|
216
|
-
const authorDisplayName = triggerMessage.author.display_name?.trim();
|
|
217
214
|
return {
|
|
218
215
|
mode: "clarification_round",
|
|
219
216
|
project: {
|
|
220
217
|
display_name: this.options.project.display_name,
|
|
221
218
|
},
|
|
222
|
-
trigger:
|
|
223
|
-
...(authorDisplayName === undefined || authorDisplayName === ""
|
|
224
|
-
? {}
|
|
225
|
-
: { author_display_name: authorDisplayName }),
|
|
226
|
-
text: messageTextForModel(triggerMessage),
|
|
227
|
-
},
|
|
219
|
+
trigger: messageToContext(triggerMessage),
|
|
228
220
|
round_request: {
|
|
229
221
|
...(round.request_payload.title === undefined
|
|
230
222
|
? {}
|
|
@@ -325,6 +317,8 @@ export class ReadOnlyChatAssistant {
|
|
|
325
317
|
}
|
|
326
318
|
async respond(triggerMessage, report) {
|
|
327
319
|
let answer;
|
|
320
|
+
let invocationRef;
|
|
321
|
+
let readSources = [];
|
|
328
322
|
try {
|
|
329
323
|
report({ stage: "inspection_wait" });
|
|
330
324
|
await this.options.waitForInspectionAvailability?.();
|
|
@@ -336,9 +330,16 @@ export class ReadOnlyChatAssistant {
|
|
|
336
330
|
report({ stage: "model_requested" });
|
|
337
331
|
const output = await this.options.model.answer({
|
|
338
332
|
prompt,
|
|
339
|
-
onProgress:
|
|
333
|
+
onProgress: (event) => {
|
|
334
|
+
if (event.stage === "context_ready")
|
|
335
|
+
invocationRef = event.invocation_ref;
|
|
336
|
+
report(event);
|
|
337
|
+
},
|
|
340
338
|
});
|
|
341
339
|
answer = normalizeAnswer(output.answer);
|
|
340
|
+
if (invocationRef !== undefined) {
|
|
341
|
+
readSources = readExternalReferenceReadSources(this.options.store.db, invocationRef);
|
|
342
|
+
}
|
|
342
343
|
}
|
|
343
344
|
catch {
|
|
344
345
|
report({ stage: "failed", reason_code: "answer_failed" });
|
|
@@ -349,8 +350,8 @@ export class ReadOnlyChatAssistant {
|
|
|
349
350
|
return;
|
|
350
351
|
}
|
|
351
352
|
// Persistence/outbound-hook failure must not retry by creating a second reply.
|
|
352
|
-
this.appendAssistantMessageForTrigger(triggerMessage, answer);
|
|
353
|
-
report({ stage: "reply_persisted" });
|
|
353
|
+
this.appendAssistantMessageForTrigger(triggerMessage, answer, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
|
|
354
|
+
report({ stage: "reply_persisted", reference_source_count: readSources.length });
|
|
354
355
|
}
|
|
355
356
|
}
|
|
356
357
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { resolveOpenAiProviderConfig } from "../providers/openai/provider-config.js";
|
|
5
|
+
import { readOpenAiApiKeyCredential } from "../providers/openai/credential-store.js";
|
|
6
|
+
import { runReferenceFreshnessSmoke } from "./reference-freshness-smoke.js";
|
|
7
|
+
async function main() {
|
|
8
|
+
const { values } = parseArgs({
|
|
9
|
+
options: {
|
|
10
|
+
"project-id": { type: "string" },
|
|
11
|
+
"tutti-home": { type: "string" },
|
|
12
|
+
"reread-only": { type: "boolean" },
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
if (!/^prj_[0-9A-Z]{26}$/u.test(values["project-id"] ?? ""))
|
|
16
|
+
throw new Error("invalid_project_id");
|
|
17
|
+
const projectId = values["project-id"];
|
|
18
|
+
const tuttiHome = values["tutti-home"] ?? join(homedir(), ".tutti");
|
|
19
|
+
const provider = resolveOpenAiProviderConfig({ tuttiHome, projectId });
|
|
20
|
+
if (provider.kind === "unavailable")
|
|
21
|
+
throw new Error("provider_unavailable");
|
|
22
|
+
const credential = readOpenAiApiKeyCredential(tuttiHome, projectId, provider.config.credential_ref);
|
|
23
|
+
// Read only the selected project's provider configuration; never its documents or database.
|
|
24
|
+
await runReferenceFreshnessSmoke({
|
|
25
|
+
apiKey: credential.secret,
|
|
26
|
+
model: provider.config.default_model,
|
|
27
|
+
rereadOnly: values["reread-only"] ?? false,
|
|
28
|
+
...(provider.config.api_base_url === undefined
|
|
29
|
+
? {}
|
|
30
|
+
: { apiBaseUrl: provider.config.api_base_url }),
|
|
31
|
+
report: (result) => console.log(JSON.stringify(result)),
|
|
32
|
+
});
|
|
33
|
+
console.log(JSON.stringify({ status: "passed", cases: values["reread-only"] ? 2 : 7 }));
|
|
34
|
+
}
|
|
35
|
+
void main().catch(() => {
|
|
36
|
+
// Provider errors and assertion messages can contain private runtime details.
|
|
37
|
+
console.error(JSON.stringify({ status: "failed", reason_code: "reference_freshness_smoke_failed" }));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=reference-freshness-smoke-cli.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Fixture-only wording check, not a production semantic validator. */
|
|
2
|
+
export declare function labelsPriorReferenceAsUnverified(answer: string): boolean;
|
|
3
|
+
/** Explicit live-model gate. All documents, project state and platform transport are synthetic. */
|
|
4
|
+
export declare function runReferenceFreshnessSmoke(input: {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
apiBaseUrl?: string;
|
|
7
|
+
model: string;
|
|
8
|
+
rereadOnly?: boolean;
|
|
9
|
+
/** Optional inspection of synthetic fixture answers; the CLI never logs answer bodies. */
|
|
10
|
+
inspectSyntheticAnswer?: (result: {
|
|
11
|
+
case: string;
|
|
12
|
+
answer: string;
|
|
13
|
+
}) => void;
|
|
14
|
+
report: (result: {
|
|
15
|
+
case: string;
|
|
16
|
+
status: "started" | "passed" | "failed";
|
|
17
|
+
reason_code?: string;
|
|
18
|
+
external_reads: number;
|
|
19
|
+
history_reads: number;
|
|
20
|
+
local_reads: number;
|
|
21
|
+
}) => void;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=reference-freshness-smoke.d.ts.map
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import Fastify from "fastify";
|
|
7
|
+
import { ExternalReferenceTransportError } from "@tutti/relay-client";
|
|
8
|
+
import { createProjectId } from "@tutti/shared/ids";
|
|
9
|
+
import { createMainChatMessage, readMainChatMessageWindow } from "../collaboration-state/index.js";
|
|
10
|
+
import { openHostProjectStore } from "../store/index.js";
|
|
11
|
+
import { runGit } from "../workspace-ops/git.js";
|
|
12
|
+
import { ExternalReferenceCache } from "../reference-access/cache.js";
|
|
13
|
+
import { ExternalReferenceAccess } from "../reference-access/service.js";
|
|
14
|
+
import { recordExternalReferenceRead } from "../reference-access/read-evidence.js";
|
|
15
|
+
import { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
|
|
16
|
+
import { AgentContextTokenRegistry } from "../server-shell/http/routes/agent-context-auth.js";
|
|
17
|
+
import { registerAgentContextReferenceRoutes } from "../server-shell/http/routes/agent-context-reference-routes.js";
|
|
18
|
+
import { registerAgentContextExternalReferenceRoutes } from "../server-shell/http/routes/agent-context-external-reference-routes.js";
|
|
19
|
+
import { registerAgentContextMessageRoutes } from "../server-shell/http/routes/agent-context-messages-routes.js";
|
|
20
|
+
import { runCodexAppServerReadOnlyProcedure } from "../providers/openai/app-server/read-only-procedure.js";
|
|
21
|
+
import { CHAT_ASSISTANT_OUTPUT_SCHEMA, isChatAssistantOutput, ReadOnlyChatAssistant, } from "./index.js";
|
|
22
|
+
/** Fixture-only wording check, not a production semantic validator. */
|
|
23
|
+
export function labelsPriorReferenceAsUnverified(answer) {
|
|
24
|
+
return (/先前|此前|上次|之前|历史|previous|historical|last (?:read|known)/iu.test(answer) &&
|
|
25
|
+
/无法确认|不能|不代表|并非|不是|未能确认|cannot|not (?:current|verified)|does not (?:prove|establish)/iu.test(answer));
|
|
26
|
+
}
|
|
27
|
+
/** Explicit live-model gate. All documents, project state and platform transport are synthetic. */
|
|
28
|
+
export async function runReferenceFreshnessSmoke(input) {
|
|
29
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "tutti-reference-freshness-")));
|
|
30
|
+
const workspaceRoot = join(root, "repo");
|
|
31
|
+
mkdirSync(workspaceRoot);
|
|
32
|
+
const projectId = createProjectId();
|
|
33
|
+
const store = openHostProjectStore({
|
|
34
|
+
projectId,
|
|
35
|
+
localStoreRoot: join(root, "store"),
|
|
36
|
+
migrationsDirectory: fileURLToPath(new URL("../../migrations", import.meta.url)),
|
|
37
|
+
});
|
|
38
|
+
const app = Fastify({ logger: false });
|
|
39
|
+
let processCleanupUncertain = false;
|
|
40
|
+
try {
|
|
41
|
+
runGit(["init", "-q", "-b", "tutti/mainline"], workspaceRoot);
|
|
42
|
+
runGit(["config", "user.name", "Reference Smoke"], workspaceRoot);
|
|
43
|
+
runGit(["config", "user.email", "smoke@example.test"], workspaceRoot);
|
|
44
|
+
const legacyPath = "docs/reference/files/project-brief.md";
|
|
45
|
+
mkdirSync(join(workspaceRoot, "docs/reference/files"), { recursive: true });
|
|
46
|
+
writeFileSync(join(workspaceRoot, legacyPath), "# Project brief\nHistorical marker: ORCHID_17\n");
|
|
47
|
+
runGit(["add", "."], workspaceRoot);
|
|
48
|
+
runGit(["commit", "-qm", "Synthetic historical snapshot"], workspaceRoot);
|
|
49
|
+
const nowIso = new Date().toISOString();
|
|
50
|
+
store.db
|
|
51
|
+
.prepare(`INSERT INTO managed_reference_sources
|
|
52
|
+
(source_ref, adapter_key, reference_path, accepted_revision_ref, accepted_blob_oid,
|
|
53
|
+
accepted_content_sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
54
|
+
.run("managed_smoke", "feishu", legacyPath, "1", "a".repeat(40), "b".repeat(64), nowIso, nowIso);
|
|
55
|
+
const source = {
|
|
56
|
+
source_ref: `external_ref_${"a".repeat(32)}`,
|
|
57
|
+
kind: "feishu_docx",
|
|
58
|
+
name: "Project brief",
|
|
59
|
+
created_at: nowIso,
|
|
60
|
+
};
|
|
61
|
+
const other = {
|
|
62
|
+
...source,
|
|
63
|
+
source_ref: `external_ref_${"b".repeat(32)}`,
|
|
64
|
+
kind: "feishu_file",
|
|
65
|
+
name: "Another team's attachment",
|
|
66
|
+
};
|
|
67
|
+
const sources = [source];
|
|
68
|
+
let revision = "1";
|
|
69
|
+
let marker = "CEDAR_29";
|
|
70
|
+
let deny = false;
|
|
71
|
+
let remoteReads = 0;
|
|
72
|
+
let historyReads = 0;
|
|
73
|
+
let localReads = 0;
|
|
74
|
+
const registry = new AgentContextTokenRegistry();
|
|
75
|
+
const service = new ExternalReferenceAccess({
|
|
76
|
+
createCache: () => new ExternalReferenceCache({ workspaceRoot, ledgerRoot: join(root, "cache") }),
|
|
77
|
+
list: () => Promise.resolve(sources.map((item) => ({ ...item }))),
|
|
78
|
+
read: (sourceRef) => {
|
|
79
|
+
remoteReads++;
|
|
80
|
+
if (deny)
|
|
81
|
+
return Promise.reject(new ExternalReferenceTransportError("feishu_docx_access_denied"));
|
|
82
|
+
assert.equal(sourceRef, source.source_ref, "wrong_source_selected");
|
|
83
|
+
return Promise.resolve({
|
|
84
|
+
content: Buffer.from(`# Project brief\nCurrent marker: ${marker}\n`),
|
|
85
|
+
revision,
|
|
86
|
+
media_type: "text/plain",
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
recordRead: (invocation, copy) => recordExternalReferenceRead(store.db, invocation, copy),
|
|
90
|
+
});
|
|
91
|
+
const routes = {
|
|
92
|
+
project: {
|
|
93
|
+
project_id: projectId,
|
|
94
|
+
display_name: "Reference smoke",
|
|
95
|
+
identity_source: "git_config",
|
|
96
|
+
},
|
|
97
|
+
store,
|
|
98
|
+
workspaceOps: { workspaceRoot, viewerAssetCacheRoot: join(root, "viewer-cache") },
|
|
99
|
+
tokenRegistry: registry,
|
|
100
|
+
externalReferences: service,
|
|
101
|
+
};
|
|
102
|
+
app.addHook("onRequest", (request, _reply, done) => {
|
|
103
|
+
const path = request.url.split("?")[0];
|
|
104
|
+
if (path === "/internal/agent-context/messages")
|
|
105
|
+
historyReads++;
|
|
106
|
+
if (path === "/internal/agent-context/references/file")
|
|
107
|
+
localReads++;
|
|
108
|
+
done();
|
|
109
|
+
});
|
|
110
|
+
registerAgentContextReferenceRoutes(app, routes);
|
|
111
|
+
registerAgentContextExternalReferenceRoutes(app, routes);
|
|
112
|
+
registerAgentContextMessageRoutes(app, routes);
|
|
113
|
+
const address = await app.listen({ port: 0, host: "127.0.0.1" });
|
|
114
|
+
const agentContext = {
|
|
115
|
+
baseUrl: `${address}/internal/agent-context`,
|
|
116
|
+
issueToken: (request) => {
|
|
117
|
+
const token = registry.issueToken({
|
|
118
|
+
projectId,
|
|
119
|
+
selection: "chat_assistant",
|
|
120
|
+
scopes: request.scopes,
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
token: token.token,
|
|
124
|
+
invocation_ref: token.reference_invocation_ref,
|
|
125
|
+
expires_at: token.expires_at,
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const say = (body, descriptors) => createMainChatMessage(store.db, {
|
|
130
|
+
author: { kind: "human", display_name: "Tester" },
|
|
131
|
+
body,
|
|
132
|
+
...(descriptors === undefined ? {} : { refs: { external_references: descriptors } }),
|
|
133
|
+
});
|
|
134
|
+
const ask = async (name, question, expected) => {
|
|
135
|
+
const before = { remote: remoteReads, history: historyReads, local: localReads };
|
|
136
|
+
input.report({
|
|
137
|
+
case: name,
|
|
138
|
+
status: "started",
|
|
139
|
+
external_reads: 0,
|
|
140
|
+
history_reads: 0,
|
|
141
|
+
local_reads: 0,
|
|
142
|
+
});
|
|
143
|
+
const check = (condition, reason) => {
|
|
144
|
+
if (condition)
|
|
145
|
+
return;
|
|
146
|
+
input.report({
|
|
147
|
+
case: name,
|
|
148
|
+
status: "failed",
|
|
149
|
+
reason_code: reason,
|
|
150
|
+
external_reads: remoteReads - before.remote,
|
|
151
|
+
history_reads: historyReads - before.history,
|
|
152
|
+
local_reads: localReads - before.local,
|
|
153
|
+
});
|
|
154
|
+
throw new Error("reference_freshness_assertion_failed");
|
|
155
|
+
};
|
|
156
|
+
let providerFailed = false;
|
|
157
|
+
// Each answer uses a new assistant instance and ephemeral app-server Thread.
|
|
158
|
+
const assistant = new ReadOnlyChatAssistant({
|
|
159
|
+
project: routes.project,
|
|
160
|
+
store,
|
|
161
|
+
workspaceRoot,
|
|
162
|
+
events: new WorkspaceEventBus(),
|
|
163
|
+
model: {
|
|
164
|
+
answer: async (turn) => {
|
|
165
|
+
try {
|
|
166
|
+
const result = await runCodexAppServerReadOnlyProcedure({
|
|
167
|
+
apiKey: input.apiKey,
|
|
168
|
+
...(input.apiBaseUrl === undefined ? {} : { apiBaseUrl: input.apiBaseUrl }),
|
|
169
|
+
defaultModel: input.model,
|
|
170
|
+
workspaceRoot,
|
|
171
|
+
prompt: turn.prompt,
|
|
172
|
+
outputSchema: CHAT_ASSISTANT_OUTPUT_SCHEMA,
|
|
173
|
+
validateOutput: isChatAssistantOutput,
|
|
174
|
+
skillSelectionName: "chat_assistant",
|
|
175
|
+
agentContext,
|
|
176
|
+
baseEnv: { PATH: process.env.PATH, HOME: root },
|
|
177
|
+
webSearchMode: "disabled",
|
|
178
|
+
requestTimeoutMs: 60_000,
|
|
179
|
+
turnTimeoutMs: 180_000,
|
|
180
|
+
...(turn.onProgress === undefined ? {} : { onProgress: turn.onProgress }),
|
|
181
|
+
onLifecycleIncident: () => {
|
|
182
|
+
processCleanupUncertain = true;
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
return result.output;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
providerFailed = true;
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
assistant.enqueue({ message: say(`@tutti ${question}`) });
|
|
195
|
+
await assistant.waitForIdle();
|
|
196
|
+
check(!providerFailed, "provider_failed");
|
|
197
|
+
const reply = readMainChatMessageWindow(store.db).items.at(-1);
|
|
198
|
+
input.inspectSyntheticAnswer?.({ case: name, answer: reply.body });
|
|
199
|
+
check(reply.author.kind === "agent", "reply_missing");
|
|
200
|
+
if (expected.marker)
|
|
201
|
+
check(reply.body.includes(expected.marker), "expected_content_missing");
|
|
202
|
+
if (expected.current) {
|
|
203
|
+
check(remoteReads - before.remote === 1, "current_read_missing_or_repeated");
|
|
204
|
+
check(JSON.stringify(reply.refs?.external_reference_reads) ===
|
|
205
|
+
JSON.stringify([source.source_ref]), "evidence_missing");
|
|
206
|
+
check(!reply.body.includes("ORCHID_17"), "legacy_content_used");
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
check(reply.refs?.external_reference_reads === undefined, "unverified_source_attached");
|
|
210
|
+
check(remoteReads - before.remote === (expected.denied ? 1 : 0), "unexpected_read_count");
|
|
211
|
+
}
|
|
212
|
+
if (expected.history)
|
|
213
|
+
check(historyReads > before.history, "history_not_consulted");
|
|
214
|
+
if (expected.local)
|
|
215
|
+
check(localReads > before.local, "historical_read_missing");
|
|
216
|
+
if (expected.denied) {
|
|
217
|
+
check(localReads === before.local, "legacy_fallback");
|
|
218
|
+
if (/ORCHID_17|CEDAR_29|MAPLE_43|BIRCH_61/u.test(reply.body)) {
|
|
219
|
+
check(labelsPriorReferenceAsUnverified(reply.body), "unlabelled_historical_content");
|
|
220
|
+
}
|
|
221
|
+
check(/无法|不能|失败|权限|不可|未能|无法确认|unable|cannot|access|unavailable/iu.test(reply.body), "missing_access_limitation");
|
|
222
|
+
}
|
|
223
|
+
input.report({
|
|
224
|
+
case: name,
|
|
225
|
+
status: "passed",
|
|
226
|
+
external_reads: remoteReads - before.remote,
|
|
227
|
+
history_reads: historyReads - before.history,
|
|
228
|
+
local_reads: localReads - before.local,
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
say("这是本项目要使用的文档。", [source]);
|
|
232
|
+
await ask("initial_external", "读取这份文档,告诉我当前标记。", { marker, current: true });
|
|
233
|
+
await ask("reread_without_update", "请重新读取刚才的文档,核对当前内容。", {
|
|
234
|
+
marker,
|
|
235
|
+
current: true,
|
|
236
|
+
});
|
|
237
|
+
if (input.rereadOnly) {
|
|
238
|
+
assert.equal(processCleanupUncertain, false, "process_cleanup_unverified");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
revision = "2";
|
|
242
|
+
marker = "MAPLE_43";
|
|
243
|
+
source.name = "Renamed project plan";
|
|
244
|
+
say("我已经修改了文档内容,也改了文件名。");
|
|
245
|
+
await ask("reread_after_update_and_rename", "请重新读取刚才的文档,核对当前内容。", {
|
|
246
|
+
marker,
|
|
247
|
+
current: true,
|
|
248
|
+
});
|
|
249
|
+
sources.push(other);
|
|
250
|
+
say("这是另一个团队的附件,先不用处理。", [other]);
|
|
251
|
+
say("我们下午再讨论排期。");
|
|
252
|
+
say("会议室已经预订好了。");
|
|
253
|
+
say("好的,收到。");
|
|
254
|
+
revision = "3";
|
|
255
|
+
marker = "BIRCH_61";
|
|
256
|
+
await ask("reread_beyond_compact_window", "最开始那份项目文档又更新了。请重新读取并核对当前标记,不是另一个团队的附件。", { marker, current: true, history: true });
|
|
257
|
+
deny = true;
|
|
258
|
+
await ask("revoked_access_no_legacy_fallback", "请再次读取刚才那份项目文档,确认现在的标记。", {
|
|
259
|
+
current: false,
|
|
260
|
+
denied: true,
|
|
261
|
+
});
|
|
262
|
+
await ask("explicit_historical_read", `请读取本地历史快照 ${legacyPath},只说明其中保存的旧标记,不要查在线版本。`, { marker: "ORCHID_17", current: false, local: true });
|
|
263
|
+
await ask("ordinary_question", "一加一等于几?", { current: false });
|
|
264
|
+
assert.equal(processCleanupUncertain, false, "process_cleanup_unverified");
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
await app.close();
|
|
268
|
+
store.close();
|
|
269
|
+
// Never remove a temp root while an unconfirmed app-server process may use it.
|
|
270
|
+
if (!processCleanupUncertain)
|
|
271
|
+
rmSync(root, { recursive: true, force: true });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=reference-freshness-smoke.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
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
3
|
export { ingestManagedReferenceSnapshot, publishManagedReferenceSnapshotChanged, type IngestManagedReferenceSnapshotOptions, } from "./managed-reference-snapshots.js";
|
|
4
|
-
export { ingestTrustedManagedReference, type IngestTrustedManagedReferenceOptions, type ResolvedManagedReferenceContent, } from "./managed-reference-sources.js";
|
|
4
|
+
export { ingestTrustedManagedReference, readManagedReferenceSourcePaths, type IngestTrustedManagedReferenceOptions, type ResolvedManagedReferenceContent, } from "./managed-reference-sources.js";
|
|
5
5
|
export { ingestReferenceFile, REFERENCE_FILE_INGESTION_COMMAND_NAME, REFERENCE_FILE_INGESTION_TARGET_REF, toReferenceFileProjection, type IngestReferenceFileOptions, } from "./reference-files.js";
|
|
6
6
|
export { ingestTrustedReferenceAttachment, TRUSTED_INTEGRATION_REFERENCE_ATTACHMENT_PATH, type IngestTrustedReferenceAttachmentOptions, type ResolvedReferenceAttachmentContent, } from "./reference-attachments.js";
|
|
7
7
|
export type { ChatAssistantEnqueuer, ReferenceSummaryStarter, ScratchpadSourceNotifier, TrustedCollaborationAuthor, } from "./types.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { ingestTrustedConversationSource, TRUSTED_INTEGRATION_INGESTION_PATH, } from "./external-source.js";
|
|
2
2
|
export { ingestMainChatMessage, } from "./main-chat.js";
|
|
3
3
|
export { ingestManagedReferenceSnapshot, publishManagedReferenceSnapshotChanged, } from "./managed-reference-snapshots.js";
|
|
4
|
-
export { ingestTrustedManagedReference, } from "./managed-reference-sources.js";
|
|
4
|
+
export { ingestTrustedManagedReference, readManagedReferenceSourcePaths, } from "./managed-reference-sources.js";
|
|
5
5
|
export { ingestReferenceFile, REFERENCE_FILE_INGESTION_COMMAND_NAME, REFERENCE_FILE_INGESTION_TARGET_REF, toReferenceFileProjection, } from "./reference-files.js";
|
|
6
6
|
export { ingestTrustedReferenceAttachment, TRUSTED_INTEGRATION_REFERENCE_ATTACHMENT_PATH, } from "./reference-attachments.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { IntegrationManagedReferenceDisposition, TrustedManagedReferenceIngestionCommand } from "@tutti/shared/schemas/internal";
|
|
2
|
-
import type { HostProjectStore } from "../store/index.js";
|
|
2
|
+
import type { HostProjectStore, SqliteDatabase } from "../store/index.js";
|
|
3
3
|
import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
|
|
4
4
|
import type { ReferenceSummaryStarter } from "./types.js";
|
|
5
|
+
/** Dedicated source records identify snapshots; filenames and text are not provenance. */
|
|
6
|
+
export declare function readManagedReferenceSourcePaths(db: SqliteDatabase): ReadonlySet<string>;
|
|
5
7
|
export type ResolvedManagedReferenceContent = {
|
|
6
8
|
content: Buffer;
|
|
7
9
|
complete: () => Promise<void>;
|
|
@@ -2,6 +2,13 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { isTrustedManagedReferenceIngestionCommand } from "@tutti/shared/schemas/internal";
|
|
3
3
|
import { readManagedReferenceSnapshotState, WorkspaceOpsError, writeManagedReferenceSnapshot, } from "../workspace-ops/index.js";
|
|
4
4
|
import { publishManagedReferenceSnapshotChanged } from "./managed-reference-snapshots.js";
|
|
5
|
+
/** Dedicated source records identify snapshots; filenames and text are not provenance. */
|
|
6
|
+
export function readManagedReferenceSourcePaths(db) {
|
|
7
|
+
return new Set(db
|
|
8
|
+
.prepare("SELECT reference_path FROM managed_reference_sources WHERE accepted_revision_ref IS NOT NULL")
|
|
9
|
+
.all()
|
|
10
|
+
.map((row) => row.reference_path));
|
|
11
|
+
}
|
|
5
12
|
function readSource(db, sourceRef) {
|
|
6
13
|
return (db
|
|
7
14
|
.prepare("SELECT * FROM managed_reference_sources WHERE source_ref = ?")
|
|
@@ -24,7 +24,7 @@ export declare function completeTaskCompileContext(db: SqliteDatabase, input: Co
|
|
|
24
24
|
task_changes: string[];
|
|
25
25
|
receipts: {
|
|
26
26
|
compiled_tasks?: {
|
|
27
|
-
status: "failed" | "done" | "
|
|
27
|
+
status: "failed" | "done" | "pending" | "todo" | "running";
|
|
28
28
|
task_id: string;
|
|
29
29
|
title: string;
|
|
30
30
|
summary: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MessageProjection } from "@tutti/shared/schemas/api";
|
|
2
|
+
/** Location hints, not access grants or current-read evidence. Never copy private locators. */
|
|
3
|
+
export declare function externalReferenceMessageContext(refs: MessageProjection["refs"]): {
|
|
4
|
+
external_reference_sources?: string[];
|
|
5
|
+
};
|
|
6
|
+
//# sourceMappingURL=message-context.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Location hints, not access grants or current-read evidence. Never copy private locators. */
|
|
2
|
+
export function externalReferenceMessageContext(refs) {
|
|
3
|
+
const sources = [
|
|
4
|
+
...(refs?.external_references?.map((source) => source.source_ref) ?? []),
|
|
5
|
+
...(refs?.external_reference_reads ?? []),
|
|
6
|
+
];
|
|
7
|
+
return sources.length === 0
|
|
8
|
+
? {}
|
|
9
|
+
: { external_reference_sources: [...new Set(sources)].slice(0, 16) };
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=message-context.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { SqliteDatabase } from "../store/index.js";
|
|
2
2
|
import type { ReferenceCopy } from "./cache.js";
|
|
3
3
|
import type { ReferenceInvocation } from "./service.js";
|
|
4
|
+
/** Prior answer context only: a source reference is not proof of freshness in a later turn. */
|
|
5
|
+
export declare function readExternalReferenceReadSources(db: SqliteDatabase, invocationRef: string): string[];
|
|
4
6
|
/** Durable observation, separate from evictable cache metadata; no content or locator. */
|
|
5
7
|
export declare function recordExternalReferenceRead(db: SqliteDatabase, invocation: ReferenceInvocation, copy: ReferenceCopy): void;
|
|
6
8
|
//# sourceMappingURL=read-evidence.d.ts.map
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** Prior answer context only: a source reference is not proof of freshness in a later turn. */
|
|
2
|
+
export function readExternalReferenceReadSources(db, invocationRef) {
|
|
3
|
+
return db
|
|
4
|
+
.prepare(`SELECT source_ref FROM external_reference_reads
|
|
5
|
+
WHERE invocation_ref = ? ORDER BY checked_at, source_ref LIMIT 16`)
|
|
6
|
+
.all(invocationRef)
|
|
7
|
+
.map((row) => row.source_ref);
|
|
8
|
+
}
|
|
1
9
|
/** Durable observation, separate from evictable cache metadata; no content or locator. */
|
|
2
10
|
export function recordExternalReferenceRead(db, invocation, copy) {
|
|
3
11
|
const invocationRef = invocation.reference_invocation_ref;
|
|
@@ -17,6 +17,7 @@ export function createExternalReferenceRuntime(options) {
|
|
|
17
17
|
return requestExternalReferences({
|
|
18
18
|
relayUrl: preparation.relay_url,
|
|
19
19
|
body: {
|
|
20
|
+
supports_drive_files: true,
|
|
20
21
|
project_identity: { source: "git_config", project_id: preparation.project_id },
|
|
21
22
|
host_auth: { kind: "host_registration_secret_v1", secret: secret.secret },
|
|
22
23
|
relay_project_ref: relay.relay_project_ref,
|
|
@@ -76,6 +76,7 @@ export function registerAgentContextExternalReferenceRoutes(app, options) {
|
|
|
76
76
|
kind: text === undefined ? "local_file" : "text",
|
|
77
77
|
...copy,
|
|
78
78
|
cache_hit: result.cache_hit,
|
|
79
|
+
provenance: { origin: "external_reference", verification: "this_invocation" },
|
|
79
80
|
...(safeText === undefined
|
|
80
81
|
? {}
|
|
81
82
|
: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readMainChatMessageWindow } from "../../../collaboration-state/index.js";
|
|
2
|
+
import { externalReferenceMessageContext } from "../../../reference-access/message-context.js";
|
|
2
3
|
import { authorizeAgentContextRequest } from "./agent-context-auth.js";
|
|
3
4
|
import { AgentContextBadRequestError } from "./agent-context-errors.js";
|
|
4
5
|
import { boundedLimit, queryString, truncateText } from "./agent-context-query.js";
|
|
@@ -44,6 +45,7 @@ export function registerAgentContextMessageRoutes(app, options) {
|
|
|
44
45
|
},
|
|
45
46
|
body: body.text,
|
|
46
47
|
body_truncated: body.truncated,
|
|
48
|
+
...externalReferenceMessageContext(message.refs),
|
|
47
49
|
...(message.refs?.reference_file === undefined
|
|
48
50
|
? {}
|
|
49
51
|
: {
|