evolcore 0.0.29 → 0.0.30
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/CHANGELOG.md +7 -0
- package/dist/agents/codex-runner.js +61 -14
- package/dist/aun/aid/agentmd.js +224 -19
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/managed-operation.js +70 -4
- package/dist/aun/group/index.js +1 -0
- package/dist/{core → aun}/group/invite-service.js +4 -4
- package/dist/channels/daemon.js +1 -1
- package/dist/cli/agent-command.js +7 -7
- package/dist/cli/aun-commands.js +97 -37
- package/dist/cli/data-command.js +2 -1
- package/dist/cli/managed-command-guard.js +2 -2
- package/dist/cli/model.js +1 -1
- package/dist/cli/response.js +1 -1
- package/dist/cli/trigger-command.js +52 -29
- package/dist/config/boot-log.js +95 -1
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/peer-role-resolver.js +21 -0
- package/dist/core/agent-ipc-registration.js +409 -0
- package/dist/core/agent-runtime.js +435 -0
- package/dist/core/auth/delegation-authorization.js +58 -0
- package/dist/core/auth/operation-catalog.js +8 -0
- package/dist/core/bootstrap-service.js +65 -1
- package/dist/core/channel-runtime.js +435 -0
- package/dist/core/command/command-handler.js +4 -1
- package/dist/core/daemon-bootstrap.js +151 -0
- package/dist/core/daemon-control.js +205 -0
- package/dist/core/daemon-lifecycle.js +225 -0
- package/dist/core/data-migration.js +4 -1
- package/dist/core/handoff/ipc-registration.js +156 -0
- package/dist/core/ipc-registration.js +457 -0
- package/dist/core/memory/consolidation-trigger.js +8 -0
- package/dist/core/memory/consolidation.js +838 -0
- package/dist/core/message/aun-ipc-sender.js +226 -0
- package/dist/core/message/outbound.js +165 -0
- package/dist/core/permission/approval-gateway.js +13 -0
- package/dist/core/permission/ec-command-parser.js +80 -35
- package/dist/core/permission/sandbox-runtime.js +23 -2
- package/dist/core/permission/tool-error-code.js +1 -0
- package/dist/core/permission/tool-policy.js +61 -24
- package/dist/core/session/session-idle-monitor.js +17 -0
- package/dist/index.js +352 -4841
- package/dist/ipc.js +2 -1
- package/dist/stats/db.js +24 -0
- package/dist/stats/index.js +1 -1
- package/dist/stats/writer.js +25 -1
- package/dist/trigger/application-service.js +157 -0
- package/dist/trigger/authorization.js +371 -0
- package/dist/trigger/ipc-service.js +406 -0
- package/dist/trigger/local-files.js +133 -0
- package/dist/trigger/manager.js +235 -22
- package/dist/trigger/scheduler.js +30 -5
- package/dist/trigger/script-executor.js +176 -39
- package/dist/trigger/script-sandbox.js +458 -0
- package/dist/trigger/system-definitions.js +188 -0
- package/dist/trigger/validation.js +0 -3
- package/dist/utils/aid-bind.js +1 -1
- package/dist/utils/evolcore-version.js +74 -0
- package/dist/utils/logger.js +16 -0
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/aid.md +9 -1
- package/kits/docs/evolcore/model.md +3 -2
- package/kits/docs/evolcore/rpc.md +4 -0
- package/kits/docs/evolcore/trigger.md +15 -1
- package/kits/schemas/relation-config.schema.9.json +1 -1
- package/kits/templates/system-fragments/commands.md +0 -1
- package/package.json +1 -1
- package/dist/core/memory/launch-consolidation.js +0 -395
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { authorizeDelegatedAunMsgSend, } from '../auth/agent-delegation.js';
|
|
2
|
+
import { tryParseChannelKey } from '../channel-loader.js';
|
|
3
|
+
import { AUN_HANDOFF_MARKER_FIELD } from '../../channels/aun.js';
|
|
4
|
+
import { classifyAunPayloadForLog } from './message-log.js';
|
|
5
|
+
import { isValidAunDeliveryForChannel } from './outbound.js';
|
|
6
|
+
import { isHClassPath } from '../permission/protected-paths.js';
|
|
7
|
+
import { evaluateOutboundContactAdmission } from './message-admission.js';
|
|
8
|
+
import { normalizeAunMentionEntries } from '../../aun/msg/mention-schema.js';
|
|
9
|
+
export function createAunHandoffSender(sessionManager, channelInstances) {
|
|
10
|
+
return async (handoff) => {
|
|
11
|
+
const targetSession = await sessionManager.getSessionById(handoff.target_session_id);
|
|
12
|
+
if (!targetSession)
|
|
13
|
+
return { ok: false, error: 'target session not found' };
|
|
14
|
+
const instance = channelInstances.find(candidate => (candidate.channelType === 'aun' && (candidate.adapter.channelKey === targetSession.metadata?.channelKey
|
|
15
|
+
|| candidate.adapter.channelName === targetSession.channel
|
|
16
|
+
|| tryParseChannelKey(candidate.adapter.channelKey)?.selfAID === targetSession.selfAID)));
|
|
17
|
+
const channel = instance?.channel;
|
|
18
|
+
if (!channel) {
|
|
19
|
+
return { ok: false, error: `AUN channel not found for ${targetSession.selfAID || '<unknown>'}` };
|
|
20
|
+
}
|
|
21
|
+
const classified = classifyAunPayloadForLog(handoff.request.payload);
|
|
22
|
+
const log = {
|
|
23
|
+
content: classified.content,
|
|
24
|
+
sessionId: targetSession.id,
|
|
25
|
+
threadId: targetSession.threadId || null,
|
|
26
|
+
msgType: classified.msgType,
|
|
27
|
+
payloadType: classified.payloadType,
|
|
28
|
+
payloadSummary: classified.payloadSummary,
|
|
29
|
+
source: 'msg',
|
|
30
|
+
handoffTrace: { version: 2, handoff_id: handoff.handoff_id },
|
|
31
|
+
};
|
|
32
|
+
const delivery = handoff.request.delivery;
|
|
33
|
+
if (!isValidAunDeliveryForChannel(delivery, targetSession.channelId)) {
|
|
34
|
+
return { ok: false, error: 'handoff is missing a valid immutable AUN delivery route' };
|
|
35
|
+
}
|
|
36
|
+
const result = delivery.chatType === 'group'
|
|
37
|
+
? typeof channel.sendDaemonGroupMsg === 'function'
|
|
38
|
+
? await channel.sendDaemonGroupMsg({
|
|
39
|
+
groupId: delivery.groupId,
|
|
40
|
+
payload: { ...handoff.request.payload, [AUN_HANDOFF_MARKER_FIELD]: handoff.handoff_id },
|
|
41
|
+
encrypt: handoff.request.encrypt,
|
|
42
|
+
causation: handoff.causation,
|
|
43
|
+
log,
|
|
44
|
+
})
|
|
45
|
+
: { ok: false, error: 'AUN channel does not support daemon group sends' }
|
|
46
|
+
: typeof channel.sendDaemonMsg === 'function'
|
|
47
|
+
? await channel.sendDaemonMsg({
|
|
48
|
+
to: targetSession.channelId,
|
|
49
|
+
payload: handoff.request.payload,
|
|
50
|
+
encrypt: handoff.request.encrypt,
|
|
51
|
+
causation: handoff.causation,
|
|
52
|
+
log,
|
|
53
|
+
})
|
|
54
|
+
: { ok: false, error: 'AUN channel does not support daemon private sends' };
|
|
55
|
+
return { ok: result.ok, message_id: result.message_id, error: result.error };
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function registerAunIpcSender({ ipcServer, channelInstances, agentDelegationRegistry, responseEngine, handoffRuntime, }) {
|
|
59
|
+
const sender = async (params) => {
|
|
60
|
+
const runtimeCausation = params.originSessionId
|
|
61
|
+
? responseEngine.getTaskRuntimeContext(params.originSessionId)?.causation
|
|
62
|
+
: undefined;
|
|
63
|
+
const delegation = authorizeDelegatedAunMsgSend(agentDelegationRegistry, {
|
|
64
|
+
delegationToken: params.delegationToken,
|
|
65
|
+
delegationCommandHash: params.delegationCommandHash,
|
|
66
|
+
sessionId: params.originSessionId,
|
|
67
|
+
messageId: params.originMessageId,
|
|
68
|
+
aid: params.aid,
|
|
69
|
+
to: params.to,
|
|
70
|
+
scope: params.scope,
|
|
71
|
+
action: params.file ? 'file' : 'send',
|
|
72
|
+
});
|
|
73
|
+
if (!delegation.ok) {
|
|
74
|
+
return { ok: false, error: delegation.reason, code: delegation.code };
|
|
75
|
+
}
|
|
76
|
+
const inst = channelInstances.find((candidate) => {
|
|
77
|
+
if (candidate.channelType !== 'aun')
|
|
78
|
+
return false;
|
|
79
|
+
const ch = candidate.channel;
|
|
80
|
+
try {
|
|
81
|
+
const aidState = typeof ch?.getAidState === 'function' ? ch.getAidState() : null;
|
|
82
|
+
if (aidState?.aid === params.aid)
|
|
83
|
+
return true;
|
|
84
|
+
if (typeof ch?.getAid === 'function' && ch.getAid() === params.aid)
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch { /* ignore */ }
|
|
88
|
+
return false;
|
|
89
|
+
});
|
|
90
|
+
const ch = inst?.channel;
|
|
91
|
+
if (!ch) {
|
|
92
|
+
return { ok: false, error: `AUN channel not found for ${params.aid}`, code: 'AUN_CHANNEL_NOT_FOUND' };
|
|
93
|
+
}
|
|
94
|
+
if (params.scope === 'msg') {
|
|
95
|
+
const admission = await evaluateOutboundContactAdmission({ selfAid: params.aid, targetAid: params.to });
|
|
96
|
+
if (!admission.allow) {
|
|
97
|
+
return { ok: false, error: admission.error, code: admission.code };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// The IPC command already carries the caller-selected transport scope.
|
|
101
|
+
// Never reclassify the target by parsing its AID string: a private AID may
|
|
102
|
+
// look group-like and a group identifier may use a future format.
|
|
103
|
+
const encrypt = typeof params.encrypt === 'boolean'
|
|
104
|
+
? params.encrypt
|
|
105
|
+
: (typeof ch.getDefaultEncrypt === 'function' ? ch.getDefaultEncrypt() : true);
|
|
106
|
+
let payload = params.payload;
|
|
107
|
+
if (params.file) {
|
|
108
|
+
if (isHClassPath(params.file.filePath)) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
error: 'H-class protected files cannot be uploaded by an agent task',
|
|
112
|
+
code: 'H_CLASS_PROTECTED',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (typeof ch.buildDaemonFilePayload !== 'function') {
|
|
116
|
+
return { ok: false, error: 'AUN channel does not support daemon file uploads', code: 'AUN_FILE_UNSUPPORTED' };
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
payload = await ch.buildDaemonFilePayload(params.file);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
error: error instanceof Error ? error.message : String(error),
|
|
125
|
+
code: error?.code ?? 'AUN_FILE_UPLOAD_FAILED',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!payload) {
|
|
130
|
+
return { ok: false, error: 'message payload is required', code: 'INVALID_PAYLOAD' };
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
if (Object.prototype.hasOwnProperty.call(payload, 'mentions')) {
|
|
134
|
+
payload = { ...payload, mentions: normalizeAunMentionEntries(payload.mentions) };
|
|
135
|
+
}
|
|
136
|
+
if (params.scope === 'group' && params.mentions !== undefined) {
|
|
137
|
+
const normalizedMentions = normalizeAunMentionEntries(params.mentions);
|
|
138
|
+
if (normalizedMentions.length > 0) {
|
|
139
|
+
payload = { ...payload, mentions: normalizedMentions };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
error: error instanceof Error ? error.message : String(error),
|
|
147
|
+
code: error?.code ?? 'INVALID_MENTION_SCHEMA',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (params.thread && typeof payload.thread_id !== 'string') {
|
|
151
|
+
payload = { ...payload, thread_id: params.thread };
|
|
152
|
+
}
|
|
153
|
+
let targetSessionId;
|
|
154
|
+
if (params.originSessionId && params.originMessageId) {
|
|
155
|
+
try {
|
|
156
|
+
const created = await handoffRuntime.createOutbound({
|
|
157
|
+
selfAid: params.aid,
|
|
158
|
+
to: params.to,
|
|
159
|
+
originSessionId: params.originSessionId,
|
|
160
|
+
originMessageId: params.originMessageId,
|
|
161
|
+
payload,
|
|
162
|
+
encrypt,
|
|
163
|
+
thread: params.thread,
|
|
164
|
+
targetChatType: params.scope === 'group' ? 'group' : 'private',
|
|
165
|
+
explicitReturnPolicy: params.returnPolicy,
|
|
166
|
+
handoffContext: params.handoffContext,
|
|
167
|
+
originAuthorization: {
|
|
168
|
+
actorId: delegation.grant.actorId,
|
|
169
|
+
channelKey: delegation.grant.channel,
|
|
170
|
+
channelType: delegation.grant.channelType,
|
|
171
|
+
chatType: delegation.grant.chatType,
|
|
172
|
+
peerKey: delegation.grant.peerKey,
|
|
173
|
+
},
|
|
174
|
+
causation: runtimeCausation,
|
|
175
|
+
});
|
|
176
|
+
targetSessionId = created.targetSession.id;
|
|
177
|
+
if (created.crossSession && created.handoff) {
|
|
178
|
+
return {
|
|
179
|
+
ok: true,
|
|
180
|
+
status: created.handoff.state === 'target_sent' ? 'delivered' : 'queued',
|
|
181
|
+
message_id: created.handoff.target_message_id ?? undefined,
|
|
182
|
+
handoff_id: created.handoff.handoff_id,
|
|
183
|
+
target_session_id: created.targetSession.id,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (params.scope === 'msg' && !payload.ref_message_id) {
|
|
187
|
+
payload.ref_message_id = params.originMessageId;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const code = error?.code;
|
|
192
|
+
const handoffId = error?.handoffId
|
|
193
|
+
?? error?.blockingHandoffId;
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
error: error instanceof Error ? error.message : String(error),
|
|
197
|
+
code,
|
|
198
|
+
...(handoffId ? { handoff_id: handoffId } : {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (params.scope === 'group') {
|
|
203
|
+
if (typeof ch.sendDaemonGroupMsg !== 'function') {
|
|
204
|
+
return { ok: false, error: 'AUN channel does not support daemon group sends', code: 'AUN_GROUP_UNSUPPORTED' };
|
|
205
|
+
}
|
|
206
|
+
return await ch.sendDaemonGroupMsg({
|
|
207
|
+
groupId: params.to,
|
|
208
|
+
payload,
|
|
209
|
+
mentions: params.mentions,
|
|
210
|
+
encrypt,
|
|
211
|
+
log: params.log ? { ...params.log, sessionId: targetSessionId ?? params.log.sessionId } : undefined,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (typeof ch.sendDaemonMsg !== 'function') {
|
|
215
|
+
return { ok: false, error: 'AUN channel does not support daemon private sends', code: 'AUN_MSG_UNSUPPORTED' };
|
|
216
|
+
}
|
|
217
|
+
return await ch.sendDaemonMsg({
|
|
218
|
+
to: params.to,
|
|
219
|
+
payload,
|
|
220
|
+
encrypt,
|
|
221
|
+
causation: runtimeCausation,
|
|
222
|
+
log: params.log ? { ...params.log, sessionId: targetSessionId ?? params.log.sessionId } : undefined,
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
ipcServer.setAunMsgSender(sender);
|
|
226
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { isDeliveryTargetForChannel } from './message-utils.js';
|
|
2
|
+
import { readEvolcoreRuntimeFlavor, readEvolcoreStartedAtIso, readInstalledEvolcoreVersion, tagErrorTextWithEvolcoreVersion, } from '../../utils/evolcore-version.js';
|
|
3
|
+
/** 出站 payload 摘要(用于 channel-out.log)。 */
|
|
4
|
+
export function summarizeOutboundPayload(payload) {
|
|
5
|
+
if (!payload)
|
|
6
|
+
return { kind: 'unknown' };
|
|
7
|
+
const summary = { kind: payload.kind };
|
|
8
|
+
switch (payload.kind) {
|
|
9
|
+
case 'activity.batch':
|
|
10
|
+
summary.itemCount = payload.items?.length ?? 0;
|
|
11
|
+
summary.items = payload.items;
|
|
12
|
+
break;
|
|
13
|
+
case 'result.text':
|
|
14
|
+
summary.isFinal = payload.isFinal;
|
|
15
|
+
summary.text = payload.text;
|
|
16
|
+
break;
|
|
17
|
+
case 'command.result':
|
|
18
|
+
case 'command.error':
|
|
19
|
+
case 'result.error':
|
|
20
|
+
summary.text = payload.text;
|
|
21
|
+
break;
|
|
22
|
+
case 'result.file':
|
|
23
|
+
summary.filePath = payload.filePath;
|
|
24
|
+
break;
|
|
25
|
+
case 'system.notice':
|
|
26
|
+
case 'system.error':
|
|
27
|
+
summary.subtype = payload.subtype;
|
|
28
|
+
summary.text = payload.text;
|
|
29
|
+
break;
|
|
30
|
+
case 'interaction':
|
|
31
|
+
summary.interactionId = payload.interaction?.id;
|
|
32
|
+
summary.interactionKind = payload.interaction?.kind?.kind;
|
|
33
|
+
break;
|
|
34
|
+
case 'status.started':
|
|
35
|
+
case 'status.progress':
|
|
36
|
+
case 'status.notice':
|
|
37
|
+
case 'status.requires_action':
|
|
38
|
+
case 'status.queued':
|
|
39
|
+
case 'status.completed':
|
|
40
|
+
case 'status.interrupted':
|
|
41
|
+
case 'status.error':
|
|
42
|
+
case 'status.timeout':
|
|
43
|
+
summary.metadata = payload.metadata;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
return summary;
|
|
47
|
+
}
|
|
48
|
+
/** Add the current EvolCore version marker to user-visible error payloads. */
|
|
49
|
+
export function tagErrorPayloadWithEvolcoreVersion(payload) {
|
|
50
|
+
switch (payload.kind) {
|
|
51
|
+
case 'result.error':
|
|
52
|
+
case 'system.error':
|
|
53
|
+
return { ...payload, text: tagErrorTextWithEvolcoreVersion(payload.text) };
|
|
54
|
+
case 'command.error':
|
|
55
|
+
if (!payload.reasonCode)
|
|
56
|
+
return payload;
|
|
57
|
+
return { ...payload, text: tagErrorTextWithEvolcoreVersion(payload.text) };
|
|
58
|
+
case 'status.error': {
|
|
59
|
+
const metadata = payload.metadata;
|
|
60
|
+
return {
|
|
61
|
+
...payload,
|
|
62
|
+
metadata: {
|
|
63
|
+
...metadata,
|
|
64
|
+
...(typeof metadata?.message === 'string'
|
|
65
|
+
? { message: tagErrorTextWithEvolcoreVersion(metadata.message) }
|
|
66
|
+
: {}),
|
|
67
|
+
evolcoreVersion: readInstalledEvolcoreVersion(),
|
|
68
|
+
evolcoreFlavor: readEvolcoreRuntimeFlavor(),
|
|
69
|
+
evolcoreStartedAt: readEvolcoreStartedAtIso(),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return payload;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function shouldCountSentPayload(payload) {
|
|
78
|
+
return [
|
|
79
|
+
'result.text',
|
|
80
|
+
'result.file',
|
|
81
|
+
'result.image',
|
|
82
|
+
'result.error',
|
|
83
|
+
'command.result',
|
|
84
|
+
'command.error',
|
|
85
|
+
'interaction',
|
|
86
|
+
].includes(payload.kind);
|
|
87
|
+
}
|
|
88
|
+
export function outboundPayloadToLogText(payload) {
|
|
89
|
+
switch (payload.kind) {
|
|
90
|
+
case 'result.text':
|
|
91
|
+
case 'command.result':
|
|
92
|
+
case 'command.error':
|
|
93
|
+
case 'result.error':
|
|
94
|
+
return { text: payload.text, msgType: 'text' };
|
|
95
|
+
case 'result.file':
|
|
96
|
+
return { text: payload.fileName || payload.filePath, msgType: 'file' };
|
|
97
|
+
case 'result.image':
|
|
98
|
+
return { text: payload.alt || '[image]', msgType: 'image' };
|
|
99
|
+
case 'interaction':
|
|
100
|
+
return { text: payload.fallbackText || '[interaction]', msgType: 'text' };
|
|
101
|
+
default:
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function normalizeMessageSource(value) {
|
|
106
|
+
return value === 'cli' || value === 'msg' || value === 'ctl' || value === 'owner-inject'
|
|
107
|
+
? value
|
|
108
|
+
: 'daemon';
|
|
109
|
+
}
|
|
110
|
+
/** Preserve the authenticated AUN route for control-plane replies. */
|
|
111
|
+
export function controlReplyContext(opts, metadata) {
|
|
112
|
+
const supplied = opts.replyContext?.delivery;
|
|
113
|
+
if (supplied) {
|
|
114
|
+
const matchesInbound = opts.chatType === 'group'
|
|
115
|
+
? supplied.chatType === 'group' && supplied.groupId === opts.groupId
|
|
116
|
+
: supplied.chatType === 'private';
|
|
117
|
+
if (!matchesInbound) {
|
|
118
|
+
throw Object.assign(new Error('AUN control reply route does not match its authenticated inbound context'), {
|
|
119
|
+
code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const delivery = supplied
|
|
124
|
+
?? (opts.chatType === 'group'
|
|
125
|
+
? (opts.groupId ? { chatType: 'group', groupId: opts.groupId } : undefined)
|
|
126
|
+
: { chatType: 'private' });
|
|
127
|
+
if (!delivery) {
|
|
128
|
+
throw Object.assign(new Error('AUN control reply is missing a trusted groupId'), {
|
|
129
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
...(opts.replyContext ?? {}),
|
|
134
|
+
delivery,
|
|
135
|
+
metadata: {
|
|
136
|
+
...(opts.replyContext?.metadata ?? {}),
|
|
137
|
+
...(opts.encrypted === undefined ? {} : { encrypted: opts.encrypted }),
|
|
138
|
+
...(opts.protectedHeaders ? { protectedHeaders: opts.protectedHeaders } : {}),
|
|
139
|
+
...(metadata ?? {}),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export function isValidAunDeliveryForChannel(delivery, channelId) {
|
|
144
|
+
return isDeliveryTargetForChannel(delivery, channelId);
|
|
145
|
+
}
|
|
146
|
+
function restartChannelType(channel) {
|
|
147
|
+
return channel?.split('#', 1)[0].toLowerCase();
|
|
148
|
+
}
|
|
149
|
+
/** Restore a persisted restart route without inferring AUN group semantics. */
|
|
150
|
+
export function resolvePersistedRestartDelivery(opts) {
|
|
151
|
+
const requiresAunRoute = restartChannelType(opts.channel) === 'aun'
|
|
152
|
+
|| restartChannelType(opts.adapterChannel) === 'aun';
|
|
153
|
+
if (!requiresAunRoute) {
|
|
154
|
+
return isDeliveryTargetForChannel(opts.delivery, opts.channelId)
|
|
155
|
+
? { valid: true, delivery: opts.delivery }
|
|
156
|
+
: { valid: true };
|
|
157
|
+
}
|
|
158
|
+
return isDeliveryTargetForChannel(opts.delivery, opts.channelId)
|
|
159
|
+
? { valid: true, delivery: opts.delivery }
|
|
160
|
+
: { valid: false };
|
|
161
|
+
}
|
|
162
|
+
/** Send a system payload through the channel's unified outbound entry point. */
|
|
163
|
+
export async function sendSystemPayload(adapter, envelope, payload) {
|
|
164
|
+
await adapter.send(envelope, payload);
|
|
165
|
+
}
|
|
@@ -9,6 +9,7 @@ import { recordCausationSpan } from '../causation/audit.js';
|
|
|
9
9
|
import { checkDangerousCommand } from './tool-policy.js';
|
|
10
10
|
import { resolveProtectedCandidate } from './protected-paths.js';
|
|
11
11
|
import { auditPermissionDecision, auditPermissionLifecycle } from '../auth/authorization-audit.js';
|
|
12
|
+
import { ConfigTarget, onConfigWrite } from '../../config/config-manager.js';
|
|
12
13
|
export async function requestDangerousCommandPermission(gateway, sessionId, toolName, input, sendPrompt, context, grantScope = 'default', mode = 'request') {
|
|
13
14
|
const dangerCheck = checkDangerousCommand(toolName, input);
|
|
14
15
|
if (!dangerCheck.isDangerous) {
|
|
@@ -302,6 +303,18 @@ export class PermissionGateway {
|
|
|
302
303
|
temporaryGrants = new Map();
|
|
303
304
|
managementRevalidationTimer;
|
|
304
305
|
eventBus;
|
|
306
|
+
registerConfigRevalidation() {
|
|
307
|
+
return onConfigWrite(({ target, selector }) => {
|
|
308
|
+
if ((target !== ConfigTarget.Agent
|
|
309
|
+
&& target !== ConfigTarget.Contact
|
|
310
|
+
&& target !== ConfigTarget.Relation) || !selector.self)
|
|
311
|
+
return;
|
|
312
|
+
const cancelled = this.revalidatePendingApprovals(selector.self);
|
|
313
|
+
if (cancelled > 0) {
|
|
314
|
+
logger.info(`[PermissionGateway] Cancelled ${cancelled} stale management approval(s) after ${target} config write: agent=${selector.self}`);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
305
318
|
setEventBus(eventBus) {
|
|
306
319
|
this.eventBus = eventBus;
|
|
307
320
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isTrustedWindowsShellExecutable } from '../../utils/windows-shell-trust.js';
|
|
4
|
-
function parseApplyPatchCommand(command) {
|
|
4
|
+
function parseApplyPatchCommand(command, options = {}) {
|
|
5
5
|
const input = command.trim();
|
|
6
6
|
let commandText = input;
|
|
7
7
|
if (commandText.startsWith('&'))
|
|
@@ -14,15 +14,10 @@ function parseApplyPatchCommand(command) {
|
|
|
14
14
|
return { kind: 'not-apply-patch' };
|
|
15
15
|
const remainder = commandText.slice(executableMatch[0].length).trimStart();
|
|
16
16
|
if (!remainder)
|
|
17
|
-
return
|
|
18
|
-
const literal = parseLiteralShellCommand(
|
|
17
|
+
return parseApplyPatchArgv([executable]);
|
|
18
|
+
const literal = parseLiteralShellCommand(commandText, { ...options, allowManagedTmpDir: true });
|
|
19
19
|
if (literal.ok && argvUsesShellApplyPatch(literal.argv)) {
|
|
20
|
-
|
|
21
|
-
return { kind: 'invalid', reason: 'apply_patch must carry exactly one patch document' };
|
|
22
|
-
const fileChanges = parseApplyPatchDocument(literal.argv[1]);
|
|
23
|
-
return fileChanges
|
|
24
|
-
? { kind: 'file-change', fileChanges }
|
|
25
|
-
: { kind: 'invalid', reason: 'patch document is not a complete apply_patch document' };
|
|
20
|
+
return parseApplyPatchArgv(literal.argv);
|
|
26
21
|
}
|
|
27
22
|
const heredoc = remainder.match(/^<<-?\s*(?:'([^']+)'|"([^"]+)"|([^\s]+))\s*\r?\n([\s\S]*)$/u);
|
|
28
23
|
if (!heredoc)
|
|
@@ -38,12 +33,64 @@ function parseApplyPatchCommand(command) {
|
|
|
38
33
|
? { kind: 'file-change', fileChanges }
|
|
39
34
|
: { kind: 'invalid', reason: 'patch document is not a complete apply_patch document' };
|
|
40
35
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
46
|
-
|
|
36
|
+
function parseApplyPatchArgv(argv) {
|
|
37
|
+
if (!argvUsesShellApplyPatch(argv))
|
|
38
|
+
return { kind: 'not-apply-patch' };
|
|
39
|
+
if (argv.length !== 2 || !argv[1].trimStart().startsWith('*** ')) {
|
|
40
|
+
return {
|
|
41
|
+
kind: 'invalid',
|
|
42
|
+
reason: 'apply_patch requires exactly one complete patch document',
|
|
43
|
+
errorDetailCode: 'INVALID_ARGUMENT',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const fileChanges = parseApplyPatchDocument(argv[1]);
|
|
47
|
+
return fileChanges
|
|
48
|
+
? { kind: 'file-change', fileChanges }
|
|
49
|
+
: { kind: 'invalid', reason: 'patch document is not a complete apply_patch document' };
|
|
50
|
+
}
|
|
51
|
+
function composedApplyPatchOnCommandLine(command, options = {}) {
|
|
52
|
+
const commandText = stripHeredocBodies(command);
|
|
53
|
+
const escape = options.dialect === 'powershell' ? '`' : options.dialect === 'cmd' ? '^' : '\\';
|
|
54
|
+
let quote = null;
|
|
55
|
+
for (let index = 0; index < commandText.length; index++) {
|
|
56
|
+
const char = commandText[index];
|
|
57
|
+
if (quote === 'single') {
|
|
58
|
+
if (char === "'")
|
|
59
|
+
quote = null;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (char === escape) {
|
|
63
|
+
index++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (quote === 'double') {
|
|
67
|
+
if (char === '"')
|
|
68
|
+
quote = null;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (char === "'" && options.dialect !== 'cmd') {
|
|
72
|
+
quote = 'single';
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (char === '"') {
|
|
76
|
+
quote = 'double';
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (char === '#' && options.dialect !== 'cmd'
|
|
80
|
+
&& (index === 0 || /\s/u.test(commandText[index - 1]))) {
|
|
81
|
+
const newline = commandText.indexOf('\n', index);
|
|
82
|
+
if (newline < 0)
|
|
83
|
+
break;
|
|
84
|
+
index = newline - 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === '|' || char === ';' || char === '&' || char === '\r' || char === '\n') {
|
|
88
|
+
const executable = leadingShellExecutable(commandText.slice(index + 1));
|
|
89
|
+
if (executable && isApplyPatchExecutable(executable))
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
47
94
|
}
|
|
48
95
|
export function parseShellApplyPatchInvocation(input, options = {}, parsedArgv) {
|
|
49
96
|
const rawCommand = typeof input.command === 'string' ? input.command : '';
|
|
@@ -56,39 +103,37 @@ export function parseShellApplyPatchInvocation(input, options = {}, parsedArgv)
|
|
|
56
103
|
if (value && !candidates.includes(value))
|
|
57
104
|
candidates.push(value);
|
|
58
105
|
};
|
|
59
|
-
|
|
106
|
+
const carrier = rawCommandArgv
|
|
107
|
+
? resolveCodexShellCarrierArgv(rawCommandArgv)
|
|
108
|
+
: resolveCodexShellCarrierString(rawCommand);
|
|
60
109
|
if (rawCommandArgv) {
|
|
61
110
|
if (argvUsesShellApplyPatch(rawCommandArgv)) {
|
|
62
|
-
|
|
63
|
-
const fileChanges = parseApplyPatchDocument(rawCommandArgv[1]);
|
|
64
|
-
return fileChanges
|
|
65
|
-
? { kind: 'file-change', fileChanges }
|
|
66
|
-
: { kind: 'invalid', reason: 'patch document is not a complete apply_patch document' };
|
|
67
|
-
}
|
|
68
|
-
return { kind: 'invalid', reason: 'apply_patch must carry exactly one patch document' };
|
|
111
|
+
return parseApplyPatchArgv(rawCommandArgv);
|
|
69
112
|
}
|
|
70
|
-
addCandidate(resolveCodexShellCarrierArgv(rawCommandArgv)?.command);
|
|
71
113
|
}
|
|
114
|
+
else {
|
|
115
|
+
addCandidate(rawCommand);
|
|
116
|
+
}
|
|
117
|
+
addCandidate(carrier?.command);
|
|
72
118
|
if (parsedArgv && argvUsesShellApplyPatch(parsedArgv)) {
|
|
73
|
-
return
|
|
119
|
+
return parseApplyPatchArgv(parsedArgv);
|
|
74
120
|
}
|
|
75
|
-
|
|
76
|
-
let sawApplyPatch = false;
|
|
77
|
-
let invalidReason = 'apply_patch payload could not be reconstructed safely';
|
|
121
|
+
let invalid;
|
|
78
122
|
for (const candidate of candidates) {
|
|
79
|
-
const
|
|
123
|
+
const candidateOptions = candidate === carrier?.command
|
|
124
|
+
? { ...options, dialect: carrier.dialect }
|
|
125
|
+
: options;
|
|
126
|
+
const parsed = parseApplyPatchCommand(candidate, candidateOptions);
|
|
80
127
|
if (parsed.kind !== 'not-apply-patch') {
|
|
81
|
-
sawApplyPatch = true;
|
|
82
128
|
if (parsed.kind === 'file-change')
|
|
83
129
|
return parsed;
|
|
84
|
-
|
|
130
|
+
invalid = parsed;
|
|
85
131
|
}
|
|
86
|
-
else if (composedApplyPatchOnCommandLine(candidate)) {
|
|
87
|
-
|
|
88
|
-
invalidReason = 'apply_patch must be the only shell command';
|
|
132
|
+
else if (composedApplyPatchOnCommandLine(candidate, candidateOptions)) {
|
|
133
|
+
invalid = { kind: 'invalid', reason: 'apply_patch must be the only shell command' };
|
|
89
134
|
}
|
|
90
135
|
}
|
|
91
|
-
return
|
|
136
|
+
return invalid ?? { kind: 'not-apply-patch' };
|
|
92
137
|
}
|
|
93
138
|
/**
|
|
94
139
|
* stdout filters allowed after an EC invocation. Each entry reads stdin and
|
|
@@ -298,6 +298,11 @@ function pushWritableBind(args, candidate) {
|
|
|
298
298
|
return;
|
|
299
299
|
args.push('--bind', candidate, candidate);
|
|
300
300
|
}
|
|
301
|
+
function pushReadonlyBind(args, candidate) {
|
|
302
|
+
if (!fs.existsSync(candidate))
|
|
303
|
+
return;
|
|
304
|
+
args.push('--ro-bind', candidate, candidate);
|
|
305
|
+
}
|
|
301
306
|
function pushMaskedFile(args, candidate) {
|
|
302
307
|
if (!fs.existsSync(candidate))
|
|
303
308
|
return;
|
|
@@ -658,7 +663,10 @@ export function buildBubblewrapCommand(executable, executableArgs, options) {
|
|
|
658
663
|
'--proc', '/proc',
|
|
659
664
|
'--tmpfs', '/tmp',
|
|
660
665
|
];
|
|
661
|
-
|
|
666
|
+
if (options.projectWritable === false)
|
|
667
|
+
pushReadonlyBind(args, projectPath);
|
|
668
|
+
else
|
|
669
|
+
pushWritableBind(args, projectPath);
|
|
662
670
|
for (const writablePath of options.writablePaths ?? []) {
|
|
663
671
|
const resolved = path.resolve(writablePath);
|
|
664
672
|
if (isSameOrDescendant(resolved, projectPath))
|
|
@@ -671,9 +679,22 @@ export function buildBubblewrapCommand(executable, executableArgs, options) {
|
|
|
671
679
|
continue;
|
|
672
680
|
args.push('--ro-bind', resolved, resolved);
|
|
673
681
|
}
|
|
682
|
+
for (const maskedPath of options.maskedPaths ?? []) {
|
|
683
|
+
const resolved = path.resolve(maskedPath);
|
|
684
|
+
if (isSameOrDescendant(projectPath, resolved))
|
|
685
|
+
continue;
|
|
686
|
+
if (fs.existsSync(resolved)) {
|
|
687
|
+
const stat = fs.lstatSync(resolved);
|
|
688
|
+
if (stat.isDirectory())
|
|
689
|
+
pushMaskedDirectory(args, resolved);
|
|
690
|
+
else
|
|
691
|
+
pushMaskedFile(args, resolved);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
674
694
|
appendLClassReadOnlyBinds(args, root, options);
|
|
675
695
|
appendHClassMasks(args, root, true, options);
|
|
676
|
-
|
|
696
|
+
if (options.exposeDaemonIpc !== false)
|
|
697
|
+
appendDaemonIpcSocket(args, root);
|
|
677
698
|
args.push('--chdir', projectPath, '--', executable, ...executableArgs);
|
|
678
699
|
return { command: bubblewrapPath, args };
|
|
679
700
|
}
|
|
@@ -7,6 +7,7 @@ const PERMISSION_ERROR_CODES = new Set([
|
|
|
7
7
|
]);
|
|
8
8
|
const DETAIL_CODE_BY_POLICY_CODE = {
|
|
9
9
|
native_file_change_required: 'NATIVE_FILE_CHANGE_REQUIRED',
|
|
10
|
+
apply_patch_invalid_argument: 'INVALID_ARGUMENT',
|
|
10
11
|
apply_patch_unparseable: 'APPLY_PATCH_UNPARSEABLE',
|
|
11
12
|
apply_patch_workspace_escape: 'APPLY_PATCH_WORKSPACE_ESCAPE',
|
|
12
13
|
ec_shell_invalid_quote: 'INVALID_QUOTE',
|