evolcore 0.0.19 → 0.0.21
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 +43 -1
- package/README.md +60 -9
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +383 -111
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +405 -137
- package/dist/agents/ecagent-runner.js +174 -63
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +66 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +86 -9
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +26 -11
- package/dist/aun/outbox.js +295 -68
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +1004 -273
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +81 -121
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +76 -24
- package/dist/cli/restart-monitor.js +3 -3
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +226 -24
- package/dist/config/config-operation-service.js +1 -2
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +29 -16
- package/dist/config/lifecycle.js +16 -5
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +161 -12
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +155 -4
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +27 -38
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/channel-loader.js +0 -2
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +9 -4
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +495 -72
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +465 -56
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +205 -13
- package/dist/core/permission/tool-policy.js +673 -62
- package/dist/core/relation/peer-identity.js +18 -0
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +18 -9
- package/dist/index.js +283 -65
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/stats.js +52 -18
- package/dist/utils/welcome.js +2 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/rules/01-overview.md +1 -1
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +10 -5
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +421 -0
- package/kits/schemas/daemon.schema.5.json +135 -0
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +119 -0
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +7 -3
- package/skills/eclink/SKILL.md +2 -0
|
@@ -8,6 +8,9 @@ export class AgentDelegationRegistry {
|
|
|
8
8
|
activeHashBySession = new Map();
|
|
9
9
|
approvedCommands = new Map();
|
|
10
10
|
issue(input) {
|
|
11
|
+
if (input.executionIdentity && !isValidFullAccessExecutionIdentity(input.executionIdentity)) {
|
|
12
|
+
throw new Error('invalid fullaccess task execution identity');
|
|
13
|
+
}
|
|
11
14
|
this.revokeSession(input.sessionId);
|
|
12
15
|
const token = crypto.randomBytes(32).toString('base64url');
|
|
13
16
|
const tokenHash = hashDelegationToken(token);
|
|
@@ -134,7 +137,7 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
|
|
|
134
137
|
if (!validation.ok)
|
|
135
138
|
return validation;
|
|
136
139
|
const grant = validation.grant;
|
|
137
|
-
if (grant.selfAid !== input.aid) {
|
|
140
|
+
if (grant.selfAid !== input.aid && !hasTrustedFullAccessDelegation(grant)) {
|
|
138
141
|
return { ok: false, code: 'INVALID_DELEGATION', reason: 'Delegation self agent does not match sender' };
|
|
139
142
|
}
|
|
140
143
|
if (grant.messageId && grant.messageId !== input.messageId) {
|
|
@@ -157,6 +160,7 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
|
|
|
157
160
|
conversationId: channelId,
|
|
158
161
|
processOwners: [],
|
|
159
162
|
fromControlChannel: false,
|
|
163
|
+
...trustedExecutionIdentityInput(grant),
|
|
160
164
|
});
|
|
161
165
|
const subject = { ...builtSubject, peerKey: grant.peerKey };
|
|
162
166
|
const decision = authorizeOperation({
|
|
@@ -180,6 +184,32 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
|
|
|
180
184
|
}
|
|
181
185
|
return { ok: true, grant };
|
|
182
186
|
}
|
|
187
|
+
/** Build the only AuthGateway input shape accepted for a task-held fullaccess claim. */
|
|
188
|
+
export function trustedExecutionIdentityInput(grant) {
|
|
189
|
+
return grant.executionIdentity && isValidFullAccessExecutionIdentity(grant.executionIdentity)
|
|
190
|
+
? {
|
|
191
|
+
trustedProcessRole: 'fullaccess-run',
|
|
192
|
+
trustedFullAccessAuthorization: grant.executionIdentity,
|
|
193
|
+
}
|
|
194
|
+
: {};
|
|
195
|
+
}
|
|
196
|
+
export function hasTrustedFullAccessDelegation(grant) {
|
|
197
|
+
return !!grant.executionIdentity && isValidFullAccessExecutionIdentity(grant.executionIdentity);
|
|
198
|
+
}
|
|
199
|
+
function isValidFullAccessExecutionIdentity(identity) {
|
|
200
|
+
if (identity.permissionMode !== 'fullaccess'
|
|
201
|
+
|| identity.processRole !== 'fullaccess-run'
|
|
202
|
+
|| identity.dataScope !== 'daemon')
|
|
203
|
+
return false;
|
|
204
|
+
if (identity.source === 'fullaccess-command') {
|
|
205
|
+
return typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0;
|
|
206
|
+
}
|
|
207
|
+
return identity.source === 'trigger'
|
|
208
|
+
&& typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
|
|
209
|
+
&& typeof identity.triggerId === 'string' && identity.triggerId.length > 0
|
|
210
|
+
&& typeof identity.runId === 'string' && identity.runId.length > 0
|
|
211
|
+
&& typeof identity.attemptId === 'string' && identity.attemptId.length > 0;
|
|
212
|
+
}
|
|
183
213
|
function hashDelegationToken(token) {
|
|
184
214
|
return crypto.createHash('sha256').update(token).digest('hex');
|
|
185
215
|
}
|
|
@@ -8,6 +8,11 @@ import { resolveRuntimePermissionMode } from '../role/runtime-policy.js';
|
|
|
8
8
|
export function isCrossAgentTriggerOperationAllowed(input) {
|
|
9
9
|
return input.control || input.daemonOwner === true || input.taskAgentAid === input.targetAgentAid;
|
|
10
10
|
}
|
|
11
|
+
/** A delegated identity may authorize ordinary work, but it is not a human approval for persistent host execution. */
|
|
12
|
+
export function canApprovePersistentFullAccessTrigger(input) {
|
|
13
|
+
return input.daemonOwner
|
|
14
|
+
&& (input.source === 'direct-user' || input.source === 'management-channel');
|
|
15
|
+
}
|
|
11
16
|
export function buildAuthSubject(input) {
|
|
12
17
|
const chatType = input.chatType === 'group' ? 'group' : 'private';
|
|
13
18
|
const actorId = input.actorId;
|
|
@@ -22,17 +27,31 @@ export function buildAuthSubject(input) {
|
|
|
22
27
|
peerType: input.peerType,
|
|
23
28
|
});
|
|
24
29
|
const processOwners = input.processOwners ?? [];
|
|
30
|
+
const fullAccessAuthorization = input.trustedFullAccessAuthorization;
|
|
31
|
+
const isFullAccessRun = input.trustedProcessRole === 'fullaccess-run'
|
|
32
|
+
&& fullAccessAuthorization?.permissionMode === 'fullaccess'
|
|
33
|
+
&& fullAccessAuthorization.processRole === 'fullaccess-run'
|
|
34
|
+
&& fullAccessAuthorization.dataScope === 'daemon';
|
|
25
35
|
const isDaemonService = input.trustedProcessRole === 'daemon-service';
|
|
26
|
-
const isDaemonOwner = !isDaemonService && isProcessOwner({
|
|
36
|
+
const isDaemonOwner = !isDaemonService && !isFullAccessRun && isProcessOwner({
|
|
27
37
|
actor: roleDetail.actor,
|
|
28
38
|
processOwners,
|
|
29
39
|
});
|
|
30
40
|
const suppliedRole = input.identity?.role && input.identity.role !== 'none' ? input.identity.role : undefined;
|
|
31
41
|
const role = suppliedRole ?? roleDetail.effectiveRole ?? 'none';
|
|
32
|
-
const processRole =
|
|
42
|
+
const processRole = isFullAccessRun
|
|
43
|
+
? 'fullaccess-run'
|
|
44
|
+
: isDaemonService ? 'daemon-service' : isDaemonOwner ? 'daemon-owner' : 'none';
|
|
45
|
+
const dataScope = processRole === 'fullaccess-run'
|
|
46
|
+
|| processRole === 'daemon-service'
|
|
47
|
+
|| processRole === 'daemon-owner'
|
|
48
|
+
? 'daemon'
|
|
49
|
+
: role === 'owner' || role === 'admin' ? 'agent' : 'relation';
|
|
33
50
|
return {
|
|
34
51
|
selfAid: input.selfAid,
|
|
52
|
+
agentName: input.agentName,
|
|
35
53
|
actorId,
|
|
54
|
+
principalId: roleDetail.actor.principalId,
|
|
36
55
|
requestId: input.requestId,
|
|
37
56
|
sessionId: input.sessionId,
|
|
38
57
|
channel: input.channel,
|
|
@@ -44,6 +63,8 @@ export function buildAuthSubject(input) {
|
|
|
44
63
|
role,
|
|
45
64
|
relationRole: role,
|
|
46
65
|
processRole,
|
|
66
|
+
dataScope,
|
|
67
|
+
authorizedBy: isFullAccessRun ? fullAccessAuthorization.authorizedBy : undefined,
|
|
47
68
|
roleSource: roleDetail.source,
|
|
48
69
|
identity: input.identity ?? roleToSessionIdentity(role === 'none' ? null : role),
|
|
49
70
|
isDaemonOwner,
|
|
@@ -51,8 +72,12 @@ export function buildAuthSubject(input) {
|
|
|
51
72
|
allowAccess: processRole !== 'none' || (suppliedRole
|
|
52
73
|
? checkRoleAccess(suppliedRole, input.selfAid)
|
|
53
74
|
: roleDetail.allowAccess && checkRoleAccess(role, input.selfAid)),
|
|
54
|
-
|
|
55
|
-
??
|
|
75
|
+
canApprovePersistentFullAccess: input.canApprovePersistentFullAccess
|
|
76
|
+
?? (isDaemonOwner && !isDaemonService),
|
|
77
|
+
permissionMode: isFullAccessRun
|
|
78
|
+
? 'fullaccess'
|
|
79
|
+
: input.permissionMode
|
|
80
|
+
?? resolveRuntimePermissionMode({ selfAid: input.selfAid, role }).effectiveValue,
|
|
56
81
|
};
|
|
57
82
|
}
|
|
58
83
|
function isProcessOwner(input) {
|
|
@@ -97,6 +122,7 @@ export function authorizeOperation(params) {
|
|
|
97
122
|
peerKey: params.subject.peerKey,
|
|
98
123
|
role: params.subject.role,
|
|
99
124
|
processRole: params.subject.processRole,
|
|
125
|
+
dataScope: params.subject.dataScope,
|
|
100
126
|
isDaemonOwner: params.subject.isDaemonOwner,
|
|
101
127
|
fromControlChannel: params.subject.fromControlChannel,
|
|
102
128
|
allowExplicitRelationTarget: params.allowExplicitRelationTarget,
|
|
@@ -162,6 +188,7 @@ function auditDecision(params, decision) {
|
|
|
162
188
|
requestId: params.auditMetadata?.requestId ?? params.subject.requestId,
|
|
163
189
|
sessionId: params.auditMetadata?.sessionId ?? params.subject.sessionId,
|
|
164
190
|
agentAid: params.auditMetadata?.agentAid ?? params.subject.selfAid,
|
|
191
|
+
agentName: params.auditMetadata?.agentName ?? params.subject.agentName ?? params.subject.selfAid,
|
|
165
192
|
permissionMode: params.auditMetadata?.permissionMode
|
|
166
193
|
?? (decision.allow ? decision.permissionMode ?? params.subject.permissionMode : params.subject.permissionMode),
|
|
167
194
|
toolName: params.auditMetadata?.toolName,
|
|
@@ -178,6 +205,8 @@ function auditDecision(params, decision) {
|
|
|
178
205
|
channelId: params.subject.channelId,
|
|
179
206
|
role: decision.allow ? decision.role : params.subject.role,
|
|
180
207
|
processRole: params.subject.processRole,
|
|
208
|
+
dataScope: params.subject.dataScope,
|
|
209
|
+
authorizedBy: params.subject.authorizedBy,
|
|
181
210
|
isDaemonOwner: params.subject.isDaemonOwner,
|
|
182
211
|
fromControlChannel: params.subject.fromControlChannel,
|
|
183
212
|
decision: decision.allow ? 'allow' : 'deny',
|
|
@@ -3,12 +3,14 @@ import { LogWriter } from '../../utils/log-writer.js';
|
|
|
3
3
|
import { resolvePaths } from '../../paths.js';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import { buildAuthorizationEventKey } from '../audit/event-key.js';
|
|
6
|
+
import { normalizeExecutionPermissionMode } from '../permission/mode.js';
|
|
6
7
|
export function auditCommandAuthorization(event) {
|
|
7
8
|
const shouldAudit = event.source === 'menu.cli' ||
|
|
8
9
|
event.decision === 'deny' ||
|
|
9
10
|
(event.decision === 'allow' && event.dangerous) ||
|
|
10
11
|
(event.source === 'agent-tool' && event.operation.startsWith('config.')) ||
|
|
11
12
|
event.operation === 'codex.approval' ||
|
|
13
|
+
event.operation.startsWith('fullaccess.') ||
|
|
12
14
|
event.operation.startsWith('role.') ||
|
|
13
15
|
event.operation === 'cli.exec.raw';
|
|
14
16
|
if (!shouldAudit)
|
|
@@ -16,6 +18,77 @@ export function auditCommandAuthorization(event) {
|
|
|
16
18
|
const auditRecord = buildAuditRecord(event);
|
|
17
19
|
logAuditEvent(auditRecord);
|
|
18
20
|
}
|
|
21
|
+
export function auditFullAccessEvent(input) {
|
|
22
|
+
const configurationEvent = input.event === 'fullaccess.trigger.configured'
|
|
23
|
+
|| input.event === 'fullaccess.trigger.deescalated';
|
|
24
|
+
const executionEvent = input.event === 'fullaccess.execution.started'
|
|
25
|
+
|| input.event === 'fullaccess.execution.ended'
|
|
26
|
+
|| input.event === 'fullaccess.trigger.execution.started'
|
|
27
|
+
|| input.event === 'fullaccess.trigger.execution.ended';
|
|
28
|
+
// The scheduler triggers a run, but the executing subject is the task-bound
|
|
29
|
+
// fullaccess identity. Keep the original owner/configuration provenance in
|
|
30
|
+
// a separate field rather than reporting the model task as a human owner.
|
|
31
|
+
const processRole = input.processRole
|
|
32
|
+
?? (executionEvent ? 'fullaccess-run' : input.source === 'trigger' ? 'daemon-service' : 'none');
|
|
33
|
+
const actorRole = processRole === 'daemon-owner' ? 'daemon-owner' : processRole;
|
|
34
|
+
const runnerProfileActive = executionEvent && input.executionState !== 'blocked';
|
|
35
|
+
const completedEvent = input.event.endsWith('.ended') || configurationEvent;
|
|
36
|
+
auditCommandAuthorization({
|
|
37
|
+
ts: Date.now(),
|
|
38
|
+
callId: input.attemptId ?? input.runId ?? input.taskId ?? input.messageId,
|
|
39
|
+
correlationId: input.runId ?? input.taskId,
|
|
40
|
+
source: input.source === 'trigger' ? 'agent-tool' : 'slash',
|
|
41
|
+
operation: input.event,
|
|
42
|
+
// CommandScope has no separate daemon value; process is the closest
|
|
43
|
+
// existing scope for daemon-owner authorization and keeps audit records
|
|
44
|
+
// compatible with the established command-audit schema.
|
|
45
|
+
scope: 'process',
|
|
46
|
+
dangerous: true,
|
|
47
|
+
decision: input.decision ?? 'allow',
|
|
48
|
+
executed: input.executed ?? completedEvent,
|
|
49
|
+
executionState: input.executionState
|
|
50
|
+
?? (input.decision === 'deny' ? 'blocked' : completedEvent ? 'completed' : 'authorized'),
|
|
51
|
+
decisionSource: input.decision === 'deny'
|
|
52
|
+
? 'policy'
|
|
53
|
+
: configurationEvent
|
|
54
|
+
? processRole === 'daemon-owner' ? 'daemon-owner' : 'policy'
|
|
55
|
+
: processRole === 'fullaccess-run'
|
|
56
|
+
? 'fullaccess-run'
|
|
57
|
+
: input.source === 'trigger'
|
|
58
|
+
? 'trigger-definition'
|
|
59
|
+
: 'daemon-owner',
|
|
60
|
+
actorId: input.actorId,
|
|
61
|
+
processRole,
|
|
62
|
+
dataScope: input.dataScope ?? (executionEvent ? 'daemon' : undefined),
|
|
63
|
+
authorizedBy: input.authorizedBy,
|
|
64
|
+
isDaemonOwner: processRole === 'daemon-owner',
|
|
65
|
+
agentAid: input.agentAid,
|
|
66
|
+
agentName: input.agentName,
|
|
67
|
+
sessionId: input.sessionId,
|
|
68
|
+
taskId: input.taskId,
|
|
69
|
+
messageId: input.messageId,
|
|
70
|
+
permissionMode: 'fullaccess',
|
|
71
|
+
role: actorRole,
|
|
72
|
+
reason: input.reason ?? input.result ?? input.event,
|
|
73
|
+
durationMs: input.durationMs,
|
|
74
|
+
argsSummary: {
|
|
75
|
+
source: input.source,
|
|
76
|
+
triggerId: input.triggerId,
|
|
77
|
+
runId: input.runId,
|
|
78
|
+
attemptId: input.attemptId,
|
|
79
|
+
baseagent: input.baseagent,
|
|
80
|
+
model: input.model,
|
|
81
|
+
dataScope: input.dataScope ?? (executionEvent ? 'daemon' : undefined),
|
|
82
|
+
authorizedBy: input.authorizedBy,
|
|
83
|
+
...(runnerProfileActive ? {
|
|
84
|
+
sandbox: 'off',
|
|
85
|
+
approval: 'bypassed',
|
|
86
|
+
applicationPolicy: 'bypassed',
|
|
87
|
+
} : {}),
|
|
88
|
+
result: input.result,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
19
92
|
/**
|
|
20
93
|
* Record a tool-level preflight denial (e.g. protected-path block) in the
|
|
21
94
|
* structured command-audit log. This covers interactive sessions whose
|
|
@@ -37,11 +110,15 @@ export function auditToolPreflightDenial(input) {
|
|
|
37
110
|
toolName: input.toolName,
|
|
38
111
|
policyCode: input.policyCode,
|
|
39
112
|
protectionClass: input.protectionClass ?? protectionClassForPolicy(input.policyCode),
|
|
40
|
-
|
|
113
|
+
// Path provenance must come from the policy parser. Do not infer it from
|
|
114
|
+
// localized summaries: words such as `daemon.json` or `.lock` may only be
|
|
115
|
+
// regex/string literals in an otherwise harmless diagnostic command.
|
|
116
|
+
matchedPath: input.matchedPath,
|
|
41
117
|
reason: input.reason,
|
|
42
118
|
argsSummary: input.summary ? { summary: input.summary } : undefined,
|
|
43
119
|
sessionId: input.sessionId ?? 'unknown',
|
|
44
120
|
agentAid: input.agentAid ?? 'unknown',
|
|
121
|
+
agentName: input.agentName,
|
|
45
122
|
permissionMode: input.permissionMode ?? 'unknown',
|
|
46
123
|
channel: input.channel,
|
|
47
124
|
actorId: input.actorId,
|
|
@@ -73,6 +150,7 @@ export function auditCodexApprovalDecision(input) {
|
|
|
73
150
|
correlationId: input.correlationId ?? input.requestId,
|
|
74
151
|
sessionId: input.sessionId ?? 'unknown',
|
|
75
152
|
agentAid: input.agentAid ?? 'unknown',
|
|
153
|
+
agentName: input.agentName,
|
|
76
154
|
permissionMode: input.permissionMode ?? 'unknown',
|
|
77
155
|
toolName: input.toolName,
|
|
78
156
|
approvalMethod: input.method,
|
|
@@ -81,7 +159,40 @@ export function auditCodexApprovalDecision(input) {
|
|
|
81
159
|
reason: input.reason,
|
|
82
160
|
role: input.role ?? 'unknown',
|
|
83
161
|
taskId: input.taskId,
|
|
84
|
-
argsSummary: {
|
|
162
|
+
argsSummary: {
|
|
163
|
+
method: input.method,
|
|
164
|
+
...(input.parseFailure ? { parseFailure: input.parseFailure } : {}),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Record a runtime permission decision outside a Codex app-server approval.
|
|
170
|
+
* The shared command-audit stream distinguishes policy, human, and
|
|
171
|
+
* infrastructure denials without parsing localized log text.
|
|
172
|
+
*/
|
|
173
|
+
export function auditPermissionDecision(input) {
|
|
174
|
+
auditCommandAuthorization({
|
|
175
|
+
ts: Date.now(),
|
|
176
|
+
callId: input.requestId,
|
|
177
|
+
correlationId: input.requestId,
|
|
178
|
+
requestId: input.requestId,
|
|
179
|
+
sessionId: input.sessionId,
|
|
180
|
+
agentAid: input.agentAid,
|
|
181
|
+
agentName: input.agentName,
|
|
182
|
+
permissionMode: input.permissionMode,
|
|
183
|
+
toolName: input.toolName,
|
|
184
|
+
policyCode: input.policyCode,
|
|
185
|
+
decisionSource: input.decisionSource,
|
|
186
|
+
source: 'agent-tool',
|
|
187
|
+
operation: 'permission.runtime',
|
|
188
|
+
scope: 'filesystem',
|
|
189
|
+
dangerous: false,
|
|
190
|
+
role: input.role ?? 'unknown',
|
|
191
|
+
taskId: input.taskId,
|
|
192
|
+
decision: 'deny',
|
|
193
|
+
executed: false,
|
|
194
|
+
executionState: 'blocked',
|
|
195
|
+
reason: input.reason,
|
|
85
196
|
});
|
|
86
197
|
}
|
|
87
198
|
/** Record a fail-closed infrastructure condition before a tool can execute. */
|
|
@@ -101,6 +212,7 @@ export function auditToolInfrastructureFailure(input) {
|
|
|
101
212
|
requestId: input.callId,
|
|
102
213
|
sessionId: input.sessionId ?? 'unknown',
|
|
103
214
|
agentAid: input.agentAid ?? 'unknown',
|
|
215
|
+
agentName: input.agentName,
|
|
104
216
|
permissionMode: input.permissionMode ?? 'unknown',
|
|
105
217
|
toolName: input.toolName,
|
|
106
218
|
policyCode: input.policyCode,
|
|
@@ -137,7 +249,8 @@ function buildAuditRecord(event) {
|
|
|
137
249
|
requestId: event.requestId,
|
|
138
250
|
sessionId: event.sessionId,
|
|
139
251
|
agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
|
|
140
|
-
|
|
252
|
+
agentName: resolveAgentName(event),
|
|
253
|
+
permissionMode: normalizeAuditPermissionMode(event.permissionMode),
|
|
141
254
|
toolName: event.toolName,
|
|
142
255
|
approvalMethod: event.approvalMethod,
|
|
143
256
|
policyCode: event.policyCode ?? event.code,
|
|
@@ -158,6 +271,8 @@ function buildAuditRecord(event) {
|
|
|
158
271
|
channelId: redactIdentifier(event.channelId),
|
|
159
272
|
role: event.role,
|
|
160
273
|
processRole: event.processRole,
|
|
274
|
+
dataScope: event.dataScope,
|
|
275
|
+
authorizedBy: redactIdentifier(event.authorizedBy),
|
|
161
276
|
isDaemonOwner: event.isDaemonOwner,
|
|
162
277
|
fromControlChannel: event.fromControlChannel,
|
|
163
278
|
taskId: event.taskId,
|
|
@@ -174,6 +289,26 @@ function buildAuditRecord(event) {
|
|
|
174
289
|
exitCode: event.exitCode,
|
|
175
290
|
};
|
|
176
291
|
}
|
|
292
|
+
function resolveAgentName(event) {
|
|
293
|
+
const displayName = typeof event.agentName === 'string' ? event.agentName.trim() : '';
|
|
294
|
+
if (displayName && !isUnknownAgentLabel(displayName))
|
|
295
|
+
return displayName;
|
|
296
|
+
const aid = typeof event.agentAid === 'string' ? event.agentAid.trim() : '';
|
|
297
|
+
if (aid && aid !== 'unknown')
|
|
298
|
+
return aid;
|
|
299
|
+
const selfAid = typeof event.selfAid === 'string' ? event.selfAid.trim() : '';
|
|
300
|
+
if (selfAid && selfAid !== 'unknown')
|
|
301
|
+
return selfAid;
|
|
302
|
+
return aid === 'unknown' || selfAid === 'unknown' ? 'unknown' : undefined;
|
|
303
|
+
}
|
|
304
|
+
function isUnknownAgentLabel(value) {
|
|
305
|
+
return value === 'unknown' || value === '<unknown>' || value === '<none>' || value === 'undefined' || value === 'null';
|
|
306
|
+
}
|
|
307
|
+
function normalizeAuditPermissionMode(value) {
|
|
308
|
+
if (!value || value === 'unknown')
|
|
309
|
+
return value;
|
|
310
|
+
return normalizeExecutionPermissionMode(value);
|
|
311
|
+
}
|
|
177
312
|
function redactIdentifier(value) {
|
|
178
313
|
if (!value)
|
|
179
314
|
return undefined;
|
|
@@ -209,6 +344,7 @@ function logAuditEvent(record) {
|
|
|
209
344
|
record.correlationId ? `correlation=${record.correlationId}` : null,
|
|
210
345
|
record.eventKey ? `eventKey=${record.eventKey}` : null,
|
|
211
346
|
record.sessionId ? `session=${record.sessionId}` : null,
|
|
347
|
+
record.agentName ? `agentName=${JSON.stringify(record.agentName)}` : null,
|
|
212
348
|
record.agentAid ? `agent=${record.agentAid}` : null,
|
|
213
349
|
record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
|
|
214
350
|
record.toolName ? `tool=${record.toolName}` : null,
|
|
@@ -293,6 +429,21 @@ function summarizeAuthorizationArgs(operation, args) {
|
|
|
293
429
|
for (const [key, value] of Object.entries(args)) {
|
|
294
430
|
if (value === undefined)
|
|
295
431
|
continue;
|
|
432
|
+
if (key === 'parseFailure' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
433
|
+
const diagnostic = value;
|
|
434
|
+
const issue = typeof diagnostic.issue === 'string' ? diagnostic.issue : undefined;
|
|
435
|
+
const offset = typeof diagnostic.offset === 'number' && Number.isFinite(diagnostic.offset)
|
|
436
|
+
? Math.max(0, Math.floor(diagnostic.offset)) : undefined;
|
|
437
|
+
const tokenIndex = typeof diagnostic.tokenIndex === 'number' && Number.isFinite(diagnostic.tokenIndex)
|
|
438
|
+
? Math.max(0, Math.floor(diagnostic.tokenIndex)) : undefined;
|
|
439
|
+
const dialect = typeof diagnostic.dialect === 'string' ? diagnostic.dialect : undefined;
|
|
440
|
+
const inputLength = typeof diagnostic.inputLength === 'number' && Number.isFinite(diagnostic.inputLength)
|
|
441
|
+
? Math.max(0, Math.floor(diagnostic.inputLength)) : undefined;
|
|
442
|
+
if (issue && offset !== undefined && tokenIndex !== undefined && dialect && inputLength !== undefined) {
|
|
443
|
+
summary.parseFailure = { issue, offset, tokenIndex, dialect, inputLength };
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
296
447
|
if (/^(?:self|peer|peerKey|target|targetId|session|sessionId|channelId)$/i.test(key)) {
|
|
297
448
|
summary[key] = typeof value === 'string' ? redactIdentifier(value) : '[redacted]';
|
|
298
449
|
continue;
|
|
@@ -341,7 +492,7 @@ function protectionClassForPolicy(policyCode) {
|
|
|
341
492
|
return 'L';
|
|
342
493
|
return undefined;
|
|
343
494
|
}
|
|
344
|
-
function
|
|
495
|
+
function extractAuditPath_UNUSED(summary) {
|
|
345
496
|
if (!summary)
|
|
346
497
|
return undefined;
|
|
347
498
|
const match = summary.match(/(?:^|\s)(\/[^\s'"`;]+|[A-Za-z]:[\\/][^\s'"`;]+|(?:src|ecagent|ecweb|scripts|tests?)\/[^\s'"`;]+)/);
|
|
@@ -95,6 +95,20 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
95
95
|
&& (resolvedConfigCommand.operationId !== operation || resolvedConfigCommand.commandScope !== intent.scope)) {
|
|
96
96
|
return denyDecision(ctx, 'NOT_ALLOWED', 'Resolved config operation does not match the command intent', operation, intent.scope, opMeta.dangerous);
|
|
97
97
|
}
|
|
98
|
+
// A fullaccess task is deliberately a daemon-scoped runtime identity. It
|
|
99
|
+
// still has to name a known operation and pass structural command parsing,
|
|
100
|
+
// but relation/Agent role rules, source lists, and daemon-owner gates are
|
|
101
|
+
// not meaningful limits for code already approved for host-level execution.
|
|
102
|
+
if (ctx.processRole === 'fullaccess-run' && ctx.dataScope === 'daemon') {
|
|
103
|
+
return {
|
|
104
|
+
allow: true,
|
|
105
|
+
operation,
|
|
106
|
+
scope: intent.scope,
|
|
107
|
+
role: 'fullaccess-run',
|
|
108
|
+
dangerous: opMeta.dangerous,
|
|
109
|
+
matchedRule: 'fullaccess-run-daemon-scope',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
98
112
|
if (ctx.chatType === 'group'
|
|
99
113
|
&& (resolvedConfigCommand ? isResolvedConfigMutation(resolvedConfigCommand) : isConfigMutationOperation(operation))
|
|
100
114
|
&& intent.scope !== 'relation') {
|
|
@@ -175,7 +189,31 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
175
189
|
if (roleDef.allowAccess === false) {
|
|
176
190
|
return denyDecision(ctx, 'ROLE_ACCESS_DENIED', `Role ${role} is not allowed to access commands`, operation, intent.scope, opMeta.dangerous);
|
|
177
191
|
}
|
|
178
|
-
|
|
192
|
+
let matchResult = matchCommandPermission(operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
|
|
193
|
+
// Agent self-service contact add deliberately reuses the same low-trust
|
|
194
|
+
// relation boundary as ec.msg.send. Older per-Agent role files may still
|
|
195
|
+
// contain the historical contact.* deny without an explicit contact.add
|
|
196
|
+
// entry; preserve their safe current-peer/private-only send capability while
|
|
197
|
+
// allowing this new request workflow. An explicit contact.add rule wins.
|
|
198
|
+
if (operation === 'contact.add'
|
|
199
|
+
&& !Object.prototype.hasOwnProperty.call(roleDef.commandPermissions || {}, 'contact.add')) {
|
|
200
|
+
const sendPermission = roleDef.commandPermissions?.['ec.msg.send'];
|
|
201
|
+
if (sendPermission?.allow) {
|
|
202
|
+
matchResult = {
|
|
203
|
+
permission: {
|
|
204
|
+
...sendPermission,
|
|
205
|
+
scopes: ['relation'],
|
|
206
|
+
constraints: {
|
|
207
|
+
...(sendPermission.constraints ?? {}),
|
|
208
|
+
ownPeerOnly: true,
|
|
209
|
+
privateOnly: true,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
matchedRule: 'ec.msg.send',
|
|
213
|
+
rank: 1000,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
179
217
|
if (!matchResult) {
|
|
180
218
|
return denyDecision(ctx, 'NO_PERMISSION', `Role ${role} has no permission for ${operation}`, operation, intent.scope, opMeta.dangerous);
|
|
181
219
|
}
|
|
@@ -274,6 +312,8 @@ function requiresDaemonOwner(ctx, resolvedConfigCommand) {
|
|
|
274
312
|
const targetAgentAid = stringArg(args.targetAgentAid) ?? stringArg(args.self);
|
|
275
313
|
return !!taskAgentAid && !!targetAgentAid && taskAgentAid !== targetAgentAid;
|
|
276
314
|
}
|
|
315
|
+
if (operation.startsWith('role.') && ctx.fromControlChannel)
|
|
316
|
+
return true;
|
|
277
317
|
return false;
|
|
278
318
|
}
|
|
279
319
|
function hasDaemonOwnerRole(ctx) {
|
|
@@ -55,7 +55,7 @@ const GROUP_MANAGEMENT_OPERATIONS = [
|
|
|
55
55
|
sources: ['agent-tool'],
|
|
56
56
|
}));
|
|
57
57
|
const MSG_READ_OPERATIONS = [
|
|
58
|
-
['ec.msg.history', 'Read message history for
|
|
58
|
+
['ec.msg.history', 'Read message history for a controlled private relation'],
|
|
59
59
|
['ec.msg.online', 'Read online status for an AUN private relation'],
|
|
60
60
|
].map(([id, description]) => ({
|
|
61
61
|
id,
|
|
@@ -476,6 +476,14 @@ const OPERATIONS = [
|
|
|
476
476
|
description: 'Read the Agent contact block list',
|
|
477
477
|
sources: ['agent-tool'],
|
|
478
478
|
},
|
|
479
|
+
{
|
|
480
|
+
id: 'contact.add',
|
|
481
|
+
category: 'write-agent',
|
|
482
|
+
dangerous: false,
|
|
483
|
+
defaultScopes: ['relation'],
|
|
484
|
+
description: 'Request adding the current private relation as an Agent contact',
|
|
485
|
+
sources: ['agent-tool'],
|
|
486
|
+
},
|
|
479
487
|
{
|
|
480
488
|
id: 'contact.block',
|
|
481
489
|
category: 'write-agent',
|
|
@@ -70,11 +70,11 @@ export function bindPostBootstrapWelcomeOutboxSession(aid, sessionId) {
|
|
|
70
70
|
return false;
|
|
71
71
|
if (entry.context?.sessionId === sessionId)
|
|
72
72
|
return true;
|
|
73
|
-
return outbox.
|
|
73
|
+
return outbox.replaceIfRouteMatches(aid, entry, {
|
|
74
74
|
...entry,
|
|
75
75
|
context: {
|
|
76
76
|
...(entry.context ?? {}),
|
|
77
77
|
sessionId,
|
|
78
78
|
},
|
|
79
|
-
});
|
|
79
|
+
}) === 'replaced';
|
|
80
80
|
}
|
|
@@ -2,10 +2,10 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
4
|
import { kitsTemplatesDir, agentMdPath } from '../paths.js';
|
|
5
|
-
import { resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
5
|
+
import { buildInitialAgentMd, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
7
|
import { loadAgent, saveAgent } from '../config-store.js';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
|
|
9
9
|
import { renderTemplate } from '../eck/manifest-engine.js';
|
|
10
10
|
import { activeBaseagent } from './model/config-scope.js';
|
|
11
11
|
import { buildEnvelope } from './message/message-utils.js';
|
|
@@ -123,12 +123,18 @@ export class BootstrapService {
|
|
|
123
123
|
this.inFlight.delete(key);
|
|
124
124
|
return false;
|
|
125
125
|
}
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
126
|
+
const lifecycle = resolveAgentLifecycle(loadedConfig);
|
|
127
|
+
if (lifecycle === 'active') {
|
|
128
128
|
this.inFlight.delete(key);
|
|
129
129
|
return false;
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
if (lifecycle !== 'created' && lifecycle !== 'bootstrapping') {
|
|
132
|
+
this.inFlight.delete(key);
|
|
133
|
+
logger.warn(`[Bootstrap] Refusing invalid lifecycle for ${aid}: ${String(loadedConfig.lifecycle)}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const config = { ...loadedConfig, lifecycle };
|
|
137
|
+
const starting = lifecycle === 'created';
|
|
132
138
|
const channelType = ctx.channelType || this.channelTypeFromKey(ctx.channelKey);
|
|
133
139
|
const configuredRecipient = this.resolveConfiguredRecipient(config, ctx.channelKey, channelType);
|
|
134
140
|
const recipientId = ctx.recipientId || configuredRecipient;
|
|
@@ -140,35 +146,14 @@ export class BootstrapService {
|
|
|
140
146
|
this.inFlight.delete(key);
|
|
141
147
|
return false;
|
|
142
148
|
}
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
isGroupRecipient = channelType === 'aun'
|
|
153
|
-
? (await ctx.adapter.isGroup?.(recipientId)) === true
|
|
154
|
-
: false;
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
// Group/private routing is security-sensitive. If the authoritative
|
|
158
|
-
// probe fails, do not guess a route, and release the single-flight key
|
|
159
|
-
// so a later connection or retry can recover.
|
|
160
|
-
this.inFlight.delete(key);
|
|
161
|
-
logger.warn(`[Bootstrap] Failed to resolve recipient type for ${aid}: ${error instanceof Error ? error.message : String(error)}`);
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
const delivery = isGroupRecipient === true
|
|
165
|
-
? { chatType: 'group', groupId: recipientId }
|
|
166
|
-
: { chatType: 'private' };
|
|
167
|
-
const channelId = delivery.chatType === 'group'
|
|
168
|
-
? delivery.groupId
|
|
169
|
-
: channelType === 'aun'
|
|
170
|
-
? recipientId
|
|
171
|
-
: ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
|
|
149
|
+
// Bootstrap is initiated by connection/owner binding, not by a reply to
|
|
150
|
+
// an existing group message. `owners[]` contains personal Owner AIDs, so
|
|
151
|
+
// the first prompt must always use the private message route. Inbound
|
|
152
|
+
// group context must never change this initial delivery target.
|
|
153
|
+
const delivery = { chatType: 'private' };
|
|
154
|
+
const channelId = channelType === 'aun'
|
|
155
|
+
? recipientId
|
|
156
|
+
: ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
|
|
172
157
|
if (!channelId) {
|
|
173
158
|
this.inFlight.delete(key);
|
|
174
159
|
return false;
|
|
@@ -234,14 +219,14 @@ export class BootstrapService {
|
|
|
234
219
|
const loadedConfig = loadAgent(aid) || agent?.config;
|
|
235
220
|
if (!loadedConfig)
|
|
236
221
|
return { ok: false, error: `Agent "${aid}" not found` };
|
|
237
|
-
const lifecycle =
|
|
222
|
+
const lifecycle = resolveAgentLifecycle(loadedConfig);
|
|
238
223
|
if (lifecycle === 'active')
|
|
239
224
|
return { ok: true, aid, transitioned: false };
|
|
240
225
|
if (lifecycle !== 'bootstrapping') {
|
|
241
226
|
return {
|
|
242
227
|
ok: false,
|
|
243
228
|
code: 'INVALID_LIFECYCLE',
|
|
244
|
-
error: `Agent "${aid}" is ${lifecycle}; only a bootstrapping agent can become ready`,
|
|
229
|
+
error: `Agent "${aid}" is ${lifecycle ?? `invalid (${String(loadedConfig.lifecycle)})`}; only a bootstrapping agent can become ready`,
|
|
245
230
|
};
|
|
246
231
|
}
|
|
247
232
|
this.setLifecycle(agent, aid, 'active');
|
|
@@ -253,7 +238,7 @@ export class BootstrapService {
|
|
|
253
238
|
const agent = this.agentRegistry.get(aid);
|
|
254
239
|
const loadedConfig = loadAgent(aid) || agent?.config;
|
|
255
240
|
return loadedConfig
|
|
256
|
-
?
|
|
241
|
+
? resolveAgentLifecycle(loadedConfig)
|
|
257
242
|
: null;
|
|
258
243
|
}
|
|
259
244
|
setLifecycle(agent, aid, lifecycle) {
|
|
@@ -307,7 +292,11 @@ export class BootstrapService {
|
|
|
307
292
|
if (typeof adapter.uploadAgentMd !== 'function')
|
|
308
293
|
return;
|
|
309
294
|
const existing = fs.existsSync(agentMdPath(aid)) ? fs.readFileSync(agentMdPath(aid), 'utf-8') : '';
|
|
310
|
-
const content = existing.trim() ||
|
|
295
|
+
const content = existing.trim() || buildInitialAgentMd({
|
|
296
|
+
aid,
|
|
297
|
+
name: fallbackName,
|
|
298
|
+
type: 'codeagent',
|
|
299
|
+
});
|
|
311
300
|
try {
|
|
312
301
|
await adapter.uploadAgentMd(content);
|
|
313
302
|
logger.info(`[Bootstrap] Published agent.md for ${aid}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { resolvePaths } from '../../paths.js';
|
|
4
|
-
import { atomicReadJson,
|
|
4
|
+
import { atomicReadJson, stableWriteJson } from '../../utils/atomic-write.js';
|
|
5
5
|
import { cloneCausation, normalizeCausation } from './context.js';
|
|
6
6
|
const DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
7
7
|
const associations = new Map();
|
|
@@ -44,9 +44,12 @@ function ensureLoaded() {
|
|
|
44
44
|
}
|
|
45
45
|
function persist() {
|
|
46
46
|
try {
|
|
47
|
-
// Keep the protected mount target
|
|
48
|
-
//
|
|
49
|
-
|
|
47
|
+
// Keep the protected mount target's inode stable. Bubblewrap binds this
|
|
48
|
+
// H-class file by pathname while constructing its namespace; an atomic
|
|
49
|
+
// rename can otherwise replace the inode during bind/remount and make
|
|
50
|
+
// sandbox initialization fail. Causation state is best-effort and small,
|
|
51
|
+
// so an in-place write is preferable to changing the protected inode.
|
|
52
|
+
stableWriteJson(associationFile(), [...associations].map(([messageId, association]) => ({
|
|
50
53
|
messageId,
|
|
51
54
|
...association,
|
|
52
55
|
})));
|