fuli-context 0.8.2 → 0.9.0-beta.1
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 +115 -12
- package/README.zh-CN.md +99 -12
- package/dist/web/assets/{AboutPage-fBq5VFoQ.js → AboutPage-u2Bv6cO-.js} +1 -1
- package/dist/web/assets/{AgentHand-CMZk5GjY.js → AgentHand-CsMcNXUm.js} +1 -1
- package/dist/web/assets/{AgentName-DRZHAIB2.js → AgentName-BboCA-Fd.js} +1 -1
- package/dist/web/assets/{AgentProfilePage-BPQEfMhB.js → AgentProfilePage-Dyux6qE3.js} +1 -1
- package/dist/web/assets/AgentsDirectoryPage-BEJdvmX7.js +1 -0
- package/dist/web/assets/{ConnectionsPage-COb56qnk.js → ConnectionsPage-Dw_hK_EM.js} +1 -1
- package/dist/web/assets/{EmployeeRecruitDialog-BLTpjCcK.js → EmployeeRecruitDialog-DP_ktv3w.js} +1 -1
- package/dist/web/assets/{EmployeeWorkbenchPage-HUd-gne2.js → EmployeeWorkbenchPage-CAt8lVnr.js} +1 -1
- package/dist/web/assets/{GrowthLoading-Bu_kH-I8.js → GrowthLoading-DIwqGFSc.js} +1 -1
- package/dist/web/assets/{KnowledgeEditDialog-BTwXXRCD.js → KnowledgeEditDialog-Degd9AGD.js} +1 -1
- package/dist/web/assets/{KnowledgeInspector-Ct5hl2--.js → KnowledgeInspector-yms1ysZ4.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-DMC8Hh43.js → KnowledgeOrganizerPage-C64wNn8a.js} +1 -1
- package/dist/web/assets/KnowledgePage-BTFEXzgi.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-DmHIc0jb.js → KnowledgeWorkspace-BsF1X4Aq.js} +1 -1
- package/dist/web/assets/{NotFoundPage-C0yOR9T9.js → NotFoundPage-DfaNTiD1.js} +1 -1
- package/dist/web/assets/{OverviewPage-BreX_kb4.js → OverviewPage-VXMfcWFI.js} +1 -1
- package/dist/web/assets/{PersonalProfilePage-CX1a6_E3.js → PersonalProfilePage-C1LBhcYu.js} +1 -1
- package/dist/web/assets/PersonalProjectsPage-B5lV4ZJW.js +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-6afJ8HUe.css +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-DF-IvEqp.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-CgL92gng.js +13 -0
- package/dist/web/assets/ProjectAgentsPage-CoJ4qQlA.css +1 -0
- package/dist/web/assets/{ProjectScopePicker-DVHfXX1f.js → ProjectScopePicker-CZUMdyYz.js} +1 -1
- package/dist/web/assets/{PublicProjectsPage-CtctKml1.js → PublicProjectsPage-e4CpxHk9.js} +1 -1
- package/dist/web/assets/{ReviewPage-DUqaK-do.js → ReviewPage-DugEaFvI.js} +1 -1
- package/dist/web/assets/RoundtablesPage-642YtZBC.js +2 -0
- package/dist/web/assets/RoundtablesPage-DCbIBnBq.css +1 -0
- package/dist/web/assets/{SearchableSelect-BZtyYmZN.js → SearchableSelect-Do6X3fXi.js} +1 -1
- package/dist/web/assets/{SettingsPage-CB4ymLr8.js → SettingsPage-wqYxDPyu.js} +1 -1
- package/dist/web/assets/{SharedJefaReportPage-CbFtrDCY.js → SharedJefaReportPage-CNQDezJr.js} +1 -1
- package/dist/web/assets/{WritingTastePage-B7lSzs2z.js → WritingTastePage-CAUvbDlL.js} +1 -1
- package/dist/web/assets/{agent-profile.css_vue_type_style_index_0_src_true_lang-C22UdfU0.js → agent-profile.css_vue_type_style_index_0_src_true_lang-DHOcWSe_.js} +1 -1
- package/dist/web/assets/{catalog-DU4Kfd-N.js → catalog-JmKyJnKL.js} +1 -1
- package/dist/web/assets/{client-CEHkJJ-5.js → client-DTQ_tUr2.js} +1 -1
- package/dist/web/assets/{console--CZxc3PW.js → console-CrhIK67Y.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-Co1RlTFH.js → conversation-launcher-settings-fUWCt3XG.js} +1 -1
- package/dist/web/assets/copy-CX6pyzAG.js +1 -0
- package/dist/web/assets/i18n-CIIxzhPZ.js +10 -0
- package/dist/web/assets/{identity-CsowvtIm.js → identity-CdFITIOu.js} +1 -1
- package/dist/web/assets/{index-CWMqDq8g.css → index-CYZi7E1e.css} +1 -1
- package/dist/web/assets/index-DMoxVzDU.js +2 -0
- package/dist/web/assets/{model-CGpllrds.js → model-CJsrDUNb.js} +1 -1
- package/dist/web/assets/{personal-profile-graph-BNTG4PSY.js → personal-profile-graph-CQLOait6.js} +1 -1
- package/dist/web/assets/{shared-report-BFUJqqQe.js → shared-report-Cll2Yj8W.js} +1 -1
- package/dist/web/assets/{task-evidence-BUu7yYIn.js → task-evidence-DTiPCs7r.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-raNlHRDl.js → useApi-CROJJdhE-fcdVGInY.js} +1 -1
- package/dist/web/assets/{useModalDialog-CroGk2Cs.js → useModalDialog-B-45idX9.js} +1 -1
- package/dist/web/assets/{vue-router-JFYBLXB1.js → vue-router-BUX6njIv.js} +1 -1
- package/dist/web/index.html +12 -11
- package/docs/agent-conversations-and-collaboration.md +18 -1
- package/docs/employee-agents.md +20 -1
- package/docs/roundtable-beta-testing.md +102 -0
- package/docs/roundtable-beta-validation.md +125 -0
- package/docs/roundtable-research.md +142 -0
- package/graph-provider/fuli_graph/app.py +5 -0
- package/graph-provider/fuli_graph/models.py +1 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +1 -0
- package/graph-provider/fuli_graph/store_project_agent_context.py +22 -1
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +23 -8
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +86 -11
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +1 -0
- package/graph-provider/fuli_graph/store_records.py +1 -0
- package/graph-provider/fuli_graph/store_task_contexts.py +2 -1
- package/graph-provider/fuli_graph/task_context_adoption.py +9 -4
- package/graph-provider/fuli_graph/task_context_temporary_owner.py +25 -0
- package/graph-provider/fuli_graph/temporary_project_scope.py +88 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +131 -79
- package/package.json +8 -4
- package/skills/capturing-session-knowledge/SKILL.md +20 -5
- package/skills/operating-fuli/SKILL.md +8 -0
- package/src/agent-tools/graph-definitions.js +1 -1
- package/src/agent-tools/interface-catalog.js +5 -0
- package/src/agent-tools/project-agent-definitions.js +2 -2
- package/src/agent-tools.js +9 -2
- package/src/agents/a2a/roundtable-participant.js +374 -0
- package/src/agents/claude-code/roundtable-participant.js +24 -0
- package/src/agents/codex/roundtable-config.js +29 -0
- package/src/agents/codex/roundtable-participant.js +34 -0
- package/src/agents/grok/roundtable-participant.js +185 -0
- package/src/agents/identity-receipt.js +29 -0
- package/src/agents/pi/roundtable-participant.js +193 -0
- package/src/agents/pi/roundtable-result-format.js +183 -0
- package/src/cli/command-registry.js +2 -0
- package/src/cli/roundtable-command.js +59 -0
- package/src/cli.js +5 -0
- package/src/conversations/task-context-view.js +3 -1
- package/src/employees/service.js +9 -2
- package/src/employees/task-entry.js +5 -1
- package/src/graphiti/agent-conversations.js +15 -1
- package/src/graphiti/agent-knowledge-workflows.js +14 -5
- package/src/graphiti/federated-application.js +9 -41
- package/src/graphiti/project-agent-mapping.js +1 -0
- package/src/graphiti/project-agent-mention.js +35 -0
- package/src/graphiti/project-agent-task-entry.js +22 -3
- package/src/graphiti/project-agent-workflows.js +31 -12
- package/src/graphiti/project-path-context.js +1 -0
- package/src/graphiti/provider-client.js +3 -0
- package/src/graphiti/task-entry-preferences.js +50 -0
- package/src/graphiti/temporary-project-scope.js +46 -0
- package/src/http/api-router.js +2 -0
- package/src/http/error-mapping.js +2 -0
- package/src/mcp/provider-task-context-registry.js +10 -0
- package/src/mcp/session-id.js +1 -0
- package/src/mcp/tool-annotations.js +2 -0
- package/src/roundtables/application.js +69 -0
- package/src/roundtables/domain.js +181 -0
- package/src/roundtables/http-router.js +56 -0
- package/src/roundtables/mcp-server.js +48 -0
- package/src/roundtables/participant-registry.js +12 -0
- package/src/roundtables/process.js +70 -0
- package/src/roundtables/result-contract.js +47 -0
- package/src/roundtables/server.js +52 -0
- package/src/roundtables/service.js +396 -0
- package/src/roundtables/store.js +59 -0
- package/src/roundtables/tool-contract.js +41 -0
- package/src/roundtables/turn-prompt.js +21 -0
- package/src/roundtables/worker.js +85 -0
- package/dist/web/assets/AgentsDirectoryPage-CKZfrcuD.js +0 -1
- package/dist/web/assets/KnowledgePage-uXW1Eovz.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-Br1S8ETM.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-CA04h1Uf.js +0 -13
- package/dist/web/assets/ProjectAgentsPage-CZWWV4Kw.css +0 -1
- package/dist/web/assets/i18n-BtOOoyJd.js +0 -9
- package/dist/web/assets/index-7X3LGByg.js +0 -2
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
const UI_MUTATION_PARITY = Object.freeze([
|
|
2
|
+
local('POST /api/roundtables', 'Creating rooms, granting private identity context and selecting execution permissions requires the local user.'),
|
|
3
|
+
local('POST /api/roundtables/:roomId/invites', 'Seat invitations are local-user issued scoped capabilities.'),
|
|
4
|
+
local('POST /api/roundtables/:roomId/revoke', 'Revoking participation requires the room owner.'),
|
|
5
|
+
local('POST /api/roundtables/:roomId/control', 'Starting, changing limits, stopping or human acceptance requires the room owner.'),
|
|
6
|
+
local('POST /api/roundtables/:roomId/messages', 'Human input must not be impersonated by an Agent.'),
|
|
2
7
|
agent('POST /api/agent-conversations/query', 'list_agent_conversations', 'read_agent_conversation', 'get_agent_conversation_policy'),
|
|
3
8
|
agent('PUT /api/agent-conversations/policy', 'update_agent_conversation_policy'),
|
|
4
9
|
agent('POST /api/agent-attention', 'request_agent_attention'),
|
|
@@ -168,7 +168,7 @@ export const projectAgentTaskSubmitInput = objectSchema({
|
|
|
168
168
|
]);
|
|
169
169
|
export const projectAgentTaskCoordinateInput = objectSchema({
|
|
170
170
|
taskContextToken: id,
|
|
171
|
-
projectPath: boundedString(4096),
|
|
171
|
+
projectPath: { ...boundedString(4096), description: 'Current absolute directory, when available. An unmatched or omitted path gets an isolated task-scoped temporary project during explicit coordination; ambiguous matches still require selection.' },
|
|
172
172
|
personalProjectId: {
|
|
173
173
|
...nullableStringSchema(), minLength: 1, maxLength: 128,
|
|
174
174
|
description: 'Explicit existing project ID when the directory has no exact match. A conflicting exact path match is rejected.'
|
|
@@ -194,7 +194,7 @@ export const projectAgentTaskCoordinateInput = objectSchema({
|
|
|
194
194
|
contextLimitPerQuery: integerSchema({ minimum: 1, maximum: 20 }),
|
|
195
195
|
includePendingContext: booleanSchema()
|
|
196
196
|
}, [
|
|
197
|
-
'
|
|
197
|
+
'idempotencyKey', 'title', 'objective', 'workKind',
|
|
198
198
|
'routingReason', 'contextQueries'
|
|
199
199
|
]);
|
|
200
200
|
export const projectAgentTaskActivityInput = objectSchema({
|
package/src/agent-tools.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { GRAPH_TOOL_DEFINITIONS } from './agent-tools/graph-definitions.js';
|
|
2
2
|
import { dispatchGraphTool } from './agent-tools/graph-handlers.js';
|
|
3
3
|
import { runWithAgentRequestContext } from './app/agent-request-context.js';
|
|
4
|
+
import { ROUNDTABLE_TOOL_DEFINITIONS, ROUNDTABLE_TOOL_NAMES, callRoundtableTool } from './roundtables/tool-contract.js';
|
|
5
|
+
import { ApplicationError } from './app/application-error.js';
|
|
4
6
|
|
|
5
|
-
const TOOL_DEFINITIONS = [...GRAPH_TOOL_DEFINITIONS];
|
|
7
|
+
const TOOL_DEFINITIONS = [...GRAPH_TOOL_DEFINITIONS, ...ROUNDTABLE_TOOL_DEFINITIONS];
|
|
6
8
|
|
|
7
9
|
export function listAgentTools() {
|
|
8
10
|
return clone(TOOL_DEFINITIONS);
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function callAgentTool(app, name, input = {}, requestContext = null) {
|
|
14
|
+
if (ROUNDTABLE_TOOL_NAMES.includes(name) && app.getAgentAccessPolicy?.().enabled === false) {
|
|
15
|
+
throw new ApplicationError('agent_access_disabled', 'Agent access is disabled in the local console');
|
|
16
|
+
}
|
|
12
17
|
return runWithAgentRequestContext(
|
|
13
18
|
requestContext,
|
|
14
|
-
() =>
|
|
19
|
+
() => ROUNDTABLE_TOOL_NAMES.includes(name)
|
|
20
|
+
? callRoundtableTool(app.roundtables, name, input)
|
|
21
|
+
: dispatchGraphTool(app, name, input)
|
|
15
22
|
);
|
|
16
23
|
}
|
|
17
24
|
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import { request as httpsRequest } from 'node:https';
|
|
6
|
+
import { Readable } from 'node:stream';
|
|
7
|
+
|
|
8
|
+
const OUTPUT_LIMIT = 16 * 1024;
|
|
9
|
+
const RESPONSE_LIMIT = 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
class A2AParticipantError extends Error {
|
|
12
|
+
constructor(code, status = null) {
|
|
13
|
+
const messages = {
|
|
14
|
+
invalid_endpoint: 'A2A requires an explicitly configured public HTTPS endpoint on one origin.',
|
|
15
|
+
waiting_auth: 'Remote A2A authentication or permission is required.',
|
|
16
|
+
waiting_input: 'The remote A2A participant requires additional input.',
|
|
17
|
+
unsupported_protocol: 'The remote agent does not advertise a supported A2A protocol interface.',
|
|
18
|
+
response_invalid: 'The remote A2A participant returned an invalid or mismatched result.',
|
|
19
|
+
response_too_large: 'The remote A2A response exceeds the transport limit.',
|
|
20
|
+
output_too_large: 'The remote A2A output exceeds the roundtable message limit.',
|
|
21
|
+
remote_failed: 'The remote A2A task or request failed.',
|
|
22
|
+
cancelled: 'The remote A2A turn was interrupted.',
|
|
23
|
+
timeout: 'The remote A2A turn timed out.',
|
|
24
|
+
participant_busy: 'The A2A participant already has a running turn.',
|
|
25
|
+
participant_closed: 'The A2A participant is closed.'
|
|
26
|
+
};
|
|
27
|
+
super(messages[code] ?? messages.remote_failed);
|
|
28
|
+
this.code = code;
|
|
29
|
+
if (status !== null) this.status = status;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function publicAddress(address) {
|
|
34
|
+
if (isIP(address) === 4) {
|
|
35
|
+
const [a, b] = address.split('.').map(Number);
|
|
36
|
+
return a !== 0 && a !== 10 && a !== 127 && a < 224 &&
|
|
37
|
+
!(a === 169 && b === 254) && !(a === 172 && b >= 16 && b <= 31) &&
|
|
38
|
+
!(a === 192 && [0, 168].includes(b)) && !(a === 100 && b >= 64 && b <= 127) &&
|
|
39
|
+
!(a === 198 && [18, 19].includes(b));
|
|
40
|
+
}
|
|
41
|
+
if (isIP(address) === 6) {
|
|
42
|
+
const normalized = address.toLowerCase();
|
|
43
|
+
if (normalized.startsWith('::ffff:')) {
|
|
44
|
+
const embedded = normalized.slice(7);
|
|
45
|
+
return isIP(embedded) === 4 && publicAddress(embedded);
|
|
46
|
+
}
|
|
47
|
+
// Global unicast only; reject loopback, ULA, link-local and multicast.
|
|
48
|
+
return /^[23][0-9a-f]{0,3}:/.test(normalized) && !normalized.startsWith('2001:db8:');
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function endpointUrl(value) {
|
|
54
|
+
let target;
|
|
55
|
+
try { target = new URL(value); } catch { throw new A2AParticipantError('invalid_endpoint'); }
|
|
56
|
+
const hostname = target.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
57
|
+
if (target.protocol !== 'https:' || target.username || target.password || target.hash ||
|
|
58
|
+
/(^|\.)(localhost|local|internal|lan|home|test|invalid)$/.test(hostname) ||
|
|
59
|
+
(!isIP(hostname) && !hostname.includes('.')) || (isIP(hostname) && !publicAddress(hostname))) {
|
|
60
|
+
throw new A2AParticipantError('invalid_endpoint');
|
|
61
|
+
}
|
|
62
|
+
return target;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function redact(value, secret) {
|
|
66
|
+
return secret && typeof value === 'string' ? value.replaceAll(secret, '[REDACTED_SECRET]') : value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function boundedText(value, secret, trim = true) {
|
|
70
|
+
const text = redact(typeof value === 'string' ? value : '', secret);
|
|
71
|
+
const result = trim ? text.trim() : text;
|
|
72
|
+
if (Buffer.byteLength(result, 'utf8') > OUTPUT_LIMIT) throw new A2AParticipantError('output_too_large');
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function identifier(value, secret) {
|
|
77
|
+
const text = boundedText(value, secret);
|
|
78
|
+
if (!text || text.length > 256 || (secret && value.includes(secret))) throw new A2AParticipantError('response_invalid');
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function abortableLookup(lookupImpl, hostname, signal) {
|
|
83
|
+
signal?.throwIfAborted();
|
|
84
|
+
let onAbort;
|
|
85
|
+
const aborted = new Promise((_, reject) => {
|
|
86
|
+
onAbort = () => reject(signal.reason);
|
|
87
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
return await Promise.race([lookupImpl(hostname, { all: true, verbatim: true }), aborted]);
|
|
91
|
+
} finally {
|
|
92
|
+
signal?.removeEventListener('abort', onAbort);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function partsText(parts) {
|
|
97
|
+
return (parts ?? []).filter((part) => part.content?.$case === 'text')
|
|
98
|
+
.map((part) => part.content.value).join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function usageFromMetadata(metadata) {
|
|
102
|
+
const value = metadata?.usage;
|
|
103
|
+
const count = (number) => Number.isSafeInteger(number) && number >= 0 ? number : null;
|
|
104
|
+
const totalTokens = count(value?.totalTokens ?? value?.total_tokens);
|
|
105
|
+
if (totalTokens === null) return null;
|
|
106
|
+
return { source: 'executor', totalTokens, inputTokens: count(value.inputTokens ?? value.input_tokens),
|
|
107
|
+
outputTokens: count(value.outputTokens ?? value.output_tokens), cachedInputTokens: null,
|
|
108
|
+
cacheWriteInputTokens: null, reasoningOutputTokens: null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function limitedResponse(response) {
|
|
112
|
+
if (!response.body) throw new A2AParticipantError('response_invalid');
|
|
113
|
+
let bytes = 0;
|
|
114
|
+
const body = response.body.pipeThrough(new TransformStream({
|
|
115
|
+
transform(chunk, controller) {
|
|
116
|
+
bytes += chunk.byteLength;
|
|
117
|
+
if (bytes > RESPONSE_LIMIT) throw new A2AParticipantError('response_too_large');
|
|
118
|
+
controller.enqueue(chunk);
|
|
119
|
+
}
|
|
120
|
+
}));
|
|
121
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function pinnedHttpsFetch(target, init, addresses, requestImpl) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const request = requestImpl(target, {
|
|
127
|
+
method: init.method ?? 'GET', signal: init.signal,
|
|
128
|
+
headers: Object.fromEntries(init.headers),
|
|
129
|
+
lookup(_hostname, options, callback) {
|
|
130
|
+
const matching = addresses.filter((entry) => !options.family || entry.family === options.family);
|
|
131
|
+
if (!matching.length) return callback(new A2AParticipantError('invalid_endpoint'));
|
|
132
|
+
if (options.all) callback(null, matching);
|
|
133
|
+
else callback(null, matching[0].address, matching[0].family);
|
|
134
|
+
}
|
|
135
|
+
}, (incoming) => {
|
|
136
|
+
const headers = new Headers();
|
|
137
|
+
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
138
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
139
|
+
if (item !== undefined) headers.append(name, item);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const bodyless = [204, 205, 304].includes(incoming.statusCode);
|
|
143
|
+
if (bodyless) incoming.resume();
|
|
144
|
+
resolve(new Response(bodyless ? null : Readable.toWeb(incoming), {
|
|
145
|
+
status: incoming.statusCode, statusText: incoming.statusMessage, headers
|
|
146
|
+
}));
|
|
147
|
+
});
|
|
148
|
+
request.on('error', reject);
|
|
149
|
+
request.end(init.body);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A2A SDK client adapter. Remote completion is a turn result, not Fuli task verification. */
|
|
154
|
+
export function createA2AParticipant({
|
|
155
|
+
url,
|
|
156
|
+
env = process.env,
|
|
157
|
+
fetchImpl,
|
|
158
|
+
lookupImpl = lookup,
|
|
159
|
+
httpsRequestImpl = httpsRequest,
|
|
160
|
+
cardPath = '/.well-known/agent-card.json',
|
|
161
|
+
legacyCompat = false,
|
|
162
|
+
timeoutMs = 5 * 60 * 1000,
|
|
163
|
+
pollIntervalMs = 500
|
|
164
|
+
} = {}) {
|
|
165
|
+
const secret = typeof env.FULI_A2A_TOKEN === 'string' ? env.FULI_A2A_TOKEN.trim() : '';
|
|
166
|
+
let client = null;
|
|
167
|
+
let sdk = null;
|
|
168
|
+
let card = null;
|
|
169
|
+
let contextId = '';
|
|
170
|
+
let pendingTaskId = '';
|
|
171
|
+
let active = null;
|
|
172
|
+
let closed = false;
|
|
173
|
+
|
|
174
|
+
async function safeFetch(input, init = {}) {
|
|
175
|
+
const configured = endpointUrl(url);
|
|
176
|
+
const target = endpointUrl(input instanceof Request ? input.url : String(input));
|
|
177
|
+
if (target.origin !== configured.origin) throw new A2AParticipantError('invalid_endpoint');
|
|
178
|
+
const hostname = target.hostname.replace(/^\[|\]$/g, '');
|
|
179
|
+
let addresses = [{ address: hostname, family: isIP(hostname) }];
|
|
180
|
+
if (!isIP(hostname)) {
|
|
181
|
+
addresses = await abortableLookup(lookupImpl, hostname, init.signal);
|
|
182
|
+
if (!addresses.length || addresses.some((item) => !publicAddress(item.address))) {
|
|
183
|
+
throw new A2AParticipantError('invalid_endpoint');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
init.signal?.throwIfAborted();
|
|
187
|
+
const headers = new Headers(init.headers);
|
|
188
|
+
if (secret) headers.set('Authorization', `Bearer ${secret}`);
|
|
189
|
+
const requestInit = { ...init, headers, redirect: 'error' };
|
|
190
|
+
// The default transport uses the validated DNS answer rather than resolving again at connection time.
|
|
191
|
+
const response = fetchImpl && fetchImpl !== globalThis.fetch
|
|
192
|
+
? await fetchImpl(target, requestInit)
|
|
193
|
+
: await pinnedHttpsFetch(target, requestInit, addresses, httpsRequestImpl);
|
|
194
|
+
if (response.redirected || (response.url && new URL(response.url).origin !== configured.origin)) {
|
|
195
|
+
await response.body?.cancel();
|
|
196
|
+
throw new A2AParticipantError('invalid_endpoint');
|
|
197
|
+
}
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
await response.body?.cancel();
|
|
200
|
+
throw new A2AParticipantError([401, 403].includes(response.status) ? 'waiting_auth' : 'remote_failed', response.status);
|
|
201
|
+
}
|
|
202
|
+
return limitedResponse(response);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function safeFailure(error, signal) {
|
|
206
|
+
if (signal?.aborted) return new A2AParticipantError(signal.reason?.name === 'TimeoutError' ? 'timeout' : 'cancelled');
|
|
207
|
+
return error instanceof A2AParticipantError ? error : new A2AParticipantError('remote_failed');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function connect(signal) {
|
|
211
|
+
if (closed) throw new A2AParticipantError('participant_closed');
|
|
212
|
+
endpointUrl(url);
|
|
213
|
+
if (secret && /[\r\n]/.test(secret)) throw new A2AParticipantError('waiting_auth');
|
|
214
|
+
const response = await safeFetch(new URL(cardPath, url), { signal, headers: { 'A2A-Version': '1.0' } });
|
|
215
|
+
const rawCard = await response.json();
|
|
216
|
+
const [protocol, clients] = await Promise.all([import('@a2a-js/sdk'), import('@a2a-js/sdk/client')]);
|
|
217
|
+
sdk = protocol;
|
|
218
|
+
const resolver = new clients.DefaultAgentCardResolver({ legacyCompat: { enabled: legacyCompat } });
|
|
219
|
+
card = resolver.normalizeAgentCard(rawCard);
|
|
220
|
+
if (!card.supportedInterfaces?.length) throw new A2AParticipantError('unsupported_protocol');
|
|
221
|
+
for (const entry of card.supportedInterfaces) {
|
|
222
|
+
if (endpointUrl(entry.url).origin !== endpointUrl(url).origin) throw new A2AParticipantError('invalid_endpoint');
|
|
223
|
+
}
|
|
224
|
+
const compatible = card.supportedInterfaces.filter((entry) =>
|
|
225
|
+
['JSONRPC', 'HTTP+JSON'].includes(entry.protocolBinding.toUpperCase()) &&
|
|
226
|
+
(/^1\.0(?:\.0)?$/.test(entry.protocolVersion) || (legacyCompat && /^0\.3(?:\.\d+)?$/.test(entry.protocolVersion))));
|
|
227
|
+
if (!compatible.length) throw new A2AParticipantError('unsupported_protocol');
|
|
228
|
+
if (card.securityRequirements?.length && !secret) throw new A2AParticipantError('waiting_auth');
|
|
229
|
+
const factory = new clients.ClientFactory({
|
|
230
|
+
transports: [new clients.JsonRpcTransportFactory({ fetchImpl: safeFetch, legacyCompat: { enabled: legacyCompat } }),
|
|
231
|
+
new clients.RestTransportFactory({ fetchImpl: safeFetch, legacyCompat: { enabled: legacyCompat } })],
|
|
232
|
+
clientConfig: { polling: true, acceptedOutputModes: ['text/plain'] }, cardResolver: resolver
|
|
233
|
+
});
|
|
234
|
+
client = await factory.createFromAgentCard({ ...card, supportedInterfaces: compatible });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
describe() {
|
|
239
|
+
return { applicationLabel: 'A2A', automatic: true, workspaceWrite: false, cancel: 'remote_request', reportsUsage: false };
|
|
240
|
+
},
|
|
241
|
+
async preflight({ signal } = {}) {
|
|
242
|
+
const boundedSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs);
|
|
243
|
+
try {
|
|
244
|
+
if (active) throw new A2AParticipantError('participant_busy');
|
|
245
|
+
await connect(boundedSignal);
|
|
246
|
+
return { ready: true, applicationLabel: 'A2A', name: boundedText(card.name, secret),
|
|
247
|
+
protocolVersion: client.protocolVersion, streaming: Boolean(card.capabilities?.streaming), evidence: 'agent_card_discovery' };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (!active) client = null;
|
|
250
|
+
const failure = safeFailure(error, boundedSignal);
|
|
251
|
+
return { ready: false, code: failure.code, reason: failure.message };
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
async dispatch({ prompt, signal, turn } = {}) {
|
|
255
|
+
if (closed) throw new A2AParticipantError('participant_closed');
|
|
256
|
+
if (active) throw new A2AParticipantError('participant_busy');
|
|
257
|
+
if (typeof prompt !== 'string' || !prompt.trim()) throw new A2AParticipantError('response_invalid');
|
|
258
|
+
const controller = new AbortController();
|
|
259
|
+
const boundedSignal = AbortSignal.any([controller.signal, AbortSignal.timeout(timeoutMs), ...(signal ? [signal] : [])]);
|
|
260
|
+
active = controller;
|
|
261
|
+
let remoteTaskId = '';
|
|
262
|
+
let cancellation = null;
|
|
263
|
+
let task = null;
|
|
264
|
+
let message = null;
|
|
265
|
+
let eventCount = 0;
|
|
266
|
+
const artifacts = new Map();
|
|
267
|
+
const actualRecord = () => {
|
|
268
|
+
if (!remoteTaskId && !message?.messageId) return null;
|
|
269
|
+
const metadata = message?.metadata ?? task?.metadata;
|
|
270
|
+
const model = typeof metadata?.model === 'string' && metadata.model.length <= 256
|
|
271
|
+
? redact(metadata.model, secret) : null;
|
|
272
|
+
return { sourceApplication: 'other', applicationLabel: 'A2A', provider: 'a2a', model,
|
|
273
|
+
sessionId: identifier(contextId || remoteTaskId || message?.messageId, secret),
|
|
274
|
+
remoteTaskId: remoteTaskId ? identifier(remoteTaskId, secret) : null,
|
|
275
|
+
usage: usageFromMetadata(metadata), evidence: 'remote_protocol_response' };
|
|
276
|
+
};
|
|
277
|
+
const requestCancellation = () => {
|
|
278
|
+
if (!remoteTaskId || !client) return Promise.resolve(false);
|
|
279
|
+
cancellation ??= client.cancelTask({ id: remoteTaskId, tenant: '' }, { signal: AbortSignal.timeout(5000) })
|
|
280
|
+
.then((result) => result.id === remoteTaskId && result.status?.state === sdk.TaskState.TASK_STATE_CANCELED)
|
|
281
|
+
.catch(() => false);
|
|
282
|
+
return cancellation;
|
|
283
|
+
};
|
|
284
|
+
const onAbort = () => { void requestCancellation(); };
|
|
285
|
+
boundedSignal.addEventListener('abort', onAbort, { once: true });
|
|
286
|
+
const capture = (value) => {
|
|
287
|
+
if (value.messageId) {
|
|
288
|
+
if (value.role !== sdk.Role.ROLE_AGENT) throw new A2AParticipantError('response_invalid');
|
|
289
|
+
identifier(value.messageId, secret);
|
|
290
|
+
if ((value.taskId && remoteTaskId && value.taskId !== remoteTaskId) ||
|
|
291
|
+
(value.contextId && contextId && value.contextId !== contextId)) {
|
|
292
|
+
throw new A2AParticipantError('response_invalid');
|
|
293
|
+
}
|
|
294
|
+
if (value.contextId) contextId = identifier(value.contextId, secret);
|
|
295
|
+
if (value.taskId) remoteTaskId = identifier(value.taskId, secret);
|
|
296
|
+
message = value;
|
|
297
|
+
boundedSignal.throwIfAborted();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const id = value.id ?? value.taskId;
|
|
301
|
+
if (!id || (remoteTaskId && remoteTaskId !== id)) throw new A2AParticipantError('response_invalid');
|
|
302
|
+
remoteTaskId = identifier(id, secret);
|
|
303
|
+
if (value.contextId && contextId && value.contextId !== contextId) throw new A2AParticipantError('response_invalid');
|
|
304
|
+
if (value.contextId) contextId = identifier(value.contextId, secret);
|
|
305
|
+
task = { ...task, ...value, id, status: value.status ?? task?.status };
|
|
306
|
+
for (const artifact of value.artifacts ?? (value.artifact ? [value.artifact] : [])) {
|
|
307
|
+
if (!artifact.artifactId || artifacts.size >= 32 && !artifacts.has(artifact.artifactId)) {
|
|
308
|
+
throw new A2AParticipantError('response_invalid');
|
|
309
|
+
}
|
|
310
|
+
const previous = artifacts.get(artifact.artifactId);
|
|
311
|
+
const text = `${value.append ? previous?.text ?? '' : ''}${partsText(artifact.parts)}`;
|
|
312
|
+
const name = boundedText(artifact.name, secret);
|
|
313
|
+
artifacts.set(artifact.artifactId, { artifactId: identifier(artifact.artifactId, secret),
|
|
314
|
+
...(name.trim() ? { name } : {}), text: boundedText(text, secret, false) });
|
|
315
|
+
}
|
|
316
|
+
boundedSignal.throwIfAborted();
|
|
317
|
+
};
|
|
318
|
+
try {
|
|
319
|
+
if (!client) await connect(boundedSignal);
|
|
320
|
+
boundedSignal.throwIfAborted();
|
|
321
|
+
const params = sdk.SendMessageRequest.fromJSON({
|
|
322
|
+
message: { messageId: randomUUID(), role: 'ROLE_USER', contextId, taskId: pendingTaskId,
|
|
323
|
+
parts: [{ text: prompt }], metadata: { fuli: { turnId: turn?.id ?? turn?.turnId ?? null, attemptId: turn?.attemptId ?? null } } },
|
|
324
|
+
configuration: { acceptedOutputModes: ['text/plain'], historyLength: 8, returnImmediately: true }
|
|
325
|
+
});
|
|
326
|
+
if (card.capabilities?.streaming) {
|
|
327
|
+
for await (const event of client.sendMessageStream(params, { signal: boundedSignal })) {
|
|
328
|
+
if (++eventCount > 512 || !event.payload) throw new A2AParticipantError('response_invalid');
|
|
329
|
+
capture(event.payload.value);
|
|
330
|
+
if (task?.status && ![sdk.TaskState.TASK_STATE_SUBMITTED, sdk.TaskState.TASK_STATE_WORKING,
|
|
331
|
+
sdk.TaskState.TASK_STATE_UNSPECIFIED].includes(task.status.state)) break;
|
|
332
|
+
if (message && !remoteTaskId) break;
|
|
333
|
+
}
|
|
334
|
+
} else capture(await client.sendMessage(params, { signal: boundedSignal }));
|
|
335
|
+
while ([sdk.TaskState.TASK_STATE_SUBMITTED, sdk.TaskState.TASK_STATE_WORKING].includes(task?.status?.state)) {
|
|
336
|
+
await delay(pollIntervalMs, undefined, { signal: boundedSignal });
|
|
337
|
+
capture(await client.getTask({ id: remoteTaskId, tenant: '', historyLength: 8 }, { signal: boundedSignal }));
|
|
338
|
+
}
|
|
339
|
+
const state = task?.status?.state;
|
|
340
|
+
if (state === sdk.TaskState.TASK_STATE_INPUT_REQUIRED || state === sdk.TaskState.TASK_STATE_AUTH_REQUIRED) {
|
|
341
|
+
pendingTaskId = remoteTaskId;
|
|
342
|
+
const failure = new A2AParticipantError(state === sdk.TaskState.TASK_STATE_INPUT_REQUIRED ? 'waiting_input' : 'waiting_auth');
|
|
343
|
+
failure.body = boundedText(partsText(task.status.message?.parts), secret);
|
|
344
|
+
failure.remoteTaskId = identifier(remoteTaskId, secret);
|
|
345
|
+
throw failure;
|
|
346
|
+
}
|
|
347
|
+
if ((!message || task) && state !== sdk.TaskState.TASK_STATE_COMPLETED) throw new A2AParticipantError('remote_failed');
|
|
348
|
+
const statusMessage = task?.status?.message?.role === sdk.Role.ROLE_AGENT ? task.status.message : null;
|
|
349
|
+
const latestMessage = message ?? statusMessage ?? [...(task?.history ?? [])].reverse().find((item) => item.role === sdk.Role.ROLE_AGENT);
|
|
350
|
+
const artifactText = [...artifacts.values()].map((item) => item.text).filter(Boolean).join('\n');
|
|
351
|
+
const body = boundedText(artifactText || partsText(latestMessage?.parts), secret);
|
|
352
|
+
if (!body) throw new A2AParticipantError('response_invalid');
|
|
353
|
+
pendingTaskId = '';
|
|
354
|
+
return { body, actual: actualRecord(), artifacts: [...artifacts.values()] };
|
|
355
|
+
} catch (error) {
|
|
356
|
+
const failure = safeFailure(error, boundedSignal);
|
|
357
|
+
const actual = actualRecord();
|
|
358
|
+
if (actual) failure.actual = actual;
|
|
359
|
+
if (boundedSignal.aborted) {
|
|
360
|
+
failure.cancellationConfirmed = await requestCancellation();
|
|
361
|
+
failure.remoteTaskId = remoteTaskId ? identifier(remoteTaskId, secret) : null;
|
|
362
|
+
}
|
|
363
|
+
throw failure;
|
|
364
|
+
} finally {
|
|
365
|
+
boundedSignal.removeEventListener('abort', onAbort);
|
|
366
|
+
active = null;
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
async close() {
|
|
370
|
+
closed = true;
|
|
371
|
+
active?.abort();
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { runParticipantProcess, parseParticipantEvents } from '../../roundtables/process.js';
|
|
2
|
+
|
|
3
|
+
export function createClaudeCodeParticipant({ command = process.env.FULI_CLAUDE_BIN ?? 'claude',
|
|
4
|
+
workspace = process.cwd(), env = process.env, model, runProcess = runParticipantProcess } = {}) {
|
|
5
|
+
const settings = JSON.stringify({ disableAllHooks: true });
|
|
6
|
+
const clientEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith('FULI_')));
|
|
7
|
+
return {
|
|
8
|
+
async preflight({ signal } = {}) {
|
|
9
|
+
try {
|
|
10
|
+
const result = await runProcess(command, ['--version'], { cwd: workspace, env: clientEnv, signal, timeoutMs: 10_000 });
|
|
11
|
+
return { ready: result.code === 0, authentication: 'checked_on_dispatch', reason: result.code === 0 ? null : 'runtime_unavailable' };
|
|
12
|
+
} catch (error) { return { ready: false, reason: error.code ?? 'runtime_unavailable' }; }
|
|
13
|
+
},
|
|
14
|
+
async dispatch({ prompt, signal, allowWrite = false, resultSchema }) {
|
|
15
|
+
const args = ['-p', '--output-format', 'stream-json', '--verbose', '--no-session-persistence',
|
|
16
|
+
'--permission-mode', allowWrite ? 'acceptEdits' : 'dontAsk', '--no-chrome', '--disable-slash-commands',
|
|
17
|
+
'--setting-sources', '', '--settings', settings, '--mcp-config', '{"mcpServers":{}}', '--strict-mcp-config',
|
|
18
|
+
'--tools', allowWrite ? 'Read,Glob,Grep,Edit,Write' : 'Read,Glob,Grep',
|
|
19
|
+
'--max-budget-usd', env.FULI_CLAUDE_MAX_BUDGET_USD ?? '1.00', ...(model ? ['--model', model] : []),
|
|
20
|
+
...(resultSchema ? ['--json-schema', JSON.stringify(resultSchema)] : [])];
|
|
21
|
+
return parseParticipantEvents('claude_code', await runProcess(command, args, { cwd: workspace, env: clientEnv, input: prompt, signal }));
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Discover the effective MCP configuration, then disable and verify every entry.
|
|
2
|
+
// Empty TOML tables merge recursively and cannot clear inherited servers.
|
|
3
|
+
export async function prepareCodexRoundtableConfig({ command, workspace, env, signal, runProcess,
|
|
4
|
+
platform = process.platform }) {
|
|
5
|
+
const options = ['--disable', 'plugins', '--disable', 'apps', '--disable', 'multi_agent',
|
|
6
|
+
'-c', 'hooks.enabled=false', '-c', 'notify=[]', '-c', 'project_doc_max_bytes=0',
|
|
7
|
+
'-c', 'sandbox_workspace_write.writable_roots=[]', '-c', 'sandbox_workspace_write.network_access=false',
|
|
8
|
+
'-c', 'approval_policy="never"', '-c', 'approvals_reviewer="user"',
|
|
9
|
+
'-c', 'features.browser_use=false', '-c', 'features.computer_use=false',
|
|
10
|
+
...(platform === 'win32' ? ['-c', 'windows.sandbox="unelevated"'] : [])];
|
|
11
|
+
const list = async (overrides) => {
|
|
12
|
+
const result = await runProcess(command, ['mcp', 'list', '--json', ...overrides],
|
|
13
|
+
{ cwd: workspace, env, signal, timeoutMs: 10000 });
|
|
14
|
+
if (result.code !== 0) throw isolationFailure();
|
|
15
|
+
let servers;
|
|
16
|
+
try { servers = JSON.parse(result.stdout); } catch { throw isolationFailure(); }
|
|
17
|
+
if (!Array.isArray(servers) || servers.length > 100 || servers.some(server =>
|
|
18
|
+
!server || !/^[A-Za-z0-9_-]{1,128}$/.test(server.name) || typeof server.enabled !== 'boolean')) throw isolationFailure();
|
|
19
|
+
return servers;
|
|
20
|
+
};
|
|
21
|
+
const servers = await list(options);
|
|
22
|
+
for (const server of servers) options.push('-c', `mcp_servers.${server.name}.enabled=false`);
|
|
23
|
+
if ((await list(options)).some(server => server.enabled)) throw isolationFailure();
|
|
24
|
+
return options;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isolationFailure() {
|
|
28
|
+
return Object.assign(new Error('Codex effective MCP isolation could not be verified'), { code: 'runtime_isolation_failed' });
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { runParticipantProcess, parseParticipantEvents } from '../../roundtables/process.js';
|
|
2
|
+
import { mkdtempSync, writeFileSync, unlinkSync, rmdirSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { prepareCodexRoundtableConfig } from './roundtable-config.js';
|
|
6
|
+
|
|
7
|
+
export function createCodexParticipant({ command = process.env.FULI_CODEX_BIN ?? 'codex',
|
|
8
|
+
workspace = process.cwd(), env = process.env, model, reasoningEffort = env.FULI_CODEX_REASONING_EFFORT,
|
|
9
|
+
runProcess = runParticipantProcess } = {}) {
|
|
10
|
+
if (reasoningEffort && !['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(reasoningEffort)) throw new TypeError('Unsupported Codex reasoning effort');
|
|
11
|
+
const clientEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith('FULI_')));
|
|
12
|
+
return {
|
|
13
|
+
async preflight({ signal } = {}) {
|
|
14
|
+
try {
|
|
15
|
+
const version = await runProcess(command, ['--version'], { cwd: workspace, env: clientEnv, signal, timeoutMs: 10_000 });
|
|
16
|
+
const auth = await runProcess(command, ['login', 'status'], { cwd: workspace, env: clientEnv, signal, timeoutMs: 10_000 });
|
|
17
|
+
return { ready: version.code === 0 && auth.code === 0, reason: auth.code === 0 ? null : 'waiting_auth', authentication: 'local_client_configuration' };
|
|
18
|
+
} catch (error) { return { ready: false, reason: error.code ?? 'runtime_unavailable' }; }
|
|
19
|
+
},
|
|
20
|
+
async dispatch({ prompt, signal, allowWrite = false, resultSchema }) {
|
|
21
|
+
const overrides = await prepareCodexRoundtableConfig({ command, workspace, env: clientEnv, signal, runProcess });
|
|
22
|
+
const directory = resultSchema ? mkdtempSync(join(tmpdir(), 'fuli-roundtable-schema-')) : null;
|
|
23
|
+
const schemaPath = directory ? join(directory, 'result.json') : null;
|
|
24
|
+
if (schemaPath) writeFileSync(schemaPath, JSON.stringify(resultSchema), { mode: 0o600 });
|
|
25
|
+
try {
|
|
26
|
+
const args = ['exec', '--ignore-rules', '--ephemeral', '--skip-git-repo-check',
|
|
27
|
+
'--sandbox', allowWrite ? 'workspace-write' : 'read-only', '--color', 'never', '--json', '-C', workspace,
|
|
28
|
+
...overrides, ...(reasoningEffort ? ['-c', `model_reasoning_effort="${reasoningEffort}"`] : []),
|
|
29
|
+
...(model ? ['--model', model] : []), ...(schemaPath ? ['--output-schema', schemaPath] : []), '-'];
|
|
30
|
+
return parseParticipantEvents('codex', await runProcess(command, args, { cwd: workspace, env: clientEnv, input: prompt, signal }));
|
|
31
|
+
} finally { if (schemaPath) unlinkSync(schemaPath); if (directory) rmdirSync(directory); }
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|