evolcore 0.0.26 → 0.0.27
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 +25 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +63 -26
- package/dist/agents/codex-runner.js +316 -411
- package/dist/agents/ecagent-runner.js +170 -25
- package/dist/agents/gemini-runner.js +6 -0
- package/dist/agents/runner-types.js +50 -2
- package/dist/aun/aid/agentmd.js +73 -31
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +180 -1
- package/dist/aun/aid/identity.js +3 -3
- package/dist/aun/group/history.js +303 -0
- package/dist/aun/group/identity.js +43 -0
- package/dist/aun/group/index.js +5 -0
- package/dist/aun/{msg/group.js → group/operations.js} +161 -38
- package/dist/aun/{menu-client.js → menu/client.js} +12 -7
- package/dist/aun/menu/index.js +2 -0
- package/dist/aun/menu/transport.js +49 -0
- package/dist/aun/msg/index.js +2 -1
- package/dist/aun/msg/managed-operation.js +84 -18
- package/dist/aun/msg/mention-schema.js +51 -0
- package/dist/aun/msg/p2p.js +13 -8
- package/dist/aun/msg/upload.js +43 -88
- package/dist/aun/outbox.js +29 -7
- package/dist/aun/rpc/client.js +1 -0
- package/dist/aun/storage/client.js +39 -50
- package/dist/channels/aun.js +522 -233
- package/dist/channels/daemon.js +10 -6
- package/dist/cli/agent-command.js +108 -6
- package/dist/cli/agent-create.js +515 -2
- package/dist/cli/agent.js +109 -8
- package/dist/cli/aun-commands.js +191 -42
- package/dist/cli/config.js +8 -5
- package/dist/cli/contact.js +2 -2
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +35 -23
- package/dist/cli/diag.js +2 -2
- package/dist/cli/fs-command.js +3 -3
- package/dist/cli/handoff-command.js +6 -6
- package/dist/cli/index.js +8 -2
- package/dist/cli/init.js +34 -1
- package/dist/cli/managed-command-guard.js +2 -1
- package/dist/cli/menu-client.js +9 -5
- package/dist/cli/model.js +3 -3
- package/dist/cli/queue-command.js +2 -2
- package/dist/cli/response.js +2 -2
- package/dist/cli/restart-monitor.js +16 -2
- package/dist/cli/task-command.js +59 -0
- package/dist/cli/task-context.js +97 -49
- package/dist/cli/trigger-command.js +20 -10
- package/dist/config/builtin-roles.js +10 -0
- package/dist/config/config-field-policy.js +11 -0
- package/dist/config/config-manager.js +32 -4
- package/dist/config/config-operation-service.js +20 -5
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/contact-book.js +1 -1
- package/dist/config/contact-operation-service.js +1 -1
- package/dist/config/peer-role-resolver.js +1 -1
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config-store.js +1 -1
- package/dist/core/agent-application-service.js +5 -0
- package/dist/core/audit/event-key.js +17 -2
- package/dist/core/audit/log-integrity.js +34 -8
- package/dist/core/auth/agent-delegation.js +48 -14
- package/dist/core/auth/auth-gateway.js +2 -0
- package/dist/core/auth/authorization-audit.js +34 -38
- package/dist/core/auth/operation-authorizer.js +67 -5
- package/dist/core/auth/operation-catalog.js +110 -0
- package/dist/core/auth/remote-access-store.js +115 -9
- package/dist/core/bootstrap-messages.js +66 -7
- package/dist/core/bootstrap-service.js +110 -19
- package/dist/core/command/agent-control.js +88 -7
- package/dist/core/command/agent-storage-menu.js +443 -0
- package/dist/core/command/cli-intent-parser.js +19 -0
- package/dist/core/command/command-handler.js +76 -39
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/group-menu.js +7 -2
- package/dist/core/command/menu-catalog.js +34 -4
- package/dist/core/command/menu-handler.js +348 -77
- package/dist/core/command/menu-protocol.js +40 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +37 -22
- package/dist/core/event-catalog.js +4 -0
- package/dist/core/evolagent-registry.js +28 -2
- package/dist/core/group/invite-service.js +924 -0
- package/dist/core/interaction-router.js +22 -8
- package/dist/core/message/im-renderer.js +76 -15
- package/dist/core/message/message-bridge.js +94 -11
- package/dist/core/message/message-log.js +5 -3
- package/dist/core/message/message-queue.js +73 -41
- package/dist/core/message/response-engine.js +709 -117
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +61 -29
- package/dist/core/permission/protected-paths.js +134 -16
- package/dist/core/permission/sandbox-runtime.js +72 -3
- package/dist/core/permission/tool-error-code.js +15 -0
- package/dist/core/permission/tool-policy.js +7 -7
- package/dist/core/relation/peer-identity.js +57 -6
- package/dist/core/relation/shared-data.js +106 -0
- package/dist/core/role/runtime-policy.js +64 -0
- package/dist/core/runtime-lock.js +10 -26
- package/dist/core/session/runner-task-history.js +170 -0
- package/dist/core/session/session-manager.js +34 -6
- package/dist/core/session/session-mapper.js +41 -0
- package/dist/core/session/session-turn-coordinator.js +37 -7
- package/dist/core/session/session-turns.js +48 -7
- package/dist/core/task/task-recovery-service.js +176 -0
- package/dist/core/task/task-resume-operation.js +32 -0
- package/dist/eck/group-rules-sync.js +5 -3
- package/dist/eck/kit-renderer.js +1 -0
- package/dist/index.js +308 -54
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +2 -0
- package/dist/trigger/event-source.js +19 -0
- package/dist/trigger/feedback.js +233 -40
- package/dist/trigger/parser.js +55 -5
- package/dist/trigger/patch.js +9 -0
- package/dist/trigger/scheduler.js +30 -4
- package/dist/trigger/validation.js +20 -3
- package/dist/utils/ecweb-utils.js +1 -0
- package/dist/utils/error-utils.js +40 -2
- package/dist/utils/logger.js +23 -13
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- package/ecagent/dist/harness/agent-harness.js +3 -0
- package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
- package/kits/docs/INDEX.md +0 -6
- package/kits/docs/channels/aun.md +15 -2
- package/kits/docs/channels/feishu.md +1 -1
- package/kits/docs/context-assembly.md +29 -17
- package/kits/docs/evolcore/INDEX.md +8 -3
- package/kits/docs/evolcore/agent-create.md +39 -7
- package/kits/docs/evolcore/agent.md +33 -1
- package/kits/docs/evolcore/aid.md +9 -1
- package/kits/docs/evolcore/config.md +40 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +117 -2
- package/kits/docs/evolcore/menu-client.md +16 -0
- package/kits/docs/evolcore/msg.md +3 -0
- package/kits/docs/evolcore/storage.md +3 -0
- package/kits/docs/evolcore/task.md +14 -0
- package/kits/docs/evolcore/trigger.md +51 -3
- package/kits/docs/venues/group.md +5 -0
- package/kits/eck_manifest.json +27 -0
- package/kits/rules/01-overview.md +11 -7
- package/kits/rules/04-relation.md +2 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +6 -4
- package/kits/schemas/agent-config.schema.12.json +25 -1
- package/kits/schemas/daemon.schema.6.json +2 -2
- package/kits/schemas/defaults.schema.5.json +1 -1
- package/kits/schemas/relation-config.schema.1.json +1 -1
- package/kits/schemas/relation-config.schema.5.json +1 -1
- package/kits/schemas/role-config.schema.1.json +14 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/baseagent.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +26 -9
- package/kits/templates/system-fragments/channel.md +8 -44
- package/kits/templates/system-fragments/commands.md +10 -2
- package/kits/templates/system-fragments/group-unread-history.md +7 -0
- package/kits/templates/system-fragments/reply.md +56 -0
- package/package.json +7 -3
- package/dist/aun/group-identity.js +0 -9
- /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
- /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { buildToolLifecycleEventKey, normalizeToolCorrelation } from './event-key.js';
|
|
4
5
|
// Logger rotation is hourly/daily, but the suffix is intentionally generic so
|
|
5
6
|
// this check also covers future rotation formats without changing the audit.
|
|
6
7
|
const STRUCTURED_LOG_RE = /^(?:daemon|events|channel-in|channel-out|messages|command-audit)(?:-[^.]+)?\.log$/;
|
|
@@ -33,6 +34,7 @@ export function inspectStructuredLogs(logDir, options = {}) {
|
|
|
33
34
|
// daemon logs mix human-readable entries with structured audit lines.
|
|
34
35
|
// Ignore the former, but still validate malformed structured payloads.
|
|
35
36
|
if (MIXED_TEXT_LOG_RE.test(file)) {
|
|
37
|
+
raw = raw.replace(/^\[[^\]]+\] \[(?:DEBUG|INFO|WARN|ERROR)\] /, '');
|
|
36
38
|
if (raw.startsWith('[CommandAuditRecord] '))
|
|
37
39
|
raw = raw.slice('[CommandAuditRecord] '.length);
|
|
38
40
|
else if (!raw.startsWith('{'))
|
|
@@ -57,8 +59,9 @@ export function inspectStructuredLogs(logDir, options = {}) {
|
|
|
57
59
|
issues.push({ file, line: index + 1, reason: 'missing_agent', key });
|
|
58
60
|
if (isLifecycleRecord(value) && !hasContextValue(lifecycleField(value, 'permissionMode')))
|
|
59
61
|
issues.push({ file, line: index + 1, reason: 'missing_permission_mode', key });
|
|
60
|
-
if (isCorrelatableRecord(value) && !key)
|
|
62
|
+
if (isCorrelatableRecord(value) && (!key || value.correlationMissing === true || key.startsWith('tool:unlinked:'))) {
|
|
61
63
|
issues.push({ file, line: index + 1, reason: 'missing_correlation' });
|
|
64
|
+
}
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
67
|
const seen = new Set();
|
|
@@ -79,16 +82,33 @@ export function inspectStructuredLogs(logDir, options = {}) {
|
|
|
79
82
|
function structuredRecordKey(value) {
|
|
80
83
|
const nested = nestedRecord(value);
|
|
81
84
|
const eventKey = firstDefined(value, nested, ['eventKey', 'event_key']);
|
|
82
|
-
const eventPhase =
|
|
85
|
+
const eventPhase = recordPhase(value);
|
|
83
86
|
if (eventKey)
|
|
84
87
|
return `${String(eventKey)}${eventPhase ? `:${String(eventPhase)}` : ''}`;
|
|
88
|
+
if (isLifecycleRecord(value)) {
|
|
89
|
+
const correlation = normalizeToolCorrelation({
|
|
90
|
+
callId: firstDefined(value, nested, ['callId', 'call_id', 'toolUseId', 'tool_use_id']),
|
|
91
|
+
correlationId: firstDefined(value, nested, ['correlationId', 'correlation_id']),
|
|
92
|
+
requestId: firstDefined(value, nested, ['requestId', 'request_id']),
|
|
93
|
+
});
|
|
94
|
+
if (!correlation.callId)
|
|
95
|
+
return undefined;
|
|
96
|
+
return `${buildToolLifecycleEventKey({
|
|
97
|
+
sessionId: lifecycleField(value, 'sessionId'),
|
|
98
|
+
...correlation,
|
|
99
|
+
})}:${eventPhase}`;
|
|
100
|
+
}
|
|
85
101
|
const correlation = firstDefined(value, nested, [
|
|
86
102
|
'correlationId', 'correlation_id', 'callId', 'call_id', 'toolUseId', 'tool_use_id',
|
|
87
103
|
'requestId', 'request_id', 'msgId', 'operationId',
|
|
88
104
|
]);
|
|
89
105
|
if (!correlation)
|
|
90
106
|
return undefined;
|
|
91
|
-
return
|
|
107
|
+
return JSON.stringify([lifecycleField(value, 'sessionId'), canonicalEventType(value), correlation, eventPhase]);
|
|
108
|
+
}
|
|
109
|
+
function recordPhase(value) {
|
|
110
|
+
return firstDefined(value, nestedRecord(value), ['eventPhase', 'event_phase'])
|
|
111
|
+
?? (canonicalEventType(value) === 'tool_use' ? 'use' : canonicalEventType(value) === 'tool_result' ? 'result' : undefined);
|
|
92
112
|
}
|
|
93
113
|
function isLifecycleRecord(value) {
|
|
94
114
|
const type = canonicalEventType(value);
|
|
@@ -101,11 +121,13 @@ function isCorrelatableRecord(value) {
|
|
|
101
121
|
}
|
|
102
122
|
function stableHash(value) {
|
|
103
123
|
const nested = nestedRecord(value);
|
|
104
|
-
const eventKey =
|
|
124
|
+
const eventKey = structuredRecordKey(value);
|
|
105
125
|
const canonical = {
|
|
106
126
|
eventKey,
|
|
107
|
-
eventPhase:
|
|
127
|
+
eventPhase: recordPhase(value),
|
|
108
128
|
type: canonicalEventType(value),
|
|
129
|
+
operation: firstDefined(value, nested, ['operation']),
|
|
130
|
+
targetAgentAid: firstDefined(value, nested, ['targetAgentAid']),
|
|
109
131
|
toolName: firstDefined(value, nested, ['toolName', 'tool', 'name']),
|
|
110
132
|
callId: eventKey ? undefined : firstDefined(value, nested, ['callId', 'call_id', 'toolUseId', 'tool_use_id']),
|
|
111
133
|
correlationId: eventKey ? undefined : firstDefined(value, nested, ['correlationId', 'correlation_id']),
|
|
@@ -116,11 +138,15 @@ function stableHash(value) {
|
|
|
116
138
|
isError: firstDefined(value, nested, ['isError', 'is_error']) ?? (value.ok === false || nested?.ok === false ? true : undefined),
|
|
117
139
|
errorCode: firstDefined(value, nested, ['errorCode', 'error_code']),
|
|
118
140
|
decision: firstDefined(value, nested, ['decision']),
|
|
141
|
+
decisionSource: firstDefined(value, nested, ['decisionSource', 'decision_source']),
|
|
142
|
+
policyCode: firstDefined(value, nested, ['policyCode', 'policy_code']),
|
|
119
143
|
executed: firstDefined(value, nested, ['executed']),
|
|
120
144
|
executionState: firstDefined(value, nested, ['executionState', 'execution_state']),
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
// A lifecycle stage may be mirrored without its large input/output body.
|
|
146
|
+
// Keep decision/classification differences, not projection payload size.
|
|
147
|
+
input: eventKey && isLifecycleRecord(value) ? undefined : firstDefined(value, nested, ['input']),
|
|
148
|
+
result: eventKey && isLifecycleRecord(value) ? undefined : firstDefined(value, nested, ['result', 'content']),
|
|
149
|
+
error: eventKey && isLifecycleRecord(value) ? undefined : firstDefined(value, nested, ['error', 'errorMessage']),
|
|
124
150
|
};
|
|
125
151
|
return crypto.createHash('sha256').update(JSON.stringify(canonical)).digest('hex').slice(0, 16);
|
|
126
152
|
}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
+
import { assertAgentPolicyAuthorization } from '../role/runtime-policy.js';
|
|
1
2
|
import crypto from 'crypto';
|
|
2
3
|
import { authorizeOperation, buildAuthSubject } from './auth-gateway.js';
|
|
3
4
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
4
5
|
import { logger } from '../../utils/logger.js';
|
|
5
6
|
export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
|
|
7
|
+
export const CODEX_SESSION_TOKEN_ENV = 'EVOLCORE_CODEX_SESSION_TOKEN';
|
|
6
8
|
export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the credential carried by an EC CLI process. A live task token has
|
|
11
|
+
* priority; persistent Codex threads fall back to their session credential.
|
|
12
|
+
*/
|
|
13
|
+
export function readDelegationCredential(env = process.env) {
|
|
14
|
+
const taskToken = env[AGENT_DELEGATION_TOKEN_ENV]?.trim();
|
|
15
|
+
if (taskToken)
|
|
16
|
+
return taskToken;
|
|
17
|
+
const sessionToken = env[CODEX_SESSION_TOKEN_ENV]?.trim();
|
|
18
|
+
return sessionToken || undefined;
|
|
19
|
+
}
|
|
7
20
|
export class AgentDelegationRegistry {
|
|
8
21
|
runtimeEpoch = crypto.randomBytes(16).toString('hex');
|
|
9
22
|
grantsByHash = new Map();
|
|
@@ -24,6 +37,13 @@ export class AgentDelegationRegistry {
|
|
|
24
37
|
return token;
|
|
25
38
|
}
|
|
26
39
|
validate(token, sessionId, commandHash) {
|
|
40
|
+
return this.validateToken(token, sessionId, commandHash, true);
|
|
41
|
+
}
|
|
42
|
+
/** Authenticate a CLI context lookup without spending its approved operation. */
|
|
43
|
+
validateForContext(token, sessionId, commandHash) {
|
|
44
|
+
return this.validateToken(token, sessionId, commandHash, false);
|
|
45
|
+
}
|
|
46
|
+
validateToken(token, sessionId, commandHash, consumeApproval) {
|
|
27
47
|
if (!token) {
|
|
28
48
|
return {
|
|
29
49
|
ok: false,
|
|
@@ -37,10 +57,19 @@ export class AgentDelegationRegistry {
|
|
|
37
57
|
if (grant
|
|
38
58
|
&& grant.sessionId === sessionId
|
|
39
59
|
&& this.activeHashBySession.get(sessionId) === tokenHash) {
|
|
60
|
+
if (grant.executionIdentity?.source === 'agent-policy') {
|
|
61
|
+
try {
|
|
62
|
+
assertAgentPolicyAuthorization(grant.executionIdentity);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
this.revokeSession(sessionId);
|
|
66
|
+
return { ok: false, code: 'INVALID_DELEGATION', reason: 'Agent fullaccess policy was revoked', diagnosticCause: 'task_replaced' };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
40
69
|
return { ok: true, grant };
|
|
41
70
|
}
|
|
42
71
|
const approved = commandHash
|
|
43
|
-
? this.
|
|
72
|
+
? this.readApprovedCommand(tokenHash, sessionId, commandHash, consumeApproval)
|
|
44
73
|
: undefined;
|
|
45
74
|
if (approved)
|
|
46
75
|
return { ok: true, grant: approved };
|
|
@@ -66,11 +95,12 @@ export class AgentDelegationRegistry {
|
|
|
66
95
|
const grant = activeTokenHash ? this.grantsByHash.get(activeTokenHash) : undefined;
|
|
67
96
|
if (!grant || grant.taskId !== input.taskId)
|
|
68
97
|
return false;
|
|
69
|
-
|
|
98
|
+
const sessionToken = input.sessionToken ?? input.carrierToken;
|
|
99
|
+
if (!sessionToken || !isDelegationCommandHash(input.commandHash))
|
|
70
100
|
return false;
|
|
71
101
|
this.deleteExpiredApprovedCommands();
|
|
72
|
-
const
|
|
73
|
-
const key = approvedCommandKey(
|
|
102
|
+
const sessionTokenHash = hashDelegationToken(sessionToken);
|
|
103
|
+
const key = approvedCommandKey(sessionTokenHash, input.sessionId, input.commandHash);
|
|
74
104
|
const existing = this.approvedCommands.get(key);
|
|
75
105
|
const ttlMs = Math.max(1, Math.min(input.ttlMs ?? AGENT_DELEGATION_COMMAND_TTL_MS, AGENT_DELEGATION_COMMAND_TTL_MS));
|
|
76
106
|
this.approvedCommands.set(key, {
|
|
@@ -105,10 +135,10 @@ export class AgentDelegationRegistry {
|
|
|
105
135
|
this.activeHashBySession.delete(sessionId);
|
|
106
136
|
this.deleteApprovedCommandsForSession(sessionId);
|
|
107
137
|
}
|
|
108
|
-
|
|
138
|
+
readApprovedCommand(sessionTokenHash, sessionId, commandHash, consume) {
|
|
109
139
|
if (!isDelegationCommandHash(commandHash))
|
|
110
140
|
return undefined;
|
|
111
|
-
const key = approvedCommandKey(
|
|
141
|
+
const key = approvedCommandKey(sessionTokenHash, sessionId, commandHash);
|
|
112
142
|
const approved = this.approvedCommands.get(key);
|
|
113
143
|
if (!approved)
|
|
114
144
|
return undefined;
|
|
@@ -124,10 +154,12 @@ export class AgentDelegationRegistry {
|
|
|
124
154
|
this.approvedCommands.delete(key);
|
|
125
155
|
return undefined;
|
|
126
156
|
}
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
157
|
+
if (consume) {
|
|
158
|
+
if (approved.remainingUses <= 1)
|
|
159
|
+
this.approvedCommands.delete(key);
|
|
160
|
+
else
|
|
161
|
+
approved.remainingUses--;
|
|
162
|
+
}
|
|
131
163
|
return grant;
|
|
132
164
|
}
|
|
133
165
|
deleteExpiredApprovedCommands() {
|
|
@@ -216,8 +248,10 @@ function isValidFullAccessExecutionIdentity(identity) {
|
|
|
216
248
|
|| identity.processRole !== 'fullaccess-run'
|
|
217
249
|
|| identity.dataScope !== 'daemon')
|
|
218
250
|
return false;
|
|
219
|
-
if (identity.source === 'fullaccess-command') {
|
|
220
|
-
return typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
|
|
251
|
+
if (identity.source === 'fullaccess-command' || identity.source === 'agent-policy') {
|
|
252
|
+
return typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
|
|
253
|
+
&& (identity.source !== 'agent-policy'
|
|
254
|
+
|| (typeof identity.agentAid === 'string' && identity.agentAid.length > 0));
|
|
221
255
|
}
|
|
222
256
|
return identity.source === 'trigger'
|
|
223
257
|
&& typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
|
|
@@ -228,8 +262,8 @@ function isValidFullAccessExecutionIdentity(identity) {
|
|
|
228
262
|
function hashDelegationToken(token) {
|
|
229
263
|
return crypto.createHash('sha256').update(token).digest('hex');
|
|
230
264
|
}
|
|
231
|
-
function approvedCommandKey(
|
|
232
|
-
return JSON.stringify([
|
|
265
|
+
function approvedCommandKey(sessionTokenHash, sessionId, commandHash) {
|
|
266
|
+
return JSON.stringify([sessionTokenHash, sessionId, commandHash]);
|
|
233
267
|
}
|
|
234
268
|
function isDelegationCommandHash(value) {
|
|
235
269
|
return /^[a-f0-9]{64}$/.test(value);
|
|
@@ -70,6 +70,7 @@ export function buildAuthSubject(input) {
|
|
|
70
70
|
identity: input.identity ?? roleToSessionIdentity(role === 'none' ? null : role),
|
|
71
71
|
isDaemonOwner,
|
|
72
72
|
fromControlChannel: !!input.fromControlChannel,
|
|
73
|
+
ecwebManagement: input.ecwebManagement === true && isTrustedDaemonOwner && input.fromControlChannel === true,
|
|
73
74
|
allowAccess: processRole !== 'none' || (suppliedRole
|
|
74
75
|
? checkRoleAccess(suppliedRole, input.selfAid)
|
|
75
76
|
: roleDetail.allowAccess && checkRoleAccess(role, input.selfAid)),
|
|
@@ -114,6 +115,7 @@ export function authorizeOperation(params) {
|
|
|
114
115
|
return access;
|
|
115
116
|
}
|
|
116
117
|
const ctx = {
|
|
118
|
+
principalId: params.subject.principalId,
|
|
117
119
|
intent: params.intent,
|
|
118
120
|
actorId: params.subject.actorId,
|
|
119
121
|
channel: params.subject.channel,
|
|
@@ -2,7 +2,7 @@ import { logger } from '../../utils/logger.js';
|
|
|
2
2
|
import { LogWriter } from '../../utils/log-writer.js';
|
|
3
3
|
import { resolvePaths } from '../../paths.js';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
|
-
import { buildAuthorizationEventKey, buildToolLifecycleEventKey } from '../audit/event-key.js';
|
|
5
|
+
import { buildAuthorizationEventKey, buildToolLifecycleEventKey, normalizeToolCorrelation } from '../audit/event-key.js';
|
|
6
6
|
import { normalizeExecutionPermissionMode } from '../permission/mode.js';
|
|
7
7
|
export function auditCommandAuthorization(event) {
|
|
8
8
|
const shouldAudit = event.source === 'menu.cli' ||
|
|
@@ -14,6 +14,8 @@ export function auditCommandAuthorization(event) {
|
|
|
14
14
|
event.operation === 'permission.runtime' ||
|
|
15
15
|
event.operation.startsWith('fullaccess.') ||
|
|
16
16
|
event.operation.startsWith('role.') ||
|
|
17
|
+
event.operation === 'file.root.storage' ||
|
|
18
|
+
event.operation.startsWith('file.storage.') ||
|
|
17
19
|
event.operation === 'cli.exec.raw';
|
|
18
20
|
if (!shouldAudit)
|
|
19
21
|
return;
|
|
@@ -25,6 +27,8 @@ export function auditFullAccessEvent(input) {
|
|
|
25
27
|
|| input.event === 'fullaccess.trigger.deescalated';
|
|
26
28
|
const executionEvent = input.event === 'fullaccess.execution.started'
|
|
27
29
|
|| input.event === 'fullaccess.execution.ended'
|
|
30
|
+
|| input.event === 'fullaccess.agent-policy.execution.started'
|
|
31
|
+
|| input.event === 'fullaccess.agent-policy.execution.ended'
|
|
28
32
|
|| input.event === 'fullaccess.trigger.execution.started'
|
|
29
33
|
|| input.event === 'fullaccess.trigger.execution.ended';
|
|
30
34
|
// The scheduler triggers a run, but the executing subject is the task-bound
|
|
@@ -39,7 +43,7 @@ export function auditFullAccessEvent(input) {
|
|
|
39
43
|
ts: Date.now(),
|
|
40
44
|
callId: input.attemptId ?? input.runId ?? input.taskId ?? input.messageId,
|
|
41
45
|
correlationId: input.runId ?? input.taskId,
|
|
42
|
-
source: input.source === '
|
|
46
|
+
source: input.source === 'fullaccess-command' ? 'slash' : 'agent-tool',
|
|
43
47
|
operation: input.event,
|
|
44
48
|
// CommandScope has no separate daemon value; process is the closest
|
|
45
49
|
// existing scope for daemon-owner authorization and keeps audit records
|
|
@@ -97,20 +101,11 @@ export function auditFullAccessEvent(input) {
|
|
|
97
101
|
* denials otherwise only reach `logger.warn` and `TriggerExecutionAnomaly`.
|
|
98
102
|
*/
|
|
99
103
|
export function auditToolPreflightDenial(input) {
|
|
100
|
-
const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
|
|
101
104
|
auditCommandAuthorization({
|
|
102
105
|
ts: Date.now(),
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
callId: input.callId,
|
|
107
|
-
correlationId: input.correlationId,
|
|
108
|
-
requestId: input.requestId,
|
|
109
|
-
}),
|
|
110
|
-
eventPhase: 'preflight',
|
|
111
|
-
} : {}),
|
|
112
|
-
callId: input.callId ?? input.requestId,
|
|
113
|
-
correlationId: input.correlationId ?? input.callId ?? input.requestId,
|
|
106
|
+
eventKey: buildToolLifecycleEventKey(input),
|
|
107
|
+
eventPhase: 'preflight',
|
|
108
|
+
...normalizeToolCorrelation(input),
|
|
114
109
|
source: 'agent-tool',
|
|
115
110
|
operation: 'tool.preflight.deny',
|
|
116
111
|
scope: 'filesystem',
|
|
@@ -138,29 +133,20 @@ export function auditToolPreflightDenial(input) {
|
|
|
138
133
|
role: input.role ?? 'unknown',
|
|
139
134
|
selfAid: input.selfAid,
|
|
140
135
|
peerKey: input.peerKey,
|
|
141
|
-
requestId: input.requestId,
|
|
142
136
|
taskId: input.taskId,
|
|
143
137
|
});
|
|
144
138
|
}
|
|
145
139
|
/**
|
|
146
140
|
* Emit one canonical, structured record for every Codex app-server approval
|
|
147
|
-
* decision.
|
|
141
|
+
* decision. Prefer the tool call id over the approval request id; method and
|
|
148
142
|
* decision source make policy, user, and infrastructure denials distinct.
|
|
149
143
|
*/
|
|
150
144
|
export function auditCodexApprovalDecision(input) {
|
|
151
|
-
const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
|
|
152
145
|
auditCommandAuthorization({
|
|
153
146
|
ts: Date.now(),
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
callId: input.callId,
|
|
158
|
-
correlationId: input.correlationId,
|
|
159
|
-
requestId: input.requestId,
|
|
160
|
-
}),
|
|
161
|
-
eventPhase: 'approval',
|
|
162
|
-
} : {}),
|
|
163
|
-
callId: input.callId ?? input.correlationId ?? input.requestId,
|
|
147
|
+
eventKey: buildToolLifecycleEventKey(input),
|
|
148
|
+
eventPhase: 'approval',
|
|
149
|
+
...normalizeToolCorrelation(input),
|
|
164
150
|
source: 'agent-tool',
|
|
165
151
|
operation: 'codex.approval',
|
|
166
152
|
scope: input.toolName === 'PermissionGrant' ? 'agent' : 'filesystem',
|
|
@@ -172,8 +158,6 @@ export function auditCodexApprovalDecision(input) {
|
|
|
172
158
|
errorCode: input.decision === 'deny'
|
|
173
159
|
? input.decisionSource === 'approval' ? 'USER_DENIED' : input.decisionSource === 'policy' ? 'POLICY_DENIED' : infrastructureErrorCode(input.policyCode)
|
|
174
160
|
: undefined,
|
|
175
|
-
requestId: input.requestId,
|
|
176
|
-
correlationId: input.correlationId ?? input.callId ?? input.requestId,
|
|
177
161
|
sessionId: input.sessionId ?? 'unknown',
|
|
178
162
|
agentAid: input.agentAid ?? 'unknown',
|
|
179
163
|
agentName: input.agentName,
|
|
@@ -204,6 +188,8 @@ export function auditPermissionDecision(input) {
|
|
|
204
188
|
auditPermissionLifecycle({
|
|
205
189
|
phase: 'final',
|
|
206
190
|
requestId,
|
|
191
|
+
callId: input.callId,
|
|
192
|
+
correlationId: input.correlationId,
|
|
207
193
|
sessionId: input.sessionId,
|
|
208
194
|
agentAid: input.agentAid,
|
|
209
195
|
agentName: input.agentName,
|
|
@@ -235,9 +221,7 @@ export function auditPermissionLifecycle(input) {
|
|
|
235
221
|
requestId: input.requestId,
|
|
236
222
|
}),
|
|
237
223
|
eventPhase: input.phase,
|
|
238
|
-
|
|
239
|
-
correlationId: input.correlationId ?? input.callId ?? input.requestId,
|
|
240
|
-
requestId: input.requestId,
|
|
224
|
+
...normalizeToolCorrelation(input),
|
|
241
225
|
sessionId,
|
|
242
226
|
agentAid: input.agentAid ?? 'unknown',
|
|
243
227
|
agentName: input.agentName,
|
|
@@ -279,6 +263,8 @@ export function auditPermissionLifecycle(input) {
|
|
|
279
263
|
export function auditToolInfrastructureFailure(input) {
|
|
280
264
|
auditCommandAuthorization({
|
|
281
265
|
ts: Date.now(),
|
|
266
|
+
eventKey: buildToolLifecycleEventKey(input),
|
|
267
|
+
eventPhase: 'final',
|
|
282
268
|
source: 'agent-tool',
|
|
283
269
|
operation: 'tool.execution.blocked',
|
|
284
270
|
scope: 'filesystem',
|
|
@@ -288,9 +274,7 @@ export function auditToolInfrastructureFailure(input) {
|
|
|
288
274
|
errorCode: infrastructureErrorCode(input.policyCode),
|
|
289
275
|
executed: false,
|
|
290
276
|
executionState: 'blocked',
|
|
291
|
-
|
|
292
|
-
correlationId: input.callId,
|
|
293
|
-
requestId: input.callId,
|
|
277
|
+
...normalizeToolCorrelation(input),
|
|
294
278
|
sessionId: input.sessionId ?? 'unknown',
|
|
295
279
|
agentAid: input.agentAid ?? 'unknown',
|
|
296
280
|
agentName: input.agentName,
|
|
@@ -300,8 +284,20 @@ export function auditToolInfrastructureFailure(input) {
|
|
|
300
284
|
reason: input.reason,
|
|
301
285
|
role: input.role ?? 'unknown',
|
|
302
286
|
taskId: input.taskId,
|
|
303
|
-
argsSummary: input.occurrences
|
|
304
|
-
? {
|
|
287
|
+
argsSummary: input.occurrences || input.sandboxFailure
|
|
288
|
+
? {
|
|
289
|
+
...(input.occurrences ? { occurrences: input.occurrences } : {}),
|
|
290
|
+
// Flat primitive fields survive the audit argument redactor and can
|
|
291
|
+
// be aggregated without reparsing (possibly truncated) stderr.
|
|
292
|
+
...(input.sandboxFailure ? {
|
|
293
|
+
sandboxFailureKind: input.sandboxFailure.kind,
|
|
294
|
+
sandboxFailureObservation: input.sandboxFailure.observation,
|
|
295
|
+
sandboxInitializationFailures: input.sandboxFailure.counters.total,
|
|
296
|
+
sandboxApplySeccompFailures: input.sandboxFailure.counters.applySeccomp,
|
|
297
|
+
...Object.fromEntries(Object.entries(input.sandboxFailure.counters.byKind)
|
|
298
|
+
.map(([kind, count]) => [`sandboxFailures_${kind}`, count])),
|
|
299
|
+
} : {}),
|
|
300
|
+
}
|
|
305
301
|
: undefined,
|
|
306
302
|
});
|
|
307
303
|
}
|
|
@@ -335,7 +331,7 @@ function buildAuditRecord(event) {
|
|
|
335
331
|
: 'authorized');
|
|
336
332
|
const ecwebRequestId = event.source === 'ecweb' ? event.ecwebRequestId : undefined;
|
|
337
333
|
const requestId = event.requestId ?? ecwebRequestId;
|
|
338
|
-
const correlationId = event.correlationId ?? requestId;
|
|
334
|
+
const correlationId = event.correlationId ?? event.callId ?? requestId;
|
|
339
335
|
const lifecycleId = event.callId ?? correlationId ?? requestId;
|
|
340
336
|
return {
|
|
341
337
|
ts: event.ts,
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { getRoleDefinition } from '../../config/roles.js';
|
|
2
2
|
import { isManagementRole } from '../../config/builtin-roles.js';
|
|
3
|
-
import { relationFieldWriteOperation, resolveRoleFieldPermission, } from '../../config/config-field-policy.js';
|
|
3
|
+
import { isUnsupportedBaseagentConfigField, relationFieldWriteOperation, resolveRoleFieldPermission, } from '../../config/config-field-policy.js';
|
|
4
4
|
import { isResolvedConfigMutation, } from '../../config/resolved-config-op.js';
|
|
5
5
|
import { normalizePeer } from '../model/config-scope.js';
|
|
6
6
|
import { getOperationMeta } from './operation-catalog.js';
|
|
7
7
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
8
|
+
import { sameGroupAid } from '../../aun/group/index.js';
|
|
8
9
|
const DAEMON_OWNER_PROCESS_OPERATIONS = new Set([
|
|
9
10
|
'agent.list',
|
|
10
11
|
'agent.create',
|
|
12
|
+
'agent.initialize',
|
|
11
13
|
'agent.delete',
|
|
12
14
|
'agent.enable',
|
|
13
15
|
'agent.disable',
|
|
@@ -99,6 +101,12 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
99
101
|
&& (resolvedConfigCommand.operationId !== operation || resolvedConfigCommand.commandScope !== intent.scope)) {
|
|
100
102
|
return denyDecision(ctx, 'NOT_ALLOWED', 'Resolved config operation does not match the command intent', operation, intent.scope, opMeta.dangerous);
|
|
101
103
|
}
|
|
104
|
+
if (resolvedConfigCommand?.kind === 'field'
|
|
105
|
+
&& resolvedConfigCommand.configScope === 'agent'
|
|
106
|
+
&& (resolvedConfigCommand.field === 'fullaccess' || resolvedConfigCommand.field.startsWith('fullaccess.'))
|
|
107
|
+
&& resolvedConfigCommand.subcommand !== 'get') {
|
|
108
|
+
return denyDecision(ctx, 'NOT_ALLOWED', 'Agent fullaccess can only be configured in ECWeb Agent settings', operation, intent.scope, true);
|
|
109
|
+
}
|
|
102
110
|
// A fullaccess task is deliberately a daemon-scoped runtime identity. It
|
|
103
111
|
// still has to name a known operation and pass structural command parsing,
|
|
104
112
|
// but relation/Agent role rules, source lists, and daemon-owner gates are
|
|
@@ -224,11 +232,30 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
224
232
|
matchedRule: role === 'owner' ? 'agent-owner-persona' : 'daemon-owner-persona',
|
|
225
233
|
};
|
|
226
234
|
}
|
|
235
|
+
if (operation === 'agent.welcome') {
|
|
236
|
+
if (!isCurrentAgentTarget(ctx) || ctx.chatType !== 'private'
|
|
237
|
+
|| (role !== 'owner' && !(role === 'admin' && intent.args.action === 'show'))) {
|
|
238
|
+
return denyDecision(ctx, 'NOT_ALLOWED', 'welcome requires the current Agent Owner in private (admin may read)', operation, intent.scope, opMeta.dangerous);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
allow: true,
|
|
242
|
+
operation,
|
|
243
|
+
scope: intent.scope,
|
|
244
|
+
role,
|
|
245
|
+
dangerous: opMeta.dangerous,
|
|
246
|
+
matchedRule: 'self-agent-welcome',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
227
249
|
if (resolvedConfigCommand) {
|
|
228
250
|
const targetCheck = checkResolvedConfigTarget(ctx, resolvedConfigCommand);
|
|
229
251
|
if (!targetCheck.ok) {
|
|
230
252
|
return denyDecision(ctx, intent.scope === 'relation' ? 'ARGUMENT_MISMATCH' : 'SCOPE_MISMATCH', targetCheck.reason || 'Config target is outside the current task context', operation, intent.scope, opMeta.dangerous);
|
|
231
253
|
}
|
|
254
|
+
if (resolvedConfigCommand.kind === 'field'
|
|
255
|
+
&& resolvedConfigCommand.subcommand === 'get'
|
|
256
|
+
&& isUnsupportedBaseagentConfigField(resolvedConfigCommand.field)) {
|
|
257
|
+
return denyDecision(ctx, 'UNSUPPORTED_FIELD', `Config field ${resolvedConfigCommand.field} is not supported by this version`, operation, intent.scope, opMeta.dangerous);
|
|
258
|
+
}
|
|
232
259
|
const identityMutationCheck = checkManagementIdentityMutation(ctx, resolvedConfigCommand);
|
|
233
260
|
if (!identityMutationCheck.ok) {
|
|
234
261
|
return denyDecision(ctx, 'NOT_ALLOWED', identityMutationCheck.reason || 'Static management identities cannot be changed by this role', operation, intent.scope, opMeta.dangerous);
|
|
@@ -236,7 +263,9 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
236
263
|
if (!isManagementRole(role) && intent.scope !== 'relation') {
|
|
237
264
|
return denyDecision(ctx, 'SCOPE_MISMATCH', `Role ${role} may only access relation-scoped config`, operation, intent.scope, opMeta.dangerous);
|
|
238
265
|
}
|
|
239
|
-
if (role === 'owner'
|
|
266
|
+
if (role === 'owner'
|
|
267
|
+
&& isCurrentAgentTarget(ctx)
|
|
268
|
+
&& !isCrossRelationConfigRead(ctx, resolvedConfigCommand)) {
|
|
240
269
|
return {
|
|
241
270
|
allow: true,
|
|
242
271
|
operation,
|
|
@@ -636,6 +665,19 @@ function checkConfigFieldPolicy(ctx, policy, resolvedConfigOp) {
|
|
|
636
665
|
}
|
|
637
666
|
return { ok: true };
|
|
638
667
|
}
|
|
668
|
+
function isCrossRelationConfigRead(ctx, command) {
|
|
669
|
+
if (!command || command.kind !== 'field' || command.subcommand !== 'get' || command.configScope !== 'relation') {
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
if (!command.peerKey || !ctx.peerKey)
|
|
673
|
+
return true;
|
|
674
|
+
try {
|
|
675
|
+
return normalizeRelationPeer(command.peerKey) !== normalizeRelationPeer(ctx.peerKey);
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
639
681
|
function checkResolvedConfigTarget(ctx, command) {
|
|
640
682
|
if (command.kind === 'global' || command.configScope === 'process' || command.configScope === 'defaults') {
|
|
641
683
|
return { ok: true };
|
|
@@ -652,8 +694,12 @@ function checkResolvedConfigTarget(ctx, command) {
|
|
|
652
694
|
return { ok: true };
|
|
653
695
|
}
|
|
654
696
|
if (ctx.allowExplicitRelationTarget
|
|
655
|
-
&& (ctx.role
|
|
656
|
-
&&
|
|
697
|
+
&& isManagementRole(ctx.role)
|
|
698
|
+
&& command.kind === 'field'
|
|
699
|
+
&& (command.operationId.startsWith('role.') || command.subcommand === 'get')) {
|
|
700
|
+
// Management reads may select another relation of this Agent. This only
|
|
701
|
+
// relaxes target matching; admin field grants/constraints are checked later.
|
|
702
|
+
// Ordinary config writes and whole-config commands remain current-relation only.
|
|
657
703
|
if (!ctx.selfAid || !command.self || command.self !== ctx.selfAid) {
|
|
658
704
|
return { ok: false, reason: 'Only the current agent relation can be targeted' };
|
|
659
705
|
}
|
|
@@ -693,7 +739,8 @@ function checkCurrentRelation(ctx, resolvedConfigCommand) {
|
|
|
693
739
|
return { ok: false, reason: 'Current relation context is required' };
|
|
694
740
|
}
|
|
695
741
|
try {
|
|
696
|
-
if (normalizeRelationPeer(requestedPeer) !== normalizeRelationPeer(ctx.peerKey)
|
|
742
|
+
if (normalizeRelationPeer(requestedPeer) !== normalizeRelationPeer(ctx.peerKey)
|
|
743
|
+
&& !sameAunGroupRelation(ctx, requestedPeer, ctx.peerKey)) {
|
|
697
744
|
if (ctx.intent.operation.startsWith('ec.group.')) {
|
|
698
745
|
const currentGroup = peerIdFromArg(ctx.peerKey);
|
|
699
746
|
const targetGroup = stringArg(ctx.intent.args.targetId) ?? peerIdFromArg(requestedPeer);
|
|
@@ -788,6 +835,20 @@ function collectStringValues(value, out) {
|
|
|
788
835
|
out.add(item);
|
|
789
836
|
}
|
|
790
837
|
}
|
|
838
|
+
function sameAunGroupRelation(ctx, requested, current) {
|
|
839
|
+
if (ctx.chatType !== 'group'
|
|
840
|
+
|| (!ctx.intent.operation.startsWith('ec.group.') && !ctx.intent.operation.startsWith('ec.fs.')))
|
|
841
|
+
return false;
|
|
842
|
+
try {
|
|
843
|
+
const target = parsePeerKey(requested);
|
|
844
|
+
const relation = parsePeerKey(current);
|
|
845
|
+
return target.channelType === 'aun' && relation.channelType === 'aun'
|
|
846
|
+
&& sameGroupAid(target.channelId, relation.channelId);
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
791
852
|
function checkOwnPeer(ctx) {
|
|
792
853
|
const argPeer = stringArg(ctx.intent.args.peer);
|
|
793
854
|
const argPeerKey = stringArg(ctx.intent.args.peerKey);
|
|
@@ -812,6 +873,7 @@ function checkOwnPeer(ctx) {
|
|
|
812
873
|
candidateKeys.add(`${legacyChannelType}::${argPeer}`);
|
|
813
874
|
}
|
|
814
875
|
return candidateKeys.has(expectedPeerKey)
|
|
876
|
+
|| [...candidateKeys].some(key => sameAunGroupRelation(ctx, key, expectedPeerKey))
|
|
815
877
|
? { ok: true }
|
|
816
878
|
: { ok: false, reason: 'Only the current group relation can be targeted' };
|
|
817
879
|
}
|