evolcore 0.0.25 → 0.0.26
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 +607 -565
- package/LICENSE +21 -21
- package/README.md +455 -455
- package/bin/codex-managed-acl.ps1 +160 -160
- package/bin/codex-managed-hook.mjs +208 -208
- package/bin/ec-safe-output.js +327 -327
- package/bin/ec.js +40 -29
- package/bin/install-codex-managed-hooks.mjs +582 -555
- package/dist/agents/baseagent.js +96 -46
- package/dist/agents/claude-runner.js +50 -12
- package/dist/agents/codex/app-server-client.js +13 -5
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +34 -1
- package/dist/agents/codex-runner.js +121 -38
- package/dist/agents/ecagent-runner.js +4 -9
- package/dist/agents/gemini-runner.js +2 -7
- package/dist/agents/runner-types.js +25 -17
- package/dist/aun/aid/agentmd.js +81 -8
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +17 -13
- package/dist/aun/menu-client.js +74 -0
- package/dist/aun/msg/group.js +1 -1
- package/dist/aun/msg/managed-operation.js +1 -1
- package/dist/aun/msg/p2p.js +6 -1
- package/dist/aun/msg/upload.js +30 -22
- package/dist/channels/aun.js +329 -94
- package/dist/cli/agent-command.js +105 -84
- package/dist/cli/agent-create.js +112 -10
- package/dist/cli/agent.js +16 -5
- package/dist/cli/aun-commands.js +297 -296
- package/dist/cli/bench.js +20 -20
- package/dist/cli/config.js +47 -28
- package/dist/cli/contact.js +70 -13
- package/dist/cli/ctl-command.js +22 -22
- package/dist/cli/daemon-commands.js +105 -40
- package/dist/cli/diag.js +31 -0
- package/dist/cli/fs-command.js +67 -67
- package/dist/cli/handoff-command.js +41 -9
- package/dist/cli/index.js +83 -77
- package/dist/cli/link-rules.js +21 -21
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +46 -31
- package/dist/cli/net-check.js +22 -22
- package/dist/cli/queue-command.js +29 -29
- package/dist/cli/response.js +30 -30
- package/dist/cli/restart-monitor.js +87 -40
- package/dist/cli/stats.js +51 -51
- package/dist/cli/trigger-command.js +56 -56
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-operation-service.js +145 -0
- package/dist/config/contact-operation-service.js +96 -7
- package/dist/config/resolved-config-op.js +27 -7
- package/dist/core/agent-application-service.js +2 -1
- package/dist/core/auth/authorization-audit.js +20 -12
- package/dist/core/auth/operation-authorizer.js +25 -4
- package/dist/core/auth/operation-catalog.js +35 -4
- package/dist/core/auth/remote-access-store.js +40 -1
- package/dist/core/bootstrap-service.js +1 -1
- package/dist/core/command/command-handler.js +83 -26
- package/dist/core/command/diag-service.js +118 -0
- package/dist/core/command/menu-catalog.js +12 -3
- package/dist/core/command/menu-handler.js +47 -26
- package/dist/core/command/menu-protocol.js +5 -0
- package/dist/core/command/menu-token-store.js +41 -8
- package/dist/core/command/slash-gate.js +34 -4
- package/dist/core/command/slash-handler.js +86 -21
- package/dist/core/handoff/runtime.js +205 -27
- package/dist/core/handoff/store.js +193 -4
- package/dist/core/message/im-renderer.js +72 -21
- package/dist/core/message/message-bridge.js +69 -9
- package/dist/core/message/message-queue.js +26 -0
- package/dist/core/message/message-utils.js +1 -0
- package/dist/core/message/response-engine.js +47 -28
- package/dist/core/model/catalog-service.js +79 -49
- package/dist/core/model/context-window-suffix.js +33 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-permission.js +12 -3
- package/dist/core/permission/ec-command-parser.js +51 -12
- package/dist/core/permission/sandbox-runtime.js +36 -14
- package/dist/core/permission/tool-policy.js +2 -1
- package/dist/index.js +280 -163
- package/dist/ipc.js +54 -2
- package/dist/response-system/engines/v1/proactive-flow.js +59 -2
- package/dist/stats/billing.js +41 -15
- package/dist/stats/db.js +159 -159
- package/dist/stats/eck-vars.js +9 -9
- package/dist/stats/price-resolver.js +28 -10
- package/dist/stats/query.js +139 -139
- package/dist/stats/role-budget.js +6 -6
- package/dist/stats/writer.js +51 -51
- package/dist/utils/cross-platform.js +30 -1
- package/dist/utils/error-dict.json +160 -160
- package/dist/utils/error-utils.js +21 -0
- package/dist/utils/evolcore-version.js +129 -0
- package/dist/utils/logger.js +3 -1
- package/dist/utils/macos-autostart.js +22 -22
- package/dist/utils/model-prices.jsonl +23 -23
- package/dist/utils/npm-ops.js +176 -35
- package/dist/utils/welcome.js +22 -22
- package/ecagent/LICENSE +21 -21
- package/ecagent/NOTICE +11 -11
- package/ecagent/README.md +61 -61
- package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
- package/ecagent/dist/harness/compaction/compaction.js +83 -83
- package/ecagent/dist/harness/messages.js +7 -7
- package/kits/docs/GUIDE.md +20 -20
- package/kits/docs/INDEX.md +66 -66
- package/kits/docs/aun/CHEATSHEET.md +19 -19
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
- package/kits/docs/channels/aun.md +56 -56
- package/kits/docs/channels/feishu.md +56 -56
- package/kits/docs/context-assembly.md +364 -364
- package/kits/docs/eck_templates/GUIDE.template.md +22 -22
- package/kits/docs/eck_templates/INDEX.template.md +28 -28
- package/kits/docs/eck_templates/path-registry.template.md +33 -33
- package/kits/docs/eck_templates/runtime.template.md +19 -19
- package/kits/docs/evolcore/INDEX.md +71 -71
- package/kits/docs/evolcore/agent-create.md +170 -165
- package/kits/docs/evolcore/agent.md +66 -66
- package/kits/docs/evolcore/aid.md +60 -60
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
- package/kits/docs/evolcore/config.md +202 -195
- package/kits/docs/evolcore/contact.md +75 -68
- package/kits/docs/evolcore/ctl.md +46 -46
- package/kits/docs/evolcore/event.md +216 -216
- package/kits/docs/evolcore/fs-architecture.md +1215 -1215
- package/kits/docs/evolcore/fs.md +110 -110
- package/kits/docs/evolcore/group-collaboration.md +243 -243
- package/kits/docs/evolcore/group-fs.md +17 -17
- package/kits/docs/evolcore/group-rules.md +251 -251
- package/kits/docs/evolcore/group.md +152 -152
- package/kits/docs/evolcore/menu-client.md +42 -34
- package/kits/docs/evolcore/model.md +50 -50
- package/kits/docs/evolcore/msg.md +166 -165
- package/kits/docs/evolcore/response.md +75 -75
- package/kits/docs/evolcore/rpc.md +37 -37
- package/kits/docs/evolcore/self-summary.md +29 -29
- package/kits/docs/evolcore/stats.md +83 -83
- package/kits/docs/evolcore/storage.md +50 -50
- package/kits/docs/evolcore/trigger.md +575 -575
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
- package/kits/docs/identity/PATH_OPS.md +16 -16
- package/kits/docs/identity/ROLE_DETAIL.md +25 -25
- package/kits/docs/identity/identity-tools.md +26 -26
- package/kits/docs/path-registry.md +62 -62
- package/kits/docs/prompt-loading-architecture.md +269 -269
- package/kits/docs/venues/aun-group.md +45 -45
- package/kits/docs/venues/aun-private.md +10 -10
- package/kits/docs/venues/client-desktop.md +10 -10
- package/kits/docs/venues/client-mobile.md +10 -10
- package/kits/docs/venues/feishu-group.md +13 -13
- package/kits/docs/venues/feishu-private.md +9 -9
- package/kits/docs/venues/group.md +37 -25
- package/kits/docs/venues/private.md +10 -10
- package/kits/eck_manifest.auxiliary.json +43 -43
- package/kits/eck_manifest.json +190 -190
- package/kits/eck_message_manifest.json +74 -74
- package/kits/migrations/README-role-config-v4.md +32 -32
- package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
- package/kits/migrations/migrate-role-config-v4.mjs +614 -614
- package/kits/migrations/migrate-role-config-v5.mjs +365 -365
- package/kits/migrations/rename-config-file.mjs +99 -99
- package/kits/rules/01-overview.md +162 -162
- package/kits/rules/02-navigation.md +96 -96
- package/kits/rules/03-identity.md +36 -36
- package/kits/rules/04-relation.md +75 -75
- package/kits/rules/05-venue.md +60 -60
- package/kits/rules/06-channel.md +63 -63
- package/kits/schemas/_meta.json +53 -53
- package/kits/schemas/agent-config.schema.1.json +177 -177
- package/kits/schemas/agent-config.schema.10.json +395 -395
- package/kits/schemas/agent-config.schema.11.json +421 -421
- package/kits/schemas/agent-config.schema.12.json +427 -427
- package/kits/schemas/agent-config.schema.2.json +239 -239
- package/kits/schemas/agent-config.schema.3.json +119 -119
- package/kits/schemas/agent-config.schema.4.json +208 -208
- package/kits/schemas/agent-config.schema.5.json +326 -326
- package/kits/schemas/agent-config.schema.6.json +322 -322
- package/kits/schemas/agent-config.schema.7.json +303 -303
- package/kits/schemas/agent-config.schema.8.json +304 -304
- package/kits/schemas/agent-config.schema.9.json +364 -364
- package/kits/schemas/contact-book.schema.1.json +36 -36
- package/kits/schemas/contact-book.schema.2.json +43 -43
- package/kits/schemas/contact-book.schema.3.json +67 -67
- package/kits/schemas/daemon.schema.1.json +91 -91
- package/kits/schemas/daemon.schema.2.json +101 -101
- package/kits/schemas/daemon.schema.3.json +123 -123
- package/kits/schemas/daemon.schema.4.json +136 -136
- package/kits/schemas/daemon.schema.5.json +135 -135
- package/kits/schemas/daemon.schema.6.json +131 -131
- package/kits/schemas/defaults.schema.1.json +81 -81
- package/kits/schemas/defaults.schema.2.json +85 -85
- package/kits/schemas/defaults.schema.3.json +73 -73
- package/kits/schemas/defaults.schema.4.json +107 -107
- package/kits/schemas/defaults.schema.5.json +119 -119
- package/kits/schemas/menu-exec-schema-commands.md +208 -208
- package/kits/schemas/migrations/README.md +30 -30
- package/kits/schemas/relation-config.schema.1.json +158 -158
- package/kits/schemas/relation-config.schema.2.json +73 -73
- package/kits/schemas/relation-config.schema.3.json +50 -50
- package/kits/schemas/relation-config.schema.4.json +47 -47
- package/kits/schemas/relation-config.schema.5.json +47 -47
- package/kits/schemas/relation-config.schema.6.json +46 -46
- package/kits/schemas/relation-config.schema.7.json +59 -59
- package/kits/schemas/relation-config.schema.8.json +103 -103
- package/kits/schemas/role-config.schema.1.json +201 -201
- package/kits/schemas/role-registry.schema.1.json +35 -35
- package/kits/schemas/single-session.schema.1.json +31 -31
- package/kits/schemas/single-session.schema.2.json +57 -57
- package/kits/schemas/single-session.schema.3.json +32 -32
- package/kits/templates/bootstrap-welcome.md +17 -17
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
- package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
- package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
- package/kits/templates/message-fragments/inject-default.md +2 -2
- package/kits/templates/message-fragments/item.md +2 -2
- package/kits/templates/roles/admin.json +92 -92
- package/kits/templates/roles/member.json +96 -96
- package/kits/templates/roles/owner.json +9 -9
- package/kits/templates/roles/visitor.json +85 -85
- package/kits/templates/system-fragments/baseagent.md +14 -14
- package/kits/templates/system-fragments/bootstrap.md +31 -29
- package/kits/templates/system-fragments/channel.md +64 -64
- package/kits/templates/system-fragments/commands.md +28 -28
- package/kits/templates/system-fragments/identity.md +13 -13
- package/kits/templates/system-fragments/relation.md +19 -19
- package/kits/templates/system-fragments/session.md +59 -59
- package/kits/templates/system-fragments/venue.md +31 -31
- package/package.json +100 -98
|
@@ -5,6 +5,9 @@ import { currentVersion, fieldSchemaDefault, isSchemaName, listSchemaNames, list
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { resolvePaths, agentDir, agentRelationConfig } from '../paths.js';
|
|
8
|
+
import { roleMenuQuery, roleMenuUpdate } from '../core/command/role-menu.js';
|
|
9
|
+
import { parsePeerKey } from '../core/relation/peer-identity.js';
|
|
10
|
+
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
8
11
|
export const AUTHENTICATED_CONFIG_ROLE_ENV = 'EVOLCORE_AUTHENTICATED_CONFIG_ROLE';
|
|
9
12
|
export function executeResolvedConfigCommand(command, options = {}) {
|
|
10
13
|
try {
|
|
@@ -23,6 +26,134 @@ export function executeResolvedConfigCommand(command, options = {}) {
|
|
|
23
26
|
return { ok: false, code: 'CONFIG_ERROR', error: error instanceof Error ? error.message : String(error) };
|
|
24
27
|
}
|
|
25
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Execute a config command that may use an asynchronous domain service.
|
|
31
|
+
* Relation role assignments are intentionally handled here instead of by
|
|
32
|
+
* mutateConfig(), so Role Menu remains the single write path for validation,
|
|
33
|
+
* revisions, journaling, and audit.
|
|
34
|
+
*/
|
|
35
|
+
export async function executeResolvedConfigCommandAsync(command, options = {}) {
|
|
36
|
+
try {
|
|
37
|
+
if (isRelationRoleCommand(command)) {
|
|
38
|
+
return await executeRelationRoleCommand(command, options);
|
|
39
|
+
}
|
|
40
|
+
return executeResolvedConfigCommand(command, options);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof ConfigError)
|
|
44
|
+
return { ok: false, code: error.code, error: error.message, ...(error.data !== undefined ? { data: error.data } : {}) };
|
|
45
|
+
const roleError = error;
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
code: roleError?.code || 'CONFIG_ERROR',
|
|
49
|
+
error: roleError?.message || String(error),
|
|
50
|
+
...(roleError?.data !== undefined ? { data: roleError.data } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isRelationRoleCommand(command) {
|
|
55
|
+
return command.kind === 'field'
|
|
56
|
+
&& command.field === 'role'
|
|
57
|
+
&& command.configScope === 'relation'
|
|
58
|
+
&& (command.operationId === 'role.relation.read'
|
|
59
|
+
|| command.operationId === 'role.assign'
|
|
60
|
+
|| command.operationId === 'role.revoke');
|
|
61
|
+
}
|
|
62
|
+
async function executeRelationRoleCommand(op, options) {
|
|
63
|
+
if (!op.self || !op.peerKey) {
|
|
64
|
+
return { ok: false, code: 'PEER_REQUIRED', error: 'role requires --self <aid> --peer <peer-aid>' };
|
|
65
|
+
}
|
|
66
|
+
const target = roleTargetFromPeerKey(op.peerKey);
|
|
67
|
+
const context = options.roleContext ?? {
|
|
68
|
+
self: op.self,
|
|
69
|
+
actorRole: options.role || 'owner',
|
|
70
|
+
actorRoleSource: 'local-cli',
|
|
71
|
+
isDaemonOwner: false,
|
|
72
|
+
fromControlChannel: false,
|
|
73
|
+
aunAvailable: true,
|
|
74
|
+
};
|
|
75
|
+
if (context.self !== op.self) {
|
|
76
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Role target does not match the receiving Agent' };
|
|
77
|
+
}
|
|
78
|
+
const targetArgs = { self: op.self, view: 'target', target };
|
|
79
|
+
const current = await roleMenuQuery(context, targetArgs);
|
|
80
|
+
const explicitRole = current?.assignment?.explicitRole
|
|
81
|
+
?? current?.assignment?.explicitDefaultRole
|
|
82
|
+
?? null;
|
|
83
|
+
const effectiveRole = current?.effectiveRole?.roleId
|
|
84
|
+
?? current?.baselineRole?.roleId
|
|
85
|
+
?? null;
|
|
86
|
+
const effectiveRoleSource = current?.effectiveRole?.source
|
|
87
|
+
?? current?.baselineRole?.source
|
|
88
|
+
?? 'none';
|
|
89
|
+
const roleDetails = { explicitRole, effectiveRole, effectiveRoleSource };
|
|
90
|
+
if (op.subcommand === 'get') {
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
subcommand: 'get',
|
|
94
|
+
field: op.field,
|
|
95
|
+
value: explicitRole,
|
|
96
|
+
scope: op.configScope,
|
|
97
|
+
roleDetails,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const nextValue = op.subcommand === 'unset' ? null : op.value;
|
|
101
|
+
const updated = await roleMenuUpdate(context, {
|
|
102
|
+
self: op.self,
|
|
103
|
+
resource: 'assignment',
|
|
104
|
+
target,
|
|
105
|
+
expectedPolicyRevision: current.policyRevision,
|
|
106
|
+
expectedRelationRevision: current.relationRevision,
|
|
107
|
+
}, JSON.stringify(nextValue));
|
|
108
|
+
const updatedExplicitRole = updated?.assignment?.explicitRole
|
|
109
|
+
?? updated?.assignment?.explicitDefaultRole
|
|
110
|
+
?? null;
|
|
111
|
+
const updatedEffectiveRole = updated?.effectiveRole?.roleId
|
|
112
|
+
?? updated?.baselineRole?.roleId
|
|
113
|
+
?? null;
|
|
114
|
+
const updatedEffectiveRoleSource = updated?.effectiveRole?.source
|
|
115
|
+
?? updated?.baselineRole?.source
|
|
116
|
+
?? 'none';
|
|
117
|
+
const updatedDetails = {
|
|
118
|
+
explicitRole: updatedExplicitRole,
|
|
119
|
+
effectiveRole: updatedEffectiveRole,
|
|
120
|
+
effectiveRoleSource: updatedEffectiveRoleSource,
|
|
121
|
+
};
|
|
122
|
+
if (op.subcommand === 'unset') {
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
subcommand: 'unset',
|
|
126
|
+
field: op.field,
|
|
127
|
+
removed: updated.changed === true,
|
|
128
|
+
scope: op.configScope,
|
|
129
|
+
roleDetails: updatedDetails,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
subcommand: 'set',
|
|
135
|
+
field: op.field,
|
|
136
|
+
value: updatedExplicitRole,
|
|
137
|
+
scope: op.configScope,
|
|
138
|
+
file: 'relation-config',
|
|
139
|
+
roleDetails: updatedDetails,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function roleTargetFromPeerKey(peerKey) {
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = parsePeerKey(peerKey);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw new ConfigError('INVALID_SELECTOR', `Invalid relation peer key: ${peerKey}`);
|
|
149
|
+
}
|
|
150
|
+
if (parsed.channelType !== 'aun') {
|
|
151
|
+
throw new ConfigError('NOT_SUPPORTED', 'Relation role assignments currently support AUN peers only');
|
|
152
|
+
}
|
|
153
|
+
return isExplicitGroupId(parsed.channelId)
|
|
154
|
+
? { kind: 'group', groupId: parsed.channelId }
|
|
155
|
+
: { kind: 'private', peerAid: parsed.channelId };
|
|
156
|
+
}
|
|
26
157
|
export function executeResolvedConfigOperation(op, options = {}) {
|
|
27
158
|
try {
|
|
28
159
|
const missingAgent = selectedAgentError(op);
|
|
@@ -298,6 +429,13 @@ function getWithSchemaDefault(op, absent, note) {
|
|
|
298
429
|
return { ok: true, subcommand: 'get', field: op.field, value: absent, scope: op.configScope, ...nt };
|
|
299
430
|
}
|
|
300
431
|
function executeSet(op, options) {
|
|
432
|
+
if (op.field === 'role') {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
code: 'ROLE_POLICY_MANAGED',
|
|
436
|
+
error: 'Relation role assignments require the asynchronous role assignment service',
|
|
437
|
+
};
|
|
438
|
+
}
|
|
301
439
|
if (!op.route)
|
|
302
440
|
return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
|
|
303
441
|
const selector = selectorFor(op, options);
|
|
@@ -341,6 +479,13 @@ function executeSet(op, options) {
|
|
|
341
479
|
};
|
|
342
480
|
}
|
|
343
481
|
function executeUnset(op, options) {
|
|
482
|
+
if (op.field === 'role') {
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
code: 'ROLE_POLICY_MANAGED',
|
|
486
|
+
error: 'Relation role assignments require the asynchronous role assignment service',
|
|
487
|
+
};
|
|
488
|
+
}
|
|
344
489
|
if (!op.route)
|
|
345
490
|
return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
|
|
346
491
|
const selector = selectorFor(op, options);
|
|
@@ -5,6 +5,8 @@ import { agentConfig } from '../paths.js';
|
|
|
5
5
|
import { getContactSnapshot, resolveContactView } from './contact-book.js';
|
|
6
6
|
import { ContactMutationError } from './contact-book-store.js';
|
|
7
7
|
import { addContact, mutateContactWithOperation } from './contact-request-service.js';
|
|
8
|
+
import { expirePendingContactRequests, reviewContactRequest } from './contact-request-service.js';
|
|
9
|
+
import { listStaticAgentOwners } from './peer-role-resolver.js';
|
|
8
10
|
export function resolveContactCommand(argv) {
|
|
9
11
|
const args = argv[0] === 'contact' ? argv.slice(1) : [...argv];
|
|
10
12
|
const selfAid = String(argValue(args, '--self') || '').trim();
|
|
@@ -15,12 +17,41 @@ export function resolveContactCommand(argv) {
|
|
|
15
17
|
if (extra.length > 0)
|
|
16
18
|
return invalid('INVALID_ARGUMENT', `Unexpected contact arguments: ${extra.join(' ')}`);
|
|
17
19
|
if (command === 'list') {
|
|
18
|
-
if (subcommand !== undefined
|
|
19
|
-
return invalid('INVALID_ARGUMENT', '
|
|
20
|
+
if (subcommand !== undefined) {
|
|
21
|
+
return invalid('INVALID_ARGUMENT', 'contact list does not accept a positional argument');
|
|
20
22
|
}
|
|
23
|
+
const filter = args.includes('--blocked')
|
|
24
|
+
? 'blocked'
|
|
25
|
+
: args.includes('--pending')
|
|
26
|
+
? 'pending'
|
|
27
|
+
: args.includes('--all') ? 'all' : undefined;
|
|
28
|
+
if (!filter)
|
|
29
|
+
return invalid('INVALID_ARGUMENT', 'contact list requires --blocked, --pending, or --all');
|
|
21
30
|
return {
|
|
22
31
|
ok: true,
|
|
23
|
-
command: { kind: 'list-blocked', operationId: 'contact.read', selfAid, canonicalArgv: canonicalArgv(argv) },
|
|
32
|
+
command: { kind: 'list-blocked', operationId: 'contact.read', selfAid, filter, canonicalArgv: canonicalArgv(argv) },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (command === 'approve' || command === 'reject' || command === 'block-request') {
|
|
36
|
+
if (!isValidAid(subcommand || '') || isExplicitGroupId(subcommand || '')) {
|
|
37
|
+
return invalid('INVALID_AID', 'primaryId must be a valid individual AID');
|
|
38
|
+
}
|
|
39
|
+
const requestId = String(argValue(args, '--request-id') || '').trim();
|
|
40
|
+
if (!requestId)
|
|
41
|
+
return invalid('INVALID_ARGUMENT', `${command} requires --request-id <request-id>`);
|
|
42
|
+
const expectedContactRevision = argValue(args, '--contact-revision')?.trim() || undefined;
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
command: {
|
|
46
|
+
kind: 'review-request',
|
|
47
|
+
operationId: 'contact.review',
|
|
48
|
+
selfAid,
|
|
49
|
+
primaryId: subcommand,
|
|
50
|
+
requestId,
|
|
51
|
+
decision: command === 'approve' ? 'approve' : command === 'reject' ? 'reject' : 'block',
|
|
52
|
+
expectedContactRevision,
|
|
53
|
+
canonicalArgv: canonicalArgv(argv),
|
|
54
|
+
},
|
|
24
55
|
};
|
|
25
56
|
}
|
|
26
57
|
if (command === 'block' || command === 'unblock') {
|
|
@@ -62,13 +93,63 @@ export async function executeResolvedContactCommand(command, actor = 'local-cli'
|
|
|
62
93
|
}
|
|
63
94
|
try {
|
|
64
95
|
if (command.kind === 'list-blocked') {
|
|
96
|
+
await expirePendingContactRequests(command.selfAid);
|
|
65
97
|
const snapshot = getContactSnapshot(command.selfAid);
|
|
98
|
+
const ids = command.filter === 'blocked'
|
|
99
|
+
? [...snapshot.blockedIndex]
|
|
100
|
+
: [...snapshot.contacts.entries()]
|
|
101
|
+
.filter(([, entry]) => command.filter === 'all' || entry.status === 'pending')
|
|
102
|
+
.map(([id]) => id);
|
|
103
|
+
const items = ids.sort().map(id => contactItem(command.selfAid, id));
|
|
104
|
+
if (command.filter === 'blocked') {
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
operation: 'list-blocked',
|
|
108
|
+
selfAid: command.selfAid,
|
|
109
|
+
contactRevision: snapshot.contactRevision,
|
|
110
|
+
items,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
66
113
|
return {
|
|
67
114
|
ok: true,
|
|
68
|
-
operation: 'list-
|
|
115
|
+
operation: 'list-contacts',
|
|
69
116
|
selfAid: command.selfAid,
|
|
117
|
+
filter: command.filter,
|
|
70
118
|
contactRevision: snapshot.contactRevision,
|
|
71
|
-
items
|
|
119
|
+
items,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (command.kind === 'review-request') {
|
|
123
|
+
const expectedContactRevision = command.expectedContactRevision ?? getContactSnapshot(command.selfAid).contactRevision;
|
|
124
|
+
const reviewerAid = actor === 'local-cli'
|
|
125
|
+
? listStaticAgentOwners(command.selfAid)[0]
|
|
126
|
+
: actor;
|
|
127
|
+
if (!reviewerAid) {
|
|
128
|
+
return { ok: false, code: 'NO_OWNER', error: `Agent has no owner: ${command.selfAid}` };
|
|
129
|
+
}
|
|
130
|
+
const reviewed = await reviewContactRequest({
|
|
131
|
+
selfAid: command.selfAid,
|
|
132
|
+
primaryId: command.primaryId,
|
|
133
|
+
requestId: command.requestId,
|
|
134
|
+
expectedContactRevision,
|
|
135
|
+
approverId: reviewerAid,
|
|
136
|
+
actorId: reviewerAid,
|
|
137
|
+
decision: command.decision,
|
|
138
|
+
});
|
|
139
|
+
if (reviewed.code !== 'approved' && reviewed.code !== 'rejected' && reviewed.code !== 'blocked') {
|
|
140
|
+
const code = reviewed.code === 'request-expired' ? 'REQUEST_EXPIRED'
|
|
141
|
+
: reviewed.code === 'revision-conflict' ? 'CONFLICT'
|
|
142
|
+
: reviewed.code === 'forbidden' ? 'PERMISSION_DENIED' : 'REQUEST_STALE';
|
|
143
|
+
return { ok: false, code, error: reviewed.code, data: { contactRevision: reviewed.contactRevision } };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
operation: 'review-request',
|
|
148
|
+
selfAid: command.selfAid,
|
|
149
|
+
primaryId: command.primaryId,
|
|
150
|
+
requestId: command.requestId,
|
|
151
|
+
result: reviewed.code,
|
|
152
|
+
contactRevision: reviewed.contactRevision,
|
|
72
153
|
};
|
|
73
154
|
}
|
|
74
155
|
if (command.kind === 'set-blocked') {
|
|
@@ -129,16 +210,24 @@ function stripOptions(args) {
|
|
|
129
210
|
const result = [];
|
|
130
211
|
for (let index = 0; index < args.length; index += 1) {
|
|
131
212
|
const arg = args[index];
|
|
132
|
-
if (arg === '--self' || arg === '--format') {
|
|
213
|
+
if (arg === '--self' || arg === '--format' || arg === '--request-id' || arg === '--contact-revision') {
|
|
133
214
|
index += 1;
|
|
134
215
|
continue;
|
|
135
216
|
}
|
|
136
|
-
if (arg === '--blocked')
|
|
217
|
+
if (arg === '--blocked' || arg === '--pending' || arg === '--all')
|
|
137
218
|
continue;
|
|
138
219
|
result.push(arg);
|
|
139
220
|
}
|
|
140
221
|
return result;
|
|
141
222
|
}
|
|
223
|
+
function contactItem(selfAid, primaryId) {
|
|
224
|
+
const view = resolveContactView(selfAid, primaryId);
|
|
225
|
+
const entry = getContactSnapshot(selfAid).contacts.get(primaryId);
|
|
226
|
+
return {
|
|
227
|
+
...view,
|
|
228
|
+
...(entry?.pendingRequest ? { pendingRequest: { ...entry.pendingRequest } } : {}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
142
231
|
function canonicalArgv(argv) {
|
|
143
232
|
return argv[0] === 'contact' ? [...argv] : ['contact', ...argv];
|
|
144
233
|
}
|
|
@@ -97,11 +97,12 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
|
|
|
97
97
|
reason: 'permissionMode is defined by roles; use the role policy editor',
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
const isRelationRole = field === 'role';
|
|
101
|
+
if (isRelationRole && selector.scope !== 'relation') {
|
|
101
102
|
return {
|
|
102
103
|
ok: false,
|
|
103
|
-
code: '
|
|
104
|
-
reason:
|
|
104
|
+
code: 'RELATION_SELECTOR_REQUIRED',
|
|
105
|
+
reason: `config ${subcommand} role needs a relation target: --self <aid> --peer <peer-aid>`,
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
108
|
if (subcommand !== 'get' && (field === 'roles' || field.startsWith('roles.'))) {
|
|
@@ -129,12 +130,14 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
|
|
|
129
130
|
reason: `Config field ${field} does not support object writes`,
|
|
130
131
|
};
|
|
131
132
|
}
|
|
132
|
-
const commandScope = selector.scope === 'defaults' ? 'process' : selector.scope;
|
|
133
|
+
const commandScope = isRelationRole ? 'agent' : selector.scope === 'defaults' ? 'process' : selector.scope;
|
|
133
134
|
const isSafe = fieldRule.class === 'safe-scalar' || fieldRule.class === 'safe-readonly-object';
|
|
134
135
|
const isManagementOperation = commandScope === 'process' || !isSafe;
|
|
135
|
-
const operationId =
|
|
136
|
-
? subcommand === 'get' ? '
|
|
137
|
-
:
|
|
136
|
+
const operationId = isRelationRole
|
|
137
|
+
? subcommand === 'get' ? 'role.relation.read' : subcommand === 'set' ? 'role.assign' : 'role.revoke'
|
|
138
|
+
: isManagementOperation
|
|
139
|
+
? subcommand === 'get' ? 'config.read' : 'config.write'
|
|
140
|
+
: `config.${subcommand}`;
|
|
138
141
|
let route;
|
|
139
142
|
const routeRequired = subcommand !== 'get' || !isManagementOperation;
|
|
140
143
|
try {
|
|
@@ -173,6 +176,23 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
|
|
|
173
176
|
return parsedOwners;
|
|
174
177
|
value = parsedOwners.value;
|
|
175
178
|
}
|
|
179
|
+
else if (isRelationRole) {
|
|
180
|
+
const raw = rawValue.trim();
|
|
181
|
+
if (!raw)
|
|
182
|
+
return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'Config field role cannot be empty' };
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(raw);
|
|
185
|
+
if (typeof parsed === 'string')
|
|
186
|
+
value = parsed;
|
|
187
|
+
else if (parsed === null)
|
|
188
|
+
return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'config set role requires a role ID; use config unset role to revoke it' };
|
|
189
|
+
else
|
|
190
|
+
return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'Config field role must be a role ID or null' };
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
value = raw;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
176
196
|
else if (fieldRule.class === 'safe-scalar') {
|
|
177
197
|
const parsedValue = parseConfigFieldValue(field, rawValue);
|
|
178
198
|
if (!parsedValue.ok)
|
|
@@ -85,13 +85,14 @@ export class AgentApplicationService {
|
|
|
85
85
|
unique_peer_count: stats?.uniquePeerCount ?? 0,
|
|
86
86
|
}
|
|
87
87
|
: null;
|
|
88
|
-
const config = agent.config;
|
|
88
|
+
const config = (loadAgent(aid) ?? agent.config);
|
|
89
89
|
const identity = readAgentMdIdentity(aid);
|
|
90
90
|
const paths = resolvePaths();
|
|
91
91
|
return {
|
|
92
92
|
ok: true,
|
|
93
93
|
aid,
|
|
94
94
|
status: info.status || 'stopped',
|
|
95
|
+
lifecycle: config?.lifecycle ?? null,
|
|
95
96
|
identity,
|
|
96
97
|
config: {
|
|
97
98
|
baseagent: info.baseagent || null,
|
|
@@ -333,23 +333,31 @@ function buildAuditRecord(event) {
|
|
|
333
333
|
: executed
|
|
334
334
|
? (event.exitCode === undefined || event.exitCode === 0 ? 'completed' : 'failed')
|
|
335
335
|
: 'authorized');
|
|
336
|
-
const
|
|
336
|
+
const ecwebRequestId = event.source === 'ecweb' ? event.ecwebRequestId : undefined;
|
|
337
|
+
const requestId = event.requestId ?? ecwebRequestId;
|
|
338
|
+
const correlationId = event.correlationId ?? requestId;
|
|
339
|
+
const lifecycleId = event.callId ?? correlationId ?? requestId;
|
|
337
340
|
return {
|
|
338
341
|
ts: event.ts,
|
|
339
|
-
eventKey: event.eventKey ?? (
|
|
340
|
-
?
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
342
|
+
eventKey: event.eventKey ?? (ecwebRequestId
|
|
343
|
+
? `ecweb:${crypto.createHash('sha256').update(JSON.stringify([
|
|
344
|
+
ecwebRequestId, event.targetAgentAid ?? event.selfAid, event.operation,
|
|
345
|
+
])).digest('hex').slice(0, 20)}`
|
|
346
|
+
: lifecycleId
|
|
347
|
+
? buildAuthorizationEventKey({
|
|
348
|
+
sessionId: event.sessionId,
|
|
349
|
+
callId: event.callId,
|
|
350
|
+
correlationId,
|
|
351
|
+
requestId,
|
|
352
|
+
operation: event.operation,
|
|
353
|
+
})
|
|
354
|
+
: undefined),
|
|
348
355
|
eventPhase: event.eventPhase,
|
|
349
356
|
callId: event.callId,
|
|
350
|
-
correlationId
|
|
351
|
-
requestId
|
|
357
|
+
correlationId,
|
|
358
|
+
requestId,
|
|
352
359
|
sessionId: event.sessionId,
|
|
360
|
+
...(event.source === 'ecweb' && !event.sessionId ? { sessionScope: 'not_applicable' } : {}),
|
|
353
361
|
agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
|
|
354
362
|
agentName: resolveAgentName(event),
|
|
355
363
|
permissionMode: normalizeAuditPermissionMode(event.permissionMode),
|
|
@@ -13,6 +13,7 @@ const DAEMON_OWNER_PROCESS_OPERATIONS = new Set([
|
|
|
13
13
|
'agent.disable',
|
|
14
14
|
'system.restart',
|
|
15
15
|
'system.upgrade',
|
|
16
|
+
'system.paircode',
|
|
16
17
|
'gateway.read',
|
|
17
18
|
'gateway.write',
|
|
18
19
|
'stats.peers',
|
|
@@ -204,6 +205,14 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
204
205
|
matchedRule: 'self-agent-read',
|
|
205
206
|
};
|
|
206
207
|
}
|
|
208
|
+
// Cross-Agent reads are a management/control-plane capability. A custom
|
|
209
|
+
// role must not widen this boundary merely by granting itself agent.show.
|
|
210
|
+
if (operation === 'agent.show'
|
|
211
|
+
&& !isCurrentAgentTarget(ctx)
|
|
212
|
+
&& !isManagementRole(role)
|
|
213
|
+
&& !hasDaemonOwnerRole(ctx)) {
|
|
214
|
+
return denyDecision(ctx, 'NOT_ALLOWED', `${operation} may only target the current Agent from a managed Agent task.`, operation, intent.scope, opMeta.dangerous, undefined, 'TARGET_SELF_ONLY');
|
|
215
|
+
}
|
|
207
216
|
if (operation === 'agent.persona' && isCurrentAgentTarget(ctx)
|
|
208
217
|
&& (role === 'owner' || hasDaemonOwnerRole(ctx))) {
|
|
209
218
|
return {
|
|
@@ -642,7 +651,9 @@ function checkResolvedConfigTarget(ctx, command) {
|
|
|
642
651
|
}
|
|
643
652
|
return { ok: true };
|
|
644
653
|
}
|
|
645
|
-
if (ctx.allowExplicitRelationTarget
|
|
654
|
+
if (ctx.allowExplicitRelationTarget
|
|
655
|
+
&& (ctx.role === 'owner' || ctx.role === 'admin')
|
|
656
|
+
&& (command.kind === 'field' && command.operationId.startsWith('role.'))) {
|
|
646
657
|
if (!ctx.selfAid || !command.self || command.self !== ctx.selfAid) {
|
|
647
658
|
return { ok: false, reason: 'Only the current agent relation can be targeted' };
|
|
648
659
|
}
|
|
@@ -806,10 +817,10 @@ function checkOwnPeer(ctx) {
|
|
|
806
817
|
}
|
|
807
818
|
if (ctx.actorId) {
|
|
808
819
|
if (requestedPeerId && requestedPeerId !== ctx.actorId) {
|
|
809
|
-
return { ok: false, reason:
|
|
820
|
+
return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerId) };
|
|
810
821
|
}
|
|
811
822
|
if (requestedPeerKeyId && requestedPeerKeyId !== ctx.actorId) {
|
|
812
|
-
return { ok: false, reason:
|
|
823
|
+
return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerKeyId) };
|
|
813
824
|
}
|
|
814
825
|
}
|
|
815
826
|
if (!expectedPeerKey)
|
|
@@ -827,10 +838,20 @@ function checkOwnPeer(ctx) {
|
|
|
827
838
|
candidateKeys.add(`${legacyChannelType}::${argPeer}`);
|
|
828
839
|
}
|
|
829
840
|
if (!candidateKeys.has(expectedPeerKey)) {
|
|
830
|
-
return { ok: false, reason:
|
|
841
|
+
return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerId ?? requestedPeerKeyId) };
|
|
831
842
|
}
|
|
832
843
|
return { ok: true };
|
|
833
844
|
}
|
|
845
|
+
function ownPeerOnlyDenialReason(ctx, requestedPeer) {
|
|
846
|
+
if (ctx.intent.operation !== 'ec.msg.send' && ctx.intent.operation !== 'ec.msg.file') {
|
|
847
|
+
return 'Only the actor peer can be targeted';
|
|
848
|
+
}
|
|
849
|
+
const currentPeer = ctx.actorId || peerIdFromArg(ctx.peerKey || '') || '当前私聊对端';
|
|
850
|
+
const target = requestedPeer || '指定目标';
|
|
851
|
+
return `当前托管会话只能联系当前私聊对端(${currentPeer}),不能联系第三方目标(${target})。`
|
|
852
|
+
+ ' 如果本次请求来自 handoff,请执行 ec handoff return <handoff-id> "<失败原因>"(已有命令)将失败原因反馈给原会话;'
|
|
853
|
+
+ '否则请让目标先直接发起会话,或由 Agent owner 为当前角色配置允许 relay/第三方外联后再发送。';
|
|
854
|
+
}
|
|
834
855
|
function peerIdFromArg(value) {
|
|
835
856
|
try {
|
|
836
857
|
return parsePeerKey(value).channelId;
|
|
@@ -100,6 +100,13 @@ const HANDOFF_WRITE_OPERATIONS = [{
|
|
|
100
100
|
defaultScopes: ['relation', 'agent'],
|
|
101
101
|
description: 'Return the current handoff result',
|
|
102
102
|
sources: ['agent-tool'],
|
|
103
|
+
}, {
|
|
104
|
+
id: 'ec.handoff.recover',
|
|
105
|
+
category: 'write-agent',
|
|
106
|
+
dangerous: true,
|
|
107
|
+
defaultScopes: ['agent'],
|
|
108
|
+
description: 'Recover an open handoff after its target session changed',
|
|
109
|
+
sources: ['agent-tool'],
|
|
103
110
|
}];
|
|
104
111
|
const QUEUE_OPERATIONS = [['read', 'Read the current Agent message queue'], ['clear', 'Clear the current Agent message queue'], ['cancel', 'Cancel a current Agent queued message'], ['interrupt', 'Interrupt a current Agent session']].map(([kind, description]) => ({
|
|
105
112
|
id: `ec.queue.${kind}`,
|
|
@@ -446,7 +453,7 @@ const OPERATIONS = [
|
|
|
446
453
|
dangerous: false,
|
|
447
454
|
defaultScopes: ['agent'],
|
|
448
455
|
description: 'Assign a role to a private or group relation',
|
|
449
|
-
sources: ['menu', 'ecweb', 'control'],
|
|
456
|
+
sources: ['menu', 'menu.cli', 'ecweb', 'control', 'agent-tool'],
|
|
450
457
|
},
|
|
451
458
|
{
|
|
452
459
|
id: 'role.revoke',
|
|
@@ -454,7 +461,7 @@ const OPERATIONS = [
|
|
|
454
461
|
dangerous: false,
|
|
455
462
|
defaultScopes: ['agent'],
|
|
456
463
|
description: 'Revoke a private or group relation role assignment',
|
|
457
|
-
sources: ['menu', 'ecweb', 'control'],
|
|
464
|
+
sources: ['menu', 'menu.cli', 'ecweb', 'control', 'agent-tool'],
|
|
458
465
|
},
|
|
459
466
|
{
|
|
460
467
|
id: 'role.policy.read',
|
|
@@ -470,7 +477,7 @@ const OPERATIONS = [
|
|
|
470
477
|
dangerous: false,
|
|
471
478
|
defaultScopes: ['agent'],
|
|
472
479
|
description: 'Read private and group relation role assignments',
|
|
473
|
-
sources: ['menu', 'ecweb', 'control'],
|
|
480
|
+
sources: ['menu', 'menu.cli', 'ecweb', 'control', 'agent-tool'],
|
|
474
481
|
},
|
|
475
482
|
{
|
|
476
483
|
id: 'role.policy.write',
|
|
@@ -485,7 +492,7 @@ const OPERATIONS = [
|
|
|
485
492
|
category: 'read',
|
|
486
493
|
dangerous: false,
|
|
487
494
|
defaultScopes: ['agent'],
|
|
488
|
-
description: 'Read
|
|
495
|
+
description: 'Read Agent contacts, pending requests, and blocked contacts',
|
|
489
496
|
sources: ['agent-tool'],
|
|
490
497
|
},
|
|
491
498
|
{
|
|
@@ -504,6 +511,14 @@ const OPERATIONS = [
|
|
|
504
511
|
description: 'Block or unblock a canonical contact across bound private channels',
|
|
505
512
|
sources: ['agent-tool'],
|
|
506
513
|
},
|
|
514
|
+
{
|
|
515
|
+
id: 'contact.review',
|
|
516
|
+
category: 'write-agent',
|
|
517
|
+
dangerous: false,
|
|
518
|
+
defaultScopes: ['agent'],
|
|
519
|
+
description: 'Review pending Agent contact requests',
|
|
520
|
+
sources: ['agent-tool'],
|
|
521
|
+
},
|
|
507
522
|
{
|
|
508
523
|
id: 'connect.read',
|
|
509
524
|
category: 'read',
|
|
@@ -666,6 +681,14 @@ const OPERATIONS = [
|
|
|
666
681
|
description: '查看系统状态(敏感信息)',
|
|
667
682
|
sources: ['slash', 'menu', 'menu.cli', 'agent-tool'],
|
|
668
683
|
},
|
|
684
|
+
{
|
|
685
|
+
id: 'system.paircode',
|
|
686
|
+
category: 'process',
|
|
687
|
+
dangerous: true,
|
|
688
|
+
defaultScopes: ['process'],
|
|
689
|
+
description: '获取 daemon 远程控制配对码',
|
|
690
|
+
sources: ['menu', 'control'],
|
|
691
|
+
},
|
|
669
692
|
{
|
|
670
693
|
id: 'activity.current',
|
|
671
694
|
category: 'read',
|
|
@@ -968,6 +991,14 @@ const OPERATIONS = [
|
|
|
968
991
|
description: '通过 ec msg send 发送私聊消息',
|
|
969
992
|
sources: ['agent-tool'],
|
|
970
993
|
},
|
|
994
|
+
{
|
|
995
|
+
id: 'ec.menu.request',
|
|
996
|
+
category: 'write-agent',
|
|
997
|
+
dangerous: false,
|
|
998
|
+
defaultScopes: ['relation'],
|
|
999
|
+
description: 'Send an authenticated remote Control Menu request through the current Agent daemon channel',
|
|
1000
|
+
sources: ['agent-tool'],
|
|
1001
|
+
},
|
|
971
1002
|
{
|
|
972
1003
|
id: 'ec.msg.file',
|
|
973
1004
|
category: 'write-own',
|
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { resolvePaths } from '../../paths.js';
|
|
5
5
|
export const REMOTE_ACCESS_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
6
|
+
export const DIAGNOSTIC_TOKEN_TTL_MS = 60 * 60 * 1000;
|
|
6
7
|
// 与 owner 绑定二维码(BindService)的默认有效期保持一致:10 分钟。
|
|
7
8
|
export const REMOTE_ACCESS_PAIRING_TTL_MS = 10 * 60 * 1000;
|
|
8
9
|
export function remoteAccessTokenStorePath() {
|
|
@@ -42,7 +43,8 @@ function pruneExpired(store, now) {
|
|
|
42
43
|
const before = store.tokens.length;
|
|
43
44
|
store.tokens = store.tokens.filter(record => typeof record?.token === 'string'
|
|
44
45
|
&& typeof record.lastActive === 'number'
|
|
45
|
-
&&
|
|
46
|
+
&& Number.isFinite(record.lastActive)
|
|
47
|
+
&& now - record.lastActive < (record.diagnostic ? DIAGNOSTIC_TOKEN_TTL_MS : REMOTE_ACCESS_TOKEN_TTL_MS));
|
|
46
48
|
return store.tokens.length !== before;
|
|
47
49
|
}
|
|
48
50
|
function validLabel(value) {
|
|
@@ -85,6 +87,43 @@ export class RemoteAccessService {
|
|
|
85
87
|
pairingCode = '';
|
|
86
88
|
pairingExpiresAt = 0;
|
|
87
89
|
pairingAuthorizedBy;
|
|
90
|
+
/** Synchronous reservation: concurrent invitations cannot issue two grants. */
|
|
91
|
+
issueDiagnosticToken(controlAid, agentAid, ownerAid, now = Date.now()) {
|
|
92
|
+
const store = loadStore();
|
|
93
|
+
pruneExpired(store, now);
|
|
94
|
+
if (store.tokens.some(record => record.diagnostic?.controlAid === controlAid)) {
|
|
95
|
+
throw Object.assign(new Error('本机已有有效诊断授权,请先执行 /diag stop 或 ec diag stop。'), { code: 'DIAG_ALREADY_ACTIVE' });
|
|
96
|
+
}
|
|
97
|
+
const record = {
|
|
98
|
+
token: crypto.randomBytes(32).toString('hex'), createdAt: now, lastActive: now,
|
|
99
|
+
label: 'diagnostic', authorizedBy: ownerAid,
|
|
100
|
+
diagnostic: { controlAid, agentAid, ownerAid, caseId: crypto.randomUUID() },
|
|
101
|
+
};
|
|
102
|
+
store.tokens.push(record);
|
|
103
|
+
saveStore(store);
|
|
104
|
+
return record;
|
|
105
|
+
}
|
|
106
|
+
getDiagnosticToken(controlAid, now = Date.now()) {
|
|
107
|
+
const store = loadStore();
|
|
108
|
+
if (pruneExpired(store, now))
|
|
109
|
+
saveStore(store);
|
|
110
|
+
return store.tokens.find(record => record.diagnostic?.controlAid === controlAid);
|
|
111
|
+
}
|
|
112
|
+
/** Expected token prevents a failed older send from revoking a newer grant. */
|
|
113
|
+
revokeDiagnosticToken(controlAid, expectedToken) {
|
|
114
|
+
const store = loadStore();
|
|
115
|
+
const pruned = pruneExpired(store, Date.now());
|
|
116
|
+
const index = store.tokens.findIndex(record => record.diagnostic?.controlAid === controlAid
|
|
117
|
+
&& (expectedToken === undefined || record.token === expectedToken));
|
|
118
|
+
if (index < 0) {
|
|
119
|
+
if (pruned)
|
|
120
|
+
saveStore(store);
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const [record] = store.tokens.splice(index, 1);
|
|
124
|
+
saveStore(store);
|
|
125
|
+
return record;
|
|
126
|
+
}
|
|
88
127
|
getPairingCode(authorizedBy, now = Date.now()) {
|
|
89
128
|
if (!this.pairingCode || now >= this.pairingExpiresAt) {
|
|
90
129
|
this.pairingCode = String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');
|