@xfey/tutti 0.1.105 → 0.1.107
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 +3 -2
- package/dist/approvals/decision-command.d.ts +124 -0
- package/dist/approvals/decision-command.js +20 -0
- package/dist/approvals/index.d.ts +1 -0
- package/dist/approvals/index.js +1 -0
- package/dist/approvals/manager.d.ts +3 -1
- package/dist/approvals/manager.js +9 -0
- package/dist/collaboration-ingestion/index.d.ts +3 -0
- package/dist/collaboration-ingestion/index.js +3 -0
- package/dist/collaboration-ingestion/managed-reference-snapshots.d.ts +27 -0
- package/dist/collaboration-ingestion/managed-reference-snapshots.js +45 -0
- package/dist/collaboration-ingestion/managed-reference-sources.d.ts +20 -0
- package/dist/collaboration-ingestion/managed-reference-sources.js +250 -0
- package/dist/collaboration-ingestion/reference-attachments.d.ts +22 -0
- package/dist/collaboration-ingestion/reference-attachments.js +132 -0
- package/dist/collaboration-ingestion/reference-files.d.ts +3 -2
- package/dist/collaboration-ingestion/reference-files.js +1 -0
- package/dist/control-plane/procedure-logging.d.ts +1 -0
- package/dist/control-plane/procedure-logging.js +16 -5
- package/dist/control-plane/reference-summary-refresh.js +2 -2
- package/dist/control-plane/workflows/openai.js +1 -0
- package/dist/platform-outbound/coordinator.d.ts +5 -2
- package/dist/platform-outbound/coordinator.js +95 -2
- package/dist/platform-outbound/repository.d.ts +17 -0
- package/dist/platform-outbound/repository.js +190 -5
- package/dist/platform-outbound/types.d.ts +13 -3
- package/dist/platform-outbound/worker.js +30 -14
- package/dist/providers/openai/app-server/read-only-procedure.d.ts +10 -2
- package/dist/providers/openai/app-server/read-only-procedure.js +65 -15
- package/dist/server-shell/cli/host-lifecycle.js +3 -0
- package/dist/server-shell/cli/host-server-runtime.d.ts +4 -1
- package/dist/server-shell/cli/host-server-runtime.js +128 -40
- package/dist/server-shell/cli/relay-registration.js +3 -0
- package/dist/server-shell/http/host-tunnel.d.ts +4 -1
- package/dist/server-shell/http/host-tunnel.js +174 -3
- package/dist/server-shell/http/routes/project-api/approval-routes.js +13 -20
- package/dist/server-shell/http/routes/project-api/reference-files-routes.js +19 -2
- package/dist/server-shell/http/routes/project-api/staged-uploads.d.ts +6 -0
- package/dist/server-shell/http/routes/project-api/staged-uploads.js +9 -2
- package/dist/workspace-ops/index.d.ts +2 -2
- package/dist/workspace-ops/index.js +1 -1
- package/dist/workspace-ops/reference-files.d.ts +14 -1
- package/dist/workspace-ops/reference-files.js +167 -2
- package/dist/workspace-ops/types.d.ts +24 -0
- package/migrations/0018_platform_outbound_events.sql +111 -0
- package/migrations/0019_platform_outbound_card_content.sql +76 -0
- package/migrations/0020_managed_reference_sources.sql +34 -0
- package/migrations/README.md +4 -1
- package/node_modules/@tutti/relay-client/dist/host-control.d.ts +5 -2
- package/node_modules/@tutti/relay-client/dist/host-control.js +3 -0
- package/node_modules/@tutti/shared/dist/schemas/api/feishu-documents.d.ts +41 -0
- package/node_modules/@tutti/shared/dist/schemas/api/feishu-documents.js +40 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +3 -2
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +2 -1
- package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +37 -0
- package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +45 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/feishu-documents.d.ts +38 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/feishu-documents.js +31 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/feishu-user-credentials.d.ts +22 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/feishu-user-credentials.js +37 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +2 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/index.js +2 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +1005 -49
- package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +434 -14
- package/package.json +1 -1
- package/prompts/skills/read-references/README.md +1 -1
- package/prompts/skills/read-references/SKILL.md +4 -2
- package/web/assets/{homepage-motion-scene-BnRTgItL.js → homepage-motion-scene-CFs5rMWq.js} +1 -1
- package/web/assets/index-BmiLZmaV.js +69 -0
- package/web/assets/{index-CTMOjCgU.css → index-DvfZ57Nl.css} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-CmzEr4-V.js +0 -69
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import { claimDuePlatformOutboundIntent, markPlatformOutboundHandedOff, markPlatformOutboundRetry, markPlatformOutboundTerminal, readPlatformOutboundIntent, recoverInterruptedPlatformOutboundIntents, } from "./repository.js";
|
|
1
|
+
import { claimDuePlatformOutboundIntent, initializePlatformOutboundEventCapture, markPlatformOutboundHandedOff, markPlatformOutboundRetry, markPlatformOutboundTerminal, readPlatformOutboundIntent, recoverInterruptedPlatformOutboundIntents, materializePlatformStatusOutboundIntents, } from "./repository.js";
|
|
2
2
|
const DEFAULT_POLL_INTERVAL_MS = 500;
|
|
3
3
|
const DEFAULT_MAX_ATTEMPTS = 8;
|
|
4
4
|
const MAX_BACKOFF_MS = 5 * 60 * 1_000;
|
|
5
|
-
function boundedPlatformText(value) {
|
|
6
|
-
const trimmed = value.trim();
|
|
7
|
-
const characters = [...trimmed];
|
|
8
|
-
return characters.length <= 4_000
|
|
9
|
-
? trimmed
|
|
10
|
-
: `${characters.slice(0, 3_997).join("")}...`;
|
|
11
|
-
}
|
|
12
5
|
export class HostPlatformOutboundWorker {
|
|
13
6
|
store;
|
|
14
7
|
transport;
|
|
@@ -33,6 +26,7 @@ export class HostPlatformOutboundWorker {
|
|
|
33
26
|
return;
|
|
34
27
|
}
|
|
35
28
|
this.stopped = false;
|
|
29
|
+
initializePlatformOutboundEventCapture(this.store.db, this.now);
|
|
36
30
|
const recovered = recoverInterruptedPlatformOutboundIntents(this.store.db, this.now);
|
|
37
31
|
if (recovered > 0) {
|
|
38
32
|
this.logger.warn({ stage: "platform_outbound_recovery", disposition: "retry_wait", count: recovered }, "Interrupted platform outbound intents recovered");
|
|
@@ -61,24 +55,46 @@ export class HostPlatformOutboundWorker {
|
|
|
61
55
|
await this.current;
|
|
62
56
|
}
|
|
63
57
|
async runOnce() {
|
|
58
|
+
const materialized = materializePlatformStatusOutboundIntents(this.store.db, this.now);
|
|
59
|
+
for (const intent of materialized) {
|
|
60
|
+
this.logger.info({
|
|
61
|
+
stage: "platform_outbound_materialize",
|
|
62
|
+
disposition: "created",
|
|
63
|
+
outbound_intent_ref: intent.intent_ref,
|
|
64
|
+
event_kind: intent.event_kind,
|
|
65
|
+
event_key: intent.event_key,
|
|
66
|
+
subject_kind: intent.subject_kind,
|
|
67
|
+
subject_ref: intent.subject_ref,
|
|
68
|
+
target_kind: intent.target_kind,
|
|
69
|
+
...(intent.canonical_message_id === null
|
|
70
|
+
? {}
|
|
71
|
+
: { canonical_message_ref: intent.canonical_message_id }),
|
|
72
|
+
}, "Platform outbound status intent materialized");
|
|
73
|
+
}
|
|
64
74
|
const item = claimDuePlatformOutboundIntent(this.store.db, this.now);
|
|
65
75
|
if (item === null) {
|
|
66
76
|
return { kind: "idle" };
|
|
67
77
|
}
|
|
68
78
|
const fields = {
|
|
69
79
|
outbound_intent_ref: item.intent.intent_ref,
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
event_kind: item.intent.event_kind,
|
|
81
|
+
event_key: item.intent.event_key,
|
|
82
|
+
subject_kind: item.intent.subject_kind,
|
|
83
|
+
subject_ref: item.intent.subject_ref,
|
|
84
|
+
target_kind: item.intent.target_kind,
|
|
85
|
+
...(item.intent.canonical_message_id === null
|
|
86
|
+
? {}
|
|
87
|
+
: { canonical_message_ref: item.intent.canonical_message_id }),
|
|
88
|
+
...(item.intent.trigger_message_id === null
|
|
89
|
+
? {}
|
|
90
|
+
: { trigger_message_ref: item.intent.trigger_message_id }),
|
|
72
91
|
installation_ref: item.intent.installation_ref,
|
|
73
92
|
binding_ref: item.intent.binding_ref,
|
|
74
93
|
attempt: item.intent.attempt_count,
|
|
75
94
|
};
|
|
76
95
|
this.logger.info({ ...fields, stage: "platform_outbound_attempt", disposition: "sending" }, "Platform outbound handoff attempted");
|
|
77
96
|
try {
|
|
78
|
-
const response = await this.transport.deliver(
|
|
79
|
-
...item,
|
|
80
|
-
body: boundedPlatformText(item.body),
|
|
81
|
-
});
|
|
97
|
+
const response = await this.transport.deliver(item);
|
|
82
98
|
if (response.disposition.kind === "accepted" || response.disposition.kind === "duplicate") {
|
|
83
99
|
markPlatformOutboundHandedOff(this.store.db, {
|
|
84
100
|
intentRef: item.intent.intent_ref,
|
|
@@ -11,6 +11,8 @@ export type CodexAppServerReadOnlyProcedureInput<TOutput> = {
|
|
|
11
11
|
workspaceRoot: string;
|
|
12
12
|
appServerCwd?: string;
|
|
13
13
|
readablePaths?: string[];
|
|
14
|
+
/** File-only workflows must not accept an answer produced without a filesystem tool attempt. */
|
|
15
|
+
requireFileToolUse?: boolean;
|
|
14
16
|
prompt: string;
|
|
15
17
|
outputSchema: unknown;
|
|
16
18
|
validateOutput: (value: unknown) => value is TOutput;
|
|
@@ -66,11 +68,17 @@ export type CodexAppServerReadOnlyProcedureErrorDetails = {
|
|
|
66
68
|
notification_methods?: string[];
|
|
67
69
|
agent_message_excerpt?: string;
|
|
68
70
|
stderr_excerpt?: string;
|
|
71
|
+
output_parse_reason?: "no_json_object" | "malformed_json";
|
|
72
|
+
agent_output_chars?: number;
|
|
73
|
+
json_candidate_count?: number;
|
|
74
|
+
agent_message_count?: number;
|
|
75
|
+
completed_tool_count?: number;
|
|
76
|
+
file_tool_count?: number;
|
|
69
77
|
};
|
|
70
78
|
export declare class CodexAppServerReadOnlyProcedureError extends Error {
|
|
71
|
-
readonly code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "write_detected" | "secret_leak_detected" | "process_lifecycle_failed" | "admission_unavailable";
|
|
79
|
+
readonly code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "file_read_not_observed" | "write_detected" | "secret_leak_detected" | "process_lifecycle_failed" | "admission_unavailable";
|
|
72
80
|
readonly details: CodexAppServerReadOnlyProcedureErrorDetails;
|
|
73
|
-
constructor(code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "write_detected" | "secret_leak_detected" | "process_lifecycle_failed" | "admission_unavailable", message: string, details?: CodexAppServerReadOnlyProcedureErrorDetails);
|
|
81
|
+
constructor(code: "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "file_read_not_observed" | "write_detected" | "secret_leak_detected" | "process_lifecycle_failed" | "admission_unavailable", message: string, details?: CodexAppServerReadOnlyProcedureErrorDetails);
|
|
74
82
|
}
|
|
75
83
|
export declare function resolveReadOnlyWebSearchMode(input: {
|
|
76
84
|
networkAccess?: boolean;
|
|
@@ -4,14 +4,14 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { buildCodexAppServerProcessPlan } from "../codex-app-server.js";
|
|
6
6
|
import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
|
|
7
|
-
import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError
|
|
7
|
+
import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError } from "./json-rpc.js";
|
|
8
8
|
import { CodexAppServerInvocation, CodexAppServerLifecycleError, createManagedProcess, } from "./invocation-lifecycle.js";
|
|
9
9
|
import { CodexAppServerAdmissionError, } from "./invocation-coordinator.js";
|
|
10
10
|
import { createCodexAppServerRuntimeObserver, } from "./runtime-telemetry.js";
|
|
11
11
|
import { createReadOnlySandboxPolicy } from "./sandbox-policy.js";
|
|
12
12
|
import { buildCodexAppServerProviderConfigOverrides, resolveCodexAppServerModelProvider, } from "./provider-config.js";
|
|
13
13
|
import { buildCodexAppServerTurnInput, resolveCodexAppServerContextRuntimeHint, resolveCodexAppServerSkillInputs, CodexAppServerSkillSelectionError, } from "./skills.js";
|
|
14
|
-
import { containsKnownSecret, createCodexSpawnEnv, extractAgentText, extractServerRequestMethod, findJsonObjectCandidates, isApiKeyAccount, readThreadId, readTurnError, readTurnStatus, redactWithKnownValues, compactText, } from "./runtime-helpers.js";
|
|
14
|
+
import { containsKnownSecret, createCodexSpawnEnv, extractAgentText, extractServerRequestMethod, findJsonObjectCandidates, isRecord, isApiKeyAccount, readThreadId, readTurnError, readTurnStatus, redactWithKnownValues, compactText, } from "./runtime-helpers.js";
|
|
15
15
|
import { extractCodexAppServerTokenUsage } from "../token-usage.js";
|
|
16
16
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
17
17
|
const DEFAULT_TURN_TIMEOUT_MS = 600_000;
|
|
@@ -65,7 +65,11 @@ function readOnlyTimeoutDetails(input) {
|
|
|
65
65
|
export function parseCodexAppServerStructuredJson(text) {
|
|
66
66
|
const candidates = findJsonObjectCandidates(text);
|
|
67
67
|
if (candidates.length === 0) {
|
|
68
|
-
throw new CodexAppServerReadOnlyProcedureError("output_parse_failed", "Codex app-server did not return a JSON object."
|
|
68
|
+
throw new CodexAppServerReadOnlyProcedureError("output_parse_failed", "Codex app-server did not return a JSON object.", {
|
|
69
|
+
output_parse_reason: "no_json_object",
|
|
70
|
+
agent_output_chars: text.length,
|
|
71
|
+
json_candidate_count: 0,
|
|
72
|
+
});
|
|
69
73
|
}
|
|
70
74
|
for (const jsonText of candidates.toReversed()) {
|
|
71
75
|
try {
|
|
@@ -75,7 +79,37 @@ export function parseCodexAppServerStructuredJson(text) {
|
|
|
75
79
|
// Earlier app-server progress can contain brace-shaped text.
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
|
-
throw new CodexAppServerReadOnlyProcedureError("output_parse_failed", "Codex app-server returned malformed JSON."
|
|
82
|
+
throw new CodexAppServerReadOnlyProcedureError("output_parse_failed", "Codex app-server returned malformed JSON.", {
|
|
83
|
+
output_parse_reason: "malformed_json",
|
|
84
|
+
agent_output_chars: text.length,
|
|
85
|
+
json_candidate_count: candidates.length,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Counts only; model text, tool arguments and stderr never enter structured diagnostics. */
|
|
89
|
+
function structuredOutputDetails(client, text) {
|
|
90
|
+
const itemTypes = client.notifications.flatMap((notification) => {
|
|
91
|
+
const params = notification.params;
|
|
92
|
+
return notification.method === "item/completed" &&
|
|
93
|
+
isRecord(params) &&
|
|
94
|
+
isRecord(params.item) &&
|
|
95
|
+
typeof params.item.type === "string"
|
|
96
|
+
? [params.item.type]
|
|
97
|
+
: [];
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
turn_status: "completed",
|
|
101
|
+
notification_count: client.notifications.length,
|
|
102
|
+
notification_methods: client.notifications
|
|
103
|
+
.map((notification) => notification.method)
|
|
104
|
+
.slice(-20),
|
|
105
|
+
server_request_methods: client.serverRequests.map(extractServerRequestMethod),
|
|
106
|
+
agent_output_chars: text.length,
|
|
107
|
+
json_candidate_count: findJsonObjectCandidates(text).length,
|
|
108
|
+
agent_message_count: itemTypes.filter((type) => type === "agentMessage").length,
|
|
109
|
+
completed_tool_count: itemTypes.filter((type) => ["commandExecution", "mcpToolCall", "dynamicToolCall", "imageView", "webSearch"].includes(type)).length,
|
|
110
|
+
file_tool_count: itemTypes.filter((type) => ["commandExecution", "imageView"].includes(type))
|
|
111
|
+
.length,
|
|
112
|
+
};
|
|
79
113
|
}
|
|
80
114
|
export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
81
115
|
if (options.invocationCoordinator !== undefined && options.invocationLease === undefined) {
|
|
@@ -102,14 +136,18 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
102
136
|
let contextRuntimeHint;
|
|
103
137
|
try {
|
|
104
138
|
skillInputs = resolveCodexAppServerSkillInputs({
|
|
105
|
-
...(options.skillSelectionName === undefined
|
|
139
|
+
...(options.skillSelectionName === undefined
|
|
140
|
+
? {}
|
|
141
|
+
: { selectionName: options.skillSelectionName }),
|
|
106
142
|
...(options.userSkillsRoot === undefined ? {} : { userSkillsRoot: options.userSkillsRoot }),
|
|
107
143
|
...(options.builtInSkillsRoot === undefined
|
|
108
144
|
? {}
|
|
109
145
|
: { builtInSkillsRoot: options.builtInSkillsRoot }),
|
|
110
146
|
});
|
|
111
147
|
contextRuntimeHint = resolveCodexAppServerContextRuntimeHint({
|
|
112
|
-
...(options.skillSelectionName === undefined
|
|
148
|
+
...(options.skillSelectionName === undefined
|
|
149
|
+
? {}
|
|
150
|
+
: { selectionName: options.skillSelectionName }),
|
|
113
151
|
...(options.agentContext === undefined ? {} : { agentContext: options.agentContext }),
|
|
114
152
|
});
|
|
115
153
|
}
|
|
@@ -184,10 +222,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
184
222
|
...(contextRuntimeHint?.sensitiveValues ?? []),
|
|
185
223
|
...skillInputs.map((skill) => skill.path),
|
|
186
224
|
];
|
|
187
|
-
const strictLeakCheckValues = [
|
|
188
|
-
options.apiKey,
|
|
189
|
-
...(contextRuntimeHint?.sensitiveValues ?? []),
|
|
190
|
-
];
|
|
225
|
+
const strictLeakCheckValues = [options.apiKey, ...(contextRuntimeHint?.sensitiveValues ?? [])];
|
|
191
226
|
let primaryError;
|
|
192
227
|
let procedureResult;
|
|
193
228
|
let lifecycleError;
|
|
@@ -249,9 +284,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
249
284
|
input: buildCodexAppServerTurnInput({
|
|
250
285
|
prompt: options.prompt,
|
|
251
286
|
skills: skillInputs,
|
|
252
|
-
...(contextRuntimeHint === undefined
|
|
253
|
-
? {}
|
|
254
|
-
: { runtimeHints: [contextRuntimeHint.text] }),
|
|
287
|
+
...(contextRuntimeHint === undefined ? {} : { runtimeHints: [contextRuntimeHint.text] }),
|
|
255
288
|
}),
|
|
256
289
|
cwd: appServerCwd,
|
|
257
290
|
approvalPolicy: "never",
|
|
@@ -314,9 +347,26 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
|
|
|
314
347
|
const agentText = client.notifications
|
|
315
348
|
.map((notification) => extractAgentText(notification))
|
|
316
349
|
.join("");
|
|
317
|
-
|
|
350
|
+
let parsed;
|
|
351
|
+
try {
|
|
352
|
+
parsed = parseCodexAppServerStructuredJson(agentText);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (!(error instanceof CodexAppServerReadOnlyProcedureError))
|
|
356
|
+
throw error;
|
|
357
|
+
throw new CodexAppServerReadOnlyProcedureError(error.code, error.message, {
|
|
358
|
+
...structuredOutputDetails(client, agentText),
|
|
359
|
+
...error.details,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
318
362
|
if (!options.validateOutput(parsed)) {
|
|
319
|
-
throw new CodexAppServerReadOnlyProcedureError("output_validation_failed", "Codex app-server structured output did not match the expected schema.");
|
|
363
|
+
throw new CodexAppServerReadOnlyProcedureError("output_validation_failed", "Codex app-server structured output did not match the expected schema.", structuredOutputDetails(client, agentText));
|
|
364
|
+
}
|
|
365
|
+
if (options.requireFileToolUse === true) {
|
|
366
|
+
const details = structuredOutputDetails(client, agentText);
|
|
367
|
+
if (details.file_tool_count === 0) {
|
|
368
|
+
throw new CodexAppServerReadOnlyProcedureError("file_read_not_observed", "Codex app-server did not attempt to inspect the summary source file.", details);
|
|
369
|
+
}
|
|
320
370
|
}
|
|
321
371
|
const gitStatusAfter = readGitStatus(options.workspaceRoot);
|
|
322
372
|
if (gitStatusAfter !== gitStatusBefore) {
|
|
@@ -311,6 +311,9 @@ export async function startLaunchProject(options) {
|
|
|
311
311
|
projectSurface: preparation.machine_local.binding.surface_profile.kind,
|
|
312
312
|
trustedMetadataStore: host.trusted_relay_metadata_store,
|
|
313
313
|
integrationDelivery: host.integration_delivery,
|
|
314
|
+
integrationReferenceAttachment: host.integration_reference_attachment,
|
|
315
|
+
integrationManagedReference: host.integration_managed_reference,
|
|
316
|
+
integrationApprovalDecision: host.integration_approval_decision,
|
|
314
317
|
}),
|
|
315
318
|
handleStreamRequest: createHostProjectApiTunnelStreamRequestHandler({
|
|
316
319
|
baseUrl: host.runtime_endpoint.base_url,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FastifyInstance } from "fastify";
|
|
2
2
|
import { type ExecutionStatusProjection } from "@tutti/shared/schemas/api";
|
|
3
|
-
import type { IntegrationDeliveryDisposition, TrustedConversationSourceIngestionCommand } from "@tutti/shared/schemas/internal";
|
|
3
|
+
import type { IntegrationApprovalDecisionDisposition, IntegrationDeliveryDisposition, IntegrationManagedReferenceDisposition, TrustedApprovalDecisionCommand, TrustedConversationSourceIngestionCommand, TrustedManagedReferenceIngestionCommand, TrustedReferenceAttachmentIngestionCommand } from "@tutti/shared/schemas/internal";
|
|
4
4
|
import { type HostProjectStore } from "../../store/index.js";
|
|
5
5
|
import type { createHostServer } from "../http/create-server.js";
|
|
6
6
|
import type { HostLocalLaunchStatus } from "../http/routes/local-control.js";
|
|
@@ -19,6 +19,9 @@ export type HostServerHandle = {
|
|
|
19
19
|
runtime_endpoint: MachineRuntimeEndpointRecord;
|
|
20
20
|
trusted_relay_metadata_store: TrustedRelaySessionMetadataStore;
|
|
21
21
|
integration_delivery: (command: TrustedConversationSourceIngestionCommand) => IntegrationDeliveryDisposition;
|
|
22
|
+
integration_approval_decision: (command: TrustedApprovalDecisionCommand) => IntegrationApprovalDecisionDisposition;
|
|
23
|
+
integration_reference_attachment: (command: TrustedReferenceAttachmentIngestionCommand) => Promise<IntegrationDeliveryDisposition>;
|
|
24
|
+
integration_managed_reference: (command: TrustedManagedReferenceIngestionCommand) => Promise<IntegrationManagedReferenceDisposition>;
|
|
22
25
|
attachRelayConnectionManager: (manager: HostRelayConnectionManager) => void;
|
|
23
26
|
closed: Promise<void>;
|
|
24
27
|
close: () => Promise<void>;
|
|
@@ -2,11 +2,11 @@ import { basename, join } from "node:path";
|
|
|
2
2
|
import { MAX_PROVIDER_MODEL_NAME_LENGTH, } from "@tutti/shared/schemas/api";
|
|
3
3
|
import { serializeRuntimeLogLine } from "@tutti/shared/utils";
|
|
4
4
|
import { completeRelayUpload, deliverRelayPlatformOutbound, RelayHostControlClientError, resolveRelayUpload, } from "@tutti/relay-client";
|
|
5
|
-
import { ApprovalRequestManager } from "../../approvals/index.js";
|
|
5
|
+
import { ApprovalRequestManager, executeApprovalDecisionCommand } from "../../approvals/index.js";
|
|
6
6
|
import { ArtifactPreviewManager } from "../../artifacts/index.js";
|
|
7
7
|
import { ReadOnlyChatAssistant } from "../../chat-assistant/index.js";
|
|
8
8
|
import { CheckProcessCoordinator } from "../../checks/index.js";
|
|
9
|
-
import { ingestTrustedConversationSource } from "../../collaboration-ingestion/index.js";
|
|
9
|
+
import { ingestTrustedConversationSource, ingestTrustedManagedReference, ingestTrustedReferenceAttachment, } from "../../collaboration-ingestion/index.js";
|
|
10
10
|
import { ensureInitialProjectWelcomeMessage } from "../../collaboration-state/index.js";
|
|
11
11
|
import { Phase5ControlPlane } from "../../control-plane/index.js";
|
|
12
12
|
import { resolveProjectOpenAiProcedureWorkflowRunner } from "../../control-plane/workflows/index.js";
|
|
@@ -21,6 +21,7 @@ import { openHostProjectStore } from "../../store/index.js";
|
|
|
21
21
|
import { resolvePackagedWebStaticRoot } from "../http/static-web.js";
|
|
22
22
|
import { AGENT_CONTEXT_BASE_PATH, AgentContextTokenRegistry, } from "../http/routes/agent-context.js";
|
|
23
23
|
import { readHostRepoProjection } from "../http/routes/project-api.js";
|
|
24
|
+
import { abortDownloadedStagedUpload, cleanupDownloadedStagedUpload, downloadStagedUploadWithResolver, } from "../http/routes/project-api/staged-uploads.js";
|
|
24
25
|
import { WorkspaceEventBus } from "../http/workspace-events.js";
|
|
25
26
|
import { createTrustedRelaySessionMetadataStore, } from "../session/relay-session-context.js";
|
|
26
27
|
import { LaunchError } from "./errors.js";
|
|
@@ -308,9 +309,60 @@ export async function startForegroundHostServer(options) {
|
|
|
308
309
|
},
|
|
309
310
|
});
|
|
310
311
|
controlPlaneLogger.info(staleTempCleanup, "Codex app-server stale temp cleanup completed");
|
|
312
|
+
const platformOutboundWorker = options.preparation.machine_local.binding.surface_profile.kind === "feishu"
|
|
313
|
+
? new HostPlatformOutboundWorker(store, {
|
|
314
|
+
deliver: async (item) => {
|
|
315
|
+
const connectionState = relayConnectionManager?.getState();
|
|
316
|
+
if (connectionState?.status !== "connected") {
|
|
317
|
+
throw new RelayHostControlClientError("relay_unavailable", "Relay is not connected for platform outbound handoff", true);
|
|
318
|
+
}
|
|
319
|
+
const hostSecret = readHostRegistrationSecret(options.preparation.tutti_home, options.preparation.project_id);
|
|
320
|
+
const relayProjectRef = connectionState.connection.relay_project_ref;
|
|
321
|
+
return await deliverRelayPlatformOutbound({
|
|
322
|
+
relayUrl: options.preparation.relay_url,
|
|
323
|
+
projectId: options.preparation.project_id,
|
|
324
|
+
relayProjectRef,
|
|
325
|
+
hostConnectionRef: connectionState.connection.host_connection_ref,
|
|
326
|
+
hostRegistrationSecret: hostSecret.secret,
|
|
327
|
+
intent: {
|
|
328
|
+
protocol_version: 1,
|
|
329
|
+
intent_ref: item.intent.intent_ref,
|
|
330
|
+
relay_project_ref: relayProjectRef,
|
|
331
|
+
installation: {
|
|
332
|
+
platform: item.intent.platform,
|
|
333
|
+
installation_ref: item.intent.installation_ref,
|
|
334
|
+
},
|
|
335
|
+
binding_ref: item.intent.binding_ref,
|
|
336
|
+
...(item.intent.canonical_message_id === null
|
|
337
|
+
? {}
|
|
338
|
+
: { canonical_message_ref: item.intent.canonical_message_id }),
|
|
339
|
+
...(item.intent.trigger_message_id === null
|
|
340
|
+
? {}
|
|
341
|
+
: { trigger_message_ref: item.intent.trigger_message_id }),
|
|
342
|
+
content: item.content,
|
|
343
|
+
event: {
|
|
344
|
+
kind: item.intent.event_kind,
|
|
345
|
+
event_key: item.intent.event_key,
|
|
346
|
+
subject: {
|
|
347
|
+
kind: item.intent.subject_kind,
|
|
348
|
+
ref: item.intent.subject_ref,
|
|
349
|
+
},
|
|
350
|
+
target: { kind: item.intent.target_kind },
|
|
351
|
+
},
|
|
352
|
+
created_at: item.intent.created_at,
|
|
353
|
+
attempt: item.intent.attempt_count,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
}, platformOutboundLogger, { now })
|
|
358
|
+
: undefined;
|
|
359
|
+
const platformOutboundCoordinator = platformOutboundWorker === undefined
|
|
360
|
+
? undefined
|
|
361
|
+
: new HostPlatformOutboundCoordinator(store, platformOutboundWorker, platformOutboundLogger, now);
|
|
311
362
|
const approvalManager = new ApprovalRequestManager({
|
|
312
363
|
events: workspaceEvents,
|
|
313
364
|
now,
|
|
365
|
+
onPending: (request) => platformOutboundCoordinator?.captureApprovalRequest(request),
|
|
314
366
|
});
|
|
315
367
|
const codexInvocationCoordinator = new CodexAppServerInvocationCoordinator();
|
|
316
368
|
const checkProcessCoordinator = new CheckProcessCoordinator();
|
|
@@ -364,43 +416,6 @@ export async function startForegroundHostServer(options) {
|
|
|
364
416
|
onLifecycleIncident: procedureLifecycleIncident,
|
|
365
417
|
}),
|
|
366
418
|
});
|
|
367
|
-
const platformOutboundWorker = options.preparation.machine_local.binding.surface_profile.kind === "feishu"
|
|
368
|
-
? new HostPlatformOutboundWorker(store, {
|
|
369
|
-
deliver: async (item) => {
|
|
370
|
-
const connectionState = relayConnectionManager?.getState();
|
|
371
|
-
if (connectionState?.status !== "connected") {
|
|
372
|
-
throw new RelayHostControlClientError("relay_unavailable", "Relay is not connected for platform outbound handoff", true);
|
|
373
|
-
}
|
|
374
|
-
const hostSecret = readHostRegistrationSecret(options.preparation.tutti_home, options.preparation.project_id);
|
|
375
|
-
const relayProjectRef = connectionState.connection.relay_project_ref;
|
|
376
|
-
return await deliverRelayPlatformOutbound({
|
|
377
|
-
relayUrl: options.preparation.relay_url,
|
|
378
|
-
projectId: options.preparation.project_id,
|
|
379
|
-
relayProjectRef,
|
|
380
|
-
hostConnectionRef: connectionState.connection.host_connection_ref,
|
|
381
|
-
hostRegistrationSecret: hostSecret.secret,
|
|
382
|
-
intent: {
|
|
383
|
-
protocol_version: 1,
|
|
384
|
-
intent_ref: item.intent.intent_ref,
|
|
385
|
-
relay_project_ref: relayProjectRef,
|
|
386
|
-
installation: {
|
|
387
|
-
platform: item.intent.platform,
|
|
388
|
-
installation_ref: item.intent.installation_ref,
|
|
389
|
-
},
|
|
390
|
-
binding_ref: item.intent.binding_ref,
|
|
391
|
-
canonical_message_ref: item.intent.canonical_message_id,
|
|
392
|
-
trigger_message_ref: item.intent.trigger_message_id,
|
|
393
|
-
content: { kind: "text", text: item.body },
|
|
394
|
-
created_at: item.intent.created_at,
|
|
395
|
-
attempt: item.intent.attempt_count,
|
|
396
|
-
},
|
|
397
|
-
});
|
|
398
|
-
},
|
|
399
|
-
}, platformOutboundLogger, { now })
|
|
400
|
-
: undefined;
|
|
401
|
-
const platformOutboundCoordinator = platformOutboundWorker === undefined
|
|
402
|
-
? undefined
|
|
403
|
-
: new HostPlatformOutboundCoordinator(platformOutboundWorker, platformOutboundLogger, now);
|
|
404
419
|
const chatAssistant = new ReadOnlyChatAssistant({
|
|
405
420
|
project: chatAssistantProject,
|
|
406
421
|
store,
|
|
@@ -508,9 +523,10 @@ export async function startForegroundHostServer(options) {
|
|
|
508
523
|
...(options.getLaunchStatus === undefined ? {} : { getLaunchStatus: options.getLaunchStatus }),
|
|
509
524
|
};
|
|
510
525
|
let app;
|
|
526
|
+
let resolveStagedUpload;
|
|
511
527
|
try {
|
|
512
528
|
const webStaticRoot = resolvePackagedWebStaticRoot();
|
|
513
|
-
|
|
529
|
+
resolveStagedUpload = createStagedUploadResolver({
|
|
514
530
|
preparation: options.preparation,
|
|
515
531
|
...(options.getLaunchStatus === undefined
|
|
516
532
|
? {}
|
|
@@ -688,6 +704,78 @@ export async function startForegroundHostServer(options) {
|
|
|
688
704
|
chatAssistant,
|
|
689
705
|
now,
|
|
690
706
|
}),
|
|
707
|
+
integration_reference_attachment: async (command) => await ingestTrustedReferenceAttachment({
|
|
708
|
+
store,
|
|
709
|
+
workspaceRoot: options.preparation.workspace_root,
|
|
710
|
+
events: workspaceEvents,
|
|
711
|
+
command,
|
|
712
|
+
resolveContent: async () => {
|
|
713
|
+
if (resolveStagedUpload === undefined) {
|
|
714
|
+
throw new Error("Staged upload resolver is unavailable");
|
|
715
|
+
}
|
|
716
|
+
const downloaded = await downloadStagedUploadWithResolver({
|
|
717
|
+
resolveStagedUpload,
|
|
718
|
+
expected: command.staged_upload,
|
|
719
|
+
purpose: "reference_file",
|
|
720
|
+
});
|
|
721
|
+
return {
|
|
722
|
+
content: downloaded.content,
|
|
723
|
+
complete: async () => await cleanupDownloadedStagedUpload(downloaded),
|
|
724
|
+
abort: async () => await abortDownloadedStagedUpload(downloaded),
|
|
725
|
+
};
|
|
726
|
+
},
|
|
727
|
+
scratchpadSource: {
|
|
728
|
+
notify: (message) => controlPlane.notifyScratchpadSourceChanged(message),
|
|
729
|
+
},
|
|
730
|
+
referenceSummaries: {
|
|
731
|
+
start: (paths) => controlPlane.startReferenceSummaryRefresh({ paths }),
|
|
732
|
+
},
|
|
733
|
+
now,
|
|
734
|
+
}),
|
|
735
|
+
integration_managed_reference: async (command) => await ingestTrustedManagedReference({
|
|
736
|
+
store,
|
|
737
|
+
workspaceRoot: options.preparation.workspace_root,
|
|
738
|
+
events: workspaceEvents,
|
|
739
|
+
command,
|
|
740
|
+
resolveContent: async () => {
|
|
741
|
+
if (resolveStagedUpload === undefined) {
|
|
742
|
+
throw new Error("Staged upload resolver is unavailable");
|
|
743
|
+
}
|
|
744
|
+
const downloaded = await downloadStagedUploadWithResolver({
|
|
745
|
+
resolveStagedUpload,
|
|
746
|
+
expected: command.staged_upload,
|
|
747
|
+
purpose: "reference_file",
|
|
748
|
+
});
|
|
749
|
+
return {
|
|
750
|
+
content: downloaded.content,
|
|
751
|
+
complete: async () => await cleanupDownloadedStagedUpload(downloaded),
|
|
752
|
+
abort: async () => await abortDownloadedStagedUpload(downloaded),
|
|
753
|
+
};
|
|
754
|
+
},
|
|
755
|
+
referenceSummaries: {
|
|
756
|
+
start: (paths) => controlPlane.startReferenceSummaryRefresh({ paths }),
|
|
757
|
+
},
|
|
758
|
+
now,
|
|
759
|
+
}),
|
|
760
|
+
integration_approval_decision: (command) => {
|
|
761
|
+
try {
|
|
762
|
+
return executeApprovalDecisionCommand({
|
|
763
|
+
db: store.db,
|
|
764
|
+
approvalManager,
|
|
765
|
+
commandId: command.command_id,
|
|
766
|
+
approvalId: command.approval_id,
|
|
767
|
+
decisionId: command.decision_id,
|
|
768
|
+
actor: {
|
|
769
|
+
account_ref: command.actor.account_ref,
|
|
770
|
+
display_name: command.actor.display_name,
|
|
771
|
+
},
|
|
772
|
+
now,
|
|
773
|
+
}).disposition;
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
return { kind: "invalid", reason_code: "approval_command_rejected" };
|
|
777
|
+
}
|
|
778
|
+
},
|
|
691
779
|
attachRelayConnectionManager: (manager) => {
|
|
692
780
|
relayConnectionManager = manager;
|
|
693
781
|
platformOutboundCoordinator?.start();
|
|
@@ -37,7 +37,10 @@ export async function registerHostWithRelay(options) {
|
|
|
37
37
|
joinToken: { mode: options.joinTokenMode ?? "rotate" },
|
|
38
38
|
capabilities: {
|
|
39
39
|
integration_source_ingestion_v1: true,
|
|
40
|
+
integration_attachment_ingestion_v1: true,
|
|
41
|
+
integration_managed_reference_ingestion_v1: true,
|
|
40
42
|
integration_outbound_v1: true,
|
|
43
|
+
integration_approval_decision_v1: true,
|
|
41
44
|
},
|
|
42
45
|
fetch: options.fetchImpl,
|
|
43
46
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FastifyInstance } from "fastify";
|
|
2
2
|
import type { RelayHostTunnelRequestHandler, RelayHostTunnelStreamRequestHandler } from "@tutti/relay-client";
|
|
3
|
-
import type { IntegrationDeliveryDisposition, TrustedConversationSourceIngestionCommand } from "@tutti/shared/schemas/internal";
|
|
3
|
+
import type { IntegrationApprovalDecisionDisposition, IntegrationDeliveryDisposition, IntegrationManagedReferenceDisposition, TrustedApprovalDecisionCommand, TrustedConversationSourceIngestionCommand, TrustedManagedReferenceIngestionCommand, TrustedReferenceAttachmentIngestionCommand } from "@tutti/shared/schemas/internal";
|
|
4
4
|
import type { ProjectSurface } from "@tutti/shared/domain";
|
|
5
5
|
import { type TrustedRelaySessionMetadataStore } from "../session/relay-session-context.js";
|
|
6
6
|
export type HostProjectApiTunnelDispatchOptions = {
|
|
@@ -8,6 +8,9 @@ export type HostProjectApiTunnelDispatchOptions = {
|
|
|
8
8
|
projectSurface?: ProjectSurface;
|
|
9
9
|
trustedMetadataStore: TrustedRelaySessionMetadataStore;
|
|
10
10
|
integrationDelivery?: (command: TrustedConversationSourceIngestionCommand) => IntegrationDeliveryDisposition;
|
|
11
|
+
integrationApprovalDecision?: (command: TrustedApprovalDecisionCommand) => IntegrationApprovalDecisionDisposition;
|
|
12
|
+
integrationReferenceAttachment?: (command: TrustedReferenceAttachmentIngestionCommand) => Promise<IntegrationDeliveryDisposition>;
|
|
13
|
+
integrationManagedReference?: (command: TrustedManagedReferenceIngestionCommand) => Promise<IntegrationManagedReferenceDisposition>;
|
|
11
14
|
};
|
|
12
15
|
export type HostProjectApiTunnelStreamDispatchOptions = {
|
|
13
16
|
baseUrl: string;
|