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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/agents/codex-runner.js +61 -14
  3. package/dist/aun/aid/agentmd.js +224 -19
  4. package/dist/aun/aid/index.js +2 -1
  5. package/dist/aun/aid/managed-operation.js +70 -4
  6. package/dist/aun/group/index.js +1 -0
  7. package/dist/{core → aun}/group/invite-service.js +4 -4
  8. package/dist/channels/daemon.js +1 -1
  9. package/dist/cli/agent-command.js +7 -7
  10. package/dist/cli/aun-commands.js +97 -37
  11. package/dist/cli/data-command.js +2 -1
  12. package/dist/cli/managed-command-guard.js +2 -2
  13. package/dist/cli/model.js +1 -1
  14. package/dist/cli/response.js +1 -1
  15. package/dist/cli/trigger-command.js +52 -29
  16. package/dist/config/boot-log.js +95 -1
  17. package/dist/config/builtin-roles.js +1 -0
  18. package/dist/config/peer-role-resolver.js +21 -0
  19. package/dist/core/agent-ipc-registration.js +409 -0
  20. package/dist/core/agent-runtime.js +435 -0
  21. package/dist/core/auth/delegation-authorization.js +58 -0
  22. package/dist/core/auth/operation-catalog.js +8 -0
  23. package/dist/core/bootstrap-service.js +65 -1
  24. package/dist/core/channel-runtime.js +435 -0
  25. package/dist/core/command/command-handler.js +4 -1
  26. package/dist/core/daemon-bootstrap.js +151 -0
  27. package/dist/core/daemon-control.js +205 -0
  28. package/dist/core/daemon-lifecycle.js +225 -0
  29. package/dist/core/data-migration.js +4 -1
  30. package/dist/core/handoff/ipc-registration.js +156 -0
  31. package/dist/core/ipc-registration.js +457 -0
  32. package/dist/core/memory/consolidation-trigger.js +8 -0
  33. package/dist/core/memory/consolidation.js +838 -0
  34. package/dist/core/message/aun-ipc-sender.js +226 -0
  35. package/dist/core/message/outbound.js +165 -0
  36. package/dist/core/permission/approval-gateway.js +13 -0
  37. package/dist/core/permission/ec-command-parser.js +80 -35
  38. package/dist/core/permission/sandbox-runtime.js +23 -2
  39. package/dist/core/permission/tool-error-code.js +1 -0
  40. package/dist/core/permission/tool-policy.js +61 -24
  41. package/dist/core/session/session-idle-monitor.js +17 -0
  42. package/dist/index.js +352 -4841
  43. package/dist/ipc.js +2 -1
  44. package/dist/stats/db.js +24 -0
  45. package/dist/stats/index.js +1 -1
  46. package/dist/stats/writer.js +25 -1
  47. package/dist/trigger/application-service.js +157 -0
  48. package/dist/trigger/authorization.js +371 -0
  49. package/dist/trigger/ipc-service.js +406 -0
  50. package/dist/trigger/local-files.js +133 -0
  51. package/dist/trigger/manager.js +235 -22
  52. package/dist/trigger/scheduler.js +30 -5
  53. package/dist/trigger/script-executor.js +176 -39
  54. package/dist/trigger/script-sandbox.js +458 -0
  55. package/dist/trigger/system-definitions.js +188 -0
  56. package/dist/trigger/validation.js +0 -3
  57. package/dist/utils/aid-bind.js +1 -1
  58. package/dist/utils/evolcore-version.js +74 -0
  59. package/dist/utils/logger.js +16 -0
  60. package/kits/docs/evolcore/INDEX.md +1 -1
  61. package/kits/docs/evolcore/aid.md +9 -1
  62. package/kits/docs/evolcore/model.md +3 -2
  63. package/kits/docs/evolcore/rpc.md +4 -0
  64. package/kits/docs/evolcore/trigger.md +15 -1
  65. package/kits/schemas/relation-config.schema.9.json +1 -1
  66. package/kits/templates/system-fragments/commands.md +0 -1
  67. package/package.json +1 -1
  68. 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 { kind: 'invalid', reason: 'missing patch document' };
18
- const literal = parseLiteralShellCommand(input, { allowManagedTmpDir: true });
17
+ return parseApplyPatchArgv([executable]);
18
+ const literal = parseLiteralShellCommand(commandText, { ...options, allowManagedTmpDir: true });
19
19
  if (literal.ok && argvUsesShellApplyPatch(literal.argv)) {
20
- if (literal.argv.length !== 2)
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 composedApplyPatchOnCommandLine(command) {
42
- const commandLine = command.split(/\r?\n/u, 1)[0] ?? command;
43
- return commandLine.split(/[|;&]/u).slice(1).some(segment => {
44
- const executable = leadingShellExecutable(segment);
45
- return !!executable && isApplyPatchExecutable(executable);
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
- addCandidate(rawCommand);
106
+ const carrier = rawCommandArgv
107
+ ? resolveCodexShellCarrierArgv(rawCommandArgv)
108
+ : resolveCodexShellCarrierString(rawCommand);
60
109
  if (rawCommandArgv) {
61
110
  if (argvUsesShellApplyPatch(rawCommandArgv)) {
62
- if (rawCommandArgv.length === 2) {
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 { kind: 'invalid', reason: 'apply_patch payload could not be reconstructed safely' };
119
+ return parseApplyPatchArgv(parsedArgv);
74
120
  }
75
- addCandidate(resolveCodexShellCarrierString(rawCommand)?.command);
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 parsed = parseApplyPatchCommand(candidate);
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
- invalidReason = parsed.reason;
130
+ invalid = parsed;
85
131
  }
86
- else if (composedApplyPatchOnCommandLine(candidate)) {
87
- sawApplyPatch = true;
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 sawApplyPatch ? { kind: 'invalid', reason: invalidReason } : { kind: 'not-apply-patch' };
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
- pushWritableBind(args, projectPath);
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
- appendDaemonIpcSocket(args, root);
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',