evolcore 0.0.31 → 0.0.32
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 +11 -0
- package/dist/agents/codex-runner.js +28 -5
- package/dist/agents/ecagent-runner.js +4 -0
- package/dist/agents/gemini-runner.js +4 -0
- package/dist/aun/aid/store.js +0 -81
- package/dist/aun/group/fs-download.js +1 -10
- package/dist/aun/msg/managed-operation.js +130 -32
- package/dist/aun/service-proxy.js +12 -18
- package/dist/aun/storage/index.js +29 -0
- package/dist/channels/aun.js +10 -135
- package/dist/cli/agent-command.js +6 -7
- package/dist/cli/agent-create.js +43 -24
- package/dist/cli/agent.js +15 -69
- package/dist/cli/aun-commands.js +149 -5
- package/dist/cli/ctl-command.js +1 -2
- package/dist/cli/daemon-commands.js +1 -1
- package/dist/cli/diag.js +2 -2
- package/dist/cli/handoff-command.js +15 -5
- package/dist/cli/memory-command.js +85 -11
- package/dist/cli/model.js +14 -138
- package/dist/cli/queue-command.js +46 -0
- package/dist/cli/response.js +5 -54
- package/dist/cli/skill-command.js +9 -3
- package/dist/cli/stats.js +10 -4
- package/dist/cli/task-command.js +1 -0
- package/dist/cli/task-context.js +15 -0
- package/dist/cli/trigger-command.js +73 -49
- package/dist/config/builtin-roles.js +8 -7
- package/dist/core/agent-ipc-registration.js +65 -22
- package/dist/core/auth/agent-delegation.js +17 -1
- package/dist/core/auth/delegation-authorization.js +1 -0
- package/dist/core/auth/operation-catalog.js +11 -3
- package/dist/core/command/command-handler.js +164 -21
- package/dist/core/command/diag-service.js +5 -1
- package/dist/core/handoff/ipc-registration.js +38 -0
- package/dist/core/ipc-registration.js +71 -7
- package/dist/core/message/aun-ipc-sender.js +48 -4
- package/dist/core/message/message-queue.js +131 -3
- package/dist/core/message/response-engine.js +30 -2
- package/dist/core/permission/sandbox-runtime.js +14 -1
- package/dist/core/task/task-recovery-service.js +2 -0
- package/dist/core/task/task-resume-operation.js +11 -0
- package/dist/index.js +2 -2
- package/dist/ipc.js +146 -8
- package/dist/trigger/authorization.js +2 -1
- package/dist/trigger/ipc-service.js +49 -35
- package/dist/trigger/script-sandbox.js +58 -77
- package/kits/docs/AUTHORING.md +1 -1
- package/kits/docs/channels/aun.md +8 -0
- package/kits/docs/evolcore/INDEX.md +2 -2
- package/kits/docs/evolcore/task.md +2 -0
- package/kits/docs/evolcore/trigger.md +2 -2
- package/kits/rules/01-overview.md +0 -9
- package/kits/rules/06-channel.md +7 -1
- package/kits/templates/system-fragments/commands.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.0.32 (2026-10-12)
|
|
9
|
+
|
|
10
|
+
- 统一受管命令的原始参数与审批哈希校验,按会话项目目录解析 Agent 初始化、人格和头像文件,强化授权后的文件访问边界。
|
|
11
|
+
- 将 Agent 初始化等待、AUN 发送、Storage、Trigger、handoff、queue、task 和 memory 操作统一纳入 daemon 授权与参数校验。
|
|
12
|
+
- 改进 Codex、EC Agent 和 Gemini 输入提交回执,避免已提交输入重复入队,并完善中断时的未提交输入处理。
|
|
13
|
+
- 移除 AUN SDK 私有游标、分页和事件补丁,改用 SDK 公共能力并记录缺口;群文件下载和 Service Proxy 改用公开 token 与身份接口。
|
|
14
|
+
- 强化 Trigger 脚本会话权限与 H/L 路径保护,保留 Git、SSH 和代理环境;完善 `/restart` 与 EC Web 重启的数据迁移处理。
|
|
15
|
+
- 支持 Owner 通过受管 rename 更新并签名发布 AID 名片显示名称,并修复 Agent welcome 参数中 `--help` / `-h` 的解析。
|
|
16
|
+
- 修正受管 `ec fs` 的存储范围:Agent owner 可在私聊或本地会话访问自身 personal storage,群文件仍严格限制为当前群,并兼容经授权的 fullaccess Trigger。
|
|
17
|
+
- 强制受管文件操作先解析并确认 `targetAid` 与 `storageBackend`,拒绝未解析目标;fullaccess 跨群目标必须通过已验证的群名片,执行身份不能通过 `--as` 切换。
|
|
18
|
+
|
|
8
19
|
## 0.0.31 (2026-10-11)
|
|
9
20
|
|
|
10
21
|
- 修复 `/restart` 和 ECWeb 系统重启在存在待迁移数据时提前终止的问题,统一由 `restart-monitor` 自动预检、停机迁移、严格校验、归档并在成功后启动。
|
|
@@ -1954,6 +1954,8 @@ export class CodexRunner {
|
|
|
1954
1954
|
openToolCalls: new Map(),
|
|
1955
1955
|
activeSubagentThreads: new Set(),
|
|
1956
1956
|
turnCompleted: false,
|
|
1957
|
+
inputId: turn?.inputId,
|
|
1958
|
+
inputAcceptedEmitted: false,
|
|
1957
1959
|
};
|
|
1958
1960
|
const unsubscribe = appServer.onNotification(notification => {
|
|
1959
1961
|
// 仅从 turn/started 锁定权威 turnId — resume 时会有上一轮 turn 的残留通知
|
|
@@ -4088,18 +4090,32 @@ export class CodexRunner {
|
|
|
4088
4090
|
const requiresSandboxExpansionApproval = hasFileChangeExpansion
|
|
4089
4091
|
|| hasFilesystemPermissions
|
|
4090
4092
|
|| hasUnknownPermissions;
|
|
4093
|
+
const resolveRuntimeApproval = (decision) => {
|
|
4094
|
+
if (decision === 'deny' && permissionContext?.approvalInteractionPolicy === 'deny') {
|
|
4095
|
+
return {
|
|
4096
|
+
decision: 'deny',
|
|
4097
|
+
decisionSource: 'policy',
|
|
4098
|
+
policyCode: 'permission_mode_denied',
|
|
4099
|
+
reason: 'permission mode denied runtime approval',
|
|
4100
|
+
};
|
|
4101
|
+
}
|
|
4102
|
+
return decision;
|
|
4103
|
+
};
|
|
4091
4104
|
if (requiresSandboxExpansionApproval) {
|
|
4092
|
-
if (!this.permissionGateway || !permissionPrompt)
|
|
4093
|
-
return 'deny'
|
|
4105
|
+
if (!this.permissionGateway || !permissionPrompt) {
|
|
4106
|
+
return permissionContext?.approvalInteractionPolicy === 'deny'
|
|
4107
|
+
? resolveRuntimeApproval('deny')
|
|
4108
|
+
: 'deny';
|
|
4109
|
+
}
|
|
4094
4110
|
if (policyToolName === 'FileChange') {
|
|
4095
|
-
return this.permissionGateway.requestPermission(sessionKey, policyToolName, policyInput, permissionPrompt, this.permissionContexts.get(sessionKey), summary, reason || 'Codex 请求扩大当前命令或 turn 的文件系统权限', grantScope, undefined, { fileChangeCwd: workspacePath });
|
|
4111
|
+
return resolveRuntimeApproval(await this.permissionGateway.requestPermission(sessionKey, policyToolName, policyInput, permissionPrompt, this.permissionContexts.get(sessionKey), summary, reason || 'Codex 请求扩大当前命令或 turn 的文件系统权限', grantScope, undefined, { fileChangeCwd: workspacePath }));
|
|
4096
4112
|
}
|
|
4097
|
-
return this.permissionGateway.requestPermission(sessionKey, toolName, checkedInput, permissionPrompt, this.permissionContexts.get(sessionKey), summary, reason || 'Codex 请求扩大当前命令或 turn 的文件系统权限', grantScope);
|
|
4113
|
+
return resolveRuntimeApproval(await this.permissionGateway.requestPermission(sessionKey, toolName, checkedInput, permissionPrompt, this.permissionContexts.get(sessionKey), summary, reason || 'Codex 请求扩大当前命令或 turn 的文件系统权限', grantScope));
|
|
4098
4114
|
}
|
|
4099
4115
|
if (mode === 'request' || mode === 'bypass') {
|
|
4100
4116
|
const dangerous = await requestDangerousCommandPermission(this.permissionGateway, sessionKey, toolName, checkedInput, permissionPrompt, this.permissionContexts.get(sessionKey), grantScope, mode);
|
|
4101
4117
|
if (dangerous.matched)
|
|
4102
|
-
return dangerous.decision;
|
|
4118
|
+
return resolveRuntimeApproval(dangerous.decision);
|
|
4103
4119
|
return 'allow';
|
|
4104
4120
|
}
|
|
4105
4121
|
// Network is already part of every profile, so a defensive
|
|
@@ -4657,6 +4673,13 @@ export class CodexRunner {
|
|
|
4657
4673
|
state.turnId = turnId;
|
|
4658
4674
|
this.activeTurns.set(sessionId, { threadId: state.threadId, turnId });
|
|
4659
4675
|
}
|
|
4676
|
+
// turn/start 已落地并回执 turn/started ⇒ 用户输入已被 app-server 接受。
|
|
4677
|
+
// 此后中断不能把该输入当作「未提交」重放,否则会产生重复用户轮次。
|
|
4678
|
+
// 缺少 turn lease inputId 时宁可不报,也不要用占位值造成误判。
|
|
4679
|
+
if (!state.inputAcceptedEmitted && state.inputId) {
|
|
4680
|
+
state.inputAcceptedEmitted = true;
|
|
4681
|
+
yield { type: 'input_accepted', inputId: state.inputId };
|
|
4682
|
+
}
|
|
4660
4683
|
yield { type: 'state_changed', state: 'running' };
|
|
4661
4684
|
break;
|
|
4662
4685
|
}
|
|
@@ -923,6 +923,10 @@ export class EcagentRunner {
|
|
|
923
923
|
const unsubscribe = harness.subscribe(event => this.mapHarnessEvent(queue, event, stats, model, effort));
|
|
924
924
|
queue.push({ type: 'state_changed', state: 'running' });
|
|
925
925
|
queue.push({ type: 'status', subtype: 'started', message: `ecagent is using ${modelId}` });
|
|
926
|
+
// Ecagent 输入在 harness.prompt 调用时提交,在此前报告
|
|
927
|
+
if (request.modelOverride?.turn?.inputId) {
|
|
928
|
+
queue.push({ type: 'input_accepted', inputId: request.modelOverride.turn.inputId });
|
|
929
|
+
}
|
|
926
930
|
const ecImages = request.images?.map(image => ({
|
|
927
931
|
type: 'image', data: image.data, mimeType: image.mimeType || 'image/png',
|
|
928
932
|
}));
|
|
@@ -514,6 +514,10 @@ export class GeminiRunner {
|
|
|
514
514
|
const pendingToolNames = new Map(); // toolId → toolName
|
|
515
515
|
const recordedDeniedToolIds = new Set();
|
|
516
516
|
const startTime = Date.now();
|
|
517
|
+
// Gemini 走 spawn + argv,输入在 spawn 时已提交;在第一个业务事件前报告
|
|
518
|
+
if (turn?.inputId) {
|
|
519
|
+
yield { type: 'input_accepted', inputId: turn.inputId };
|
|
520
|
+
}
|
|
517
521
|
const rl = createInterface({ input: child.stdout });
|
|
518
522
|
// Build async queue from readline
|
|
519
523
|
const queue = [];
|
package/dist/aun/aid/store.js
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { AUN_KEYSTORE_ENCRYPTION_SEED } from './encryption-seed-policy.js';
|
|
4
|
-
// fastaun uses maxPages=1 for the background tail -> forward handoff. That is
|
|
5
|
-
// useful as a latency guard, but it can leave a group backlog when no later
|
|
6
|
-
// push arrives to trigger the next page. Bound the daemon recovery pull to ten
|
|
7
|
-
// pages so a large backlog is drained without monopolizing the sync loop.
|
|
8
|
-
const FULL_GROUP_PULL_MAX_PAGES = 10;
|
|
9
|
-
const GROUP_PULL_PAGINATION_PATCH = Symbol('evolcore.groupPullPaginationPatch');
|
|
10
|
-
const RELIABLE_MESSAGE_DELIVERY_PATCH = Symbol('evolcore.reliableMessageDeliveryPatch');
|
|
11
4
|
/**
|
|
12
5
|
* Slot 命名约定(基于 fastaun 0.4.3 隔离键语义)。
|
|
13
6
|
*
|
|
@@ -77,80 +70,6 @@ export async function loadClient(store, aid) {
|
|
|
77
70
|
const { AUNClient } = await import('@agentunion/fastaun');
|
|
78
71
|
return new AUNClient(loadAid(store, aid));
|
|
79
72
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Enable bounded multi-page group pulls for the long-lived daemon client.
|
|
82
|
-
*
|
|
83
|
-
* Older SDK recovery paths used `maxPages=1`; for a daemon that can strand a
|
|
84
|
-
* backlog after a tail sync. SDK 0.5.15 normally chooses a larger recovery
|
|
85
|
-
* window, but this adapter still expands an explicit single-page request and
|
|
86
|
-
* keeps older clients safe. It is isolated to the client that opts in.
|
|
87
|
-
*/
|
|
88
|
-
export function enableFullGroupPullPagination(client) {
|
|
89
|
-
const target = client;
|
|
90
|
-
if (target[GROUP_PULL_PAGINATION_PATCH])
|
|
91
|
-
return false;
|
|
92
|
-
const pull = target._pullGroupV2;
|
|
93
|
-
if (typeof pull !== 'function')
|
|
94
|
-
return false;
|
|
95
|
-
const wrappedPull = function (groupId, afterSeq, limit, opts) {
|
|
96
|
-
const expandedOpts = opts?.maxPages === 1
|
|
97
|
-
? { ...opts, maxPages: FULL_GROUP_PULL_MAX_PAGES }
|
|
98
|
-
: opts;
|
|
99
|
-
return pull.call(this, groupId, afterSeq, limit, expandedOpts);
|
|
100
|
-
};
|
|
101
|
-
Object.defineProperty(target, GROUP_PULL_PAGINATION_PATCH, {
|
|
102
|
-
configurable: false,
|
|
103
|
-
enumerable: false,
|
|
104
|
-
value: true,
|
|
105
|
-
writable: false,
|
|
106
|
-
});
|
|
107
|
-
target._pullGroupV2 = wrappedPull;
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Make SDK pull delivery wait until EvolCore's application handlers finish.
|
|
112
|
-
*
|
|
113
|
-
* fastaun publishes message events asynchronously, while the sequence tracker
|
|
114
|
-
* is a transport watermark. A daemon restart can therefore acknowledge a
|
|
115
|
-
* pulled message before the application has durably claimed it. SDK 0.5.15
|
|
116
|
-
* split protocol and application queues; prefer its application-only flush so
|
|
117
|
-
* an unrelated protocol event cannot delay the pull commit. Older SDKs fall
|
|
118
|
-
* back to the combined flush method.
|
|
119
|
-
*/
|
|
120
|
-
export function enableReliableMessageDelivery(client) {
|
|
121
|
-
const target = client;
|
|
122
|
-
if (target[RELIABLE_MESSAGE_DELIVERY_PATCH])
|
|
123
|
-
return false;
|
|
124
|
-
const publish = target._publishAppEvent;
|
|
125
|
-
const dispatcher = target._dispatcher;
|
|
126
|
-
const flushApplication = typeof dispatcher?.flushApplication === 'function'
|
|
127
|
-
? dispatcher.flushApplication.bind(dispatcher)
|
|
128
|
-
: typeof dispatcher?.flush === 'function'
|
|
129
|
-
? dispatcher.flush.bind(dispatcher)
|
|
130
|
-
: undefined;
|
|
131
|
-
if (typeof publish !== 'function' || !flushApplication)
|
|
132
|
-
return false;
|
|
133
|
-
const wrappedPublish = async function (event, payload, source, ...rest) {
|
|
134
|
-
const result = await publish.call(this, event, payload, source, ...rest);
|
|
135
|
-
// Pull/recovery paths must not advance SeqTracker until the application
|
|
136
|
-
// handlers have completed. Push delivery has no pull commit to gate and
|
|
137
|
-
// remains asynchronous as intended by the SDK.
|
|
138
|
-
const reliable = (event === 'message.received' || event === 'group.message_created')
|
|
139
|
-
&& source !== 'push'
|
|
140
|
-
&& source !== 'inline-push';
|
|
141
|
-
if (reliable)
|
|
142
|
-
await flushApplication();
|
|
143
|
-
return result;
|
|
144
|
-
};
|
|
145
|
-
Object.defineProperty(target, RELIABLE_MESSAGE_DELIVERY_PATCH, {
|
|
146
|
-
configurable: false,
|
|
147
|
-
enumerable: false,
|
|
148
|
-
value: true,
|
|
149
|
-
writable: false,
|
|
150
|
-
});
|
|
151
|
-
target._publishAppEvent = wrappedPublish;
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
73
|
/** 加载本地 AID 值对象(用于离线签名/验签,无需连接)。load 失败抛 AidLoadError。 */
|
|
155
74
|
export function loadAid(store, aid) {
|
|
156
75
|
const r = store.load(aid);
|
|
@@ -39,16 +39,7 @@ function sha256Hex(bytes) {
|
|
|
39
39
|
return createHash('sha256').update(bytes).digest('hex');
|
|
40
40
|
}
|
|
41
41
|
function bearerHeaders(client) {
|
|
42
|
-
const
|
|
43
|
-
if (typeof anyClient.getAccessToken === 'function') {
|
|
44
|
-
const token = stringValue(anyClient.getAccessToken());
|
|
45
|
-
if (token)
|
|
46
|
-
return { Authorization: `Bearer ${token}` };
|
|
47
|
-
}
|
|
48
|
-
const token = stringValue(anyClient.accessToken)
|
|
49
|
-
|| stringValue(anyClient.access_token)
|
|
50
|
-
|| stringValue(anyClient._identity?.access_token)
|
|
51
|
-
|| stringValue(anyClient._sessionParams?.access_token);
|
|
42
|
+
const token = stringValue(client.getAccessToken());
|
|
52
43
|
return token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
53
44
|
}
|
|
54
45
|
function stringValue(value) {
|
|
@@ -807,27 +807,30 @@ function positiveInteger(value, flag, max) {
|
|
|
807
807
|
}
|
|
808
808
|
function parseRemote(value, allowEmpty) {
|
|
809
809
|
if (!value)
|
|
810
|
-
return failFs('INVALID_ARGUMENT', 'missing remote
|
|
810
|
+
return failFs('INVALID_ARGUMENT', 'missing remote storage path');
|
|
811
811
|
const separator = value.indexOf(':');
|
|
812
812
|
if (separator <= 0)
|
|
813
813
|
return failFs('INVALID_PATH', 'remote path must use <group-aid>:<absolute-path>');
|
|
814
|
-
let
|
|
814
|
+
let targetAid;
|
|
815
815
|
const remotePath = value.slice(separator + 1);
|
|
816
|
+
const rawId = value.slice(0, separator).trim();
|
|
816
817
|
try {
|
|
817
|
-
|
|
818
|
+
targetAid = normalizeGroupAid(rawId);
|
|
818
819
|
}
|
|
819
820
|
catch (error) {
|
|
820
|
-
|
|
821
|
+
if (!isValidAid(rawId))
|
|
822
|
+
return failFs('INVALID_GROUP', `invalid group AID: ${error.message}`);
|
|
823
|
+
targetAid = rawId;
|
|
821
824
|
}
|
|
822
825
|
if (remotePath === '' && allowEmpty)
|
|
823
|
-
return {
|
|
826
|
+
return { targetAid, remotePath: '' };
|
|
824
827
|
if (!remotePath.startsWith('/') || remotePath.length > MAX_REMOTE_PATH_LENGTH || /[\u0000-\u001f\u007f]/.test(remotePath)
|
|
825
828
|
|| remotePath.split('/').includes('..')) {
|
|
826
|
-
return failFs('INVALID_PATH', 'remote
|
|
829
|
+
return failFs('INVALID_PATH', 'remote storage path must be a bounded absolute path');
|
|
827
830
|
}
|
|
828
|
-
return {
|
|
831
|
+
return { targetAid, remotePath };
|
|
829
832
|
}
|
|
830
|
-
/** Parse the daemon-owned
|
|
833
|
+
/** Parse the daemon-owned managed filesystem grammar for group or self storage. */
|
|
831
834
|
export function resolveManagedFsOperation(argv) {
|
|
832
835
|
const raw = argv[0] === 'fs' ? argv.slice(1) : argv;
|
|
833
836
|
const rawKind = raw[0];
|
|
@@ -904,11 +907,11 @@ export function resolveManagedFsOperation(argv) {
|
|
|
904
907
|
if (values.has('-m') || values.has('-x'))
|
|
905
908
|
return failFs('NOT_ALLOWED', 'getfacl does not accept ACL mutation flags');
|
|
906
909
|
const remote = remoteOrError(positionals[0]);
|
|
907
|
-
if (!('
|
|
910
|
+
if (!('targetAid' in remote))
|
|
908
911
|
return remote;
|
|
909
912
|
return { ok: true, command: {
|
|
910
913
|
kind: 'getfacl', operationId: 'ec.fs.getfacl', canonicalArgv: ['fs', 'getfacl', positionals[0], ...(actorAid ? ['--as', actorAid] : [])],
|
|
911
|
-
actorAid,
|
|
914
|
+
actorAid, targetAid: remote.targetAid, remotePath: remote.remotePath,
|
|
912
915
|
} };
|
|
913
916
|
}
|
|
914
917
|
if (rawKind === 'setfacl') {
|
|
@@ -921,17 +924,17 @@ export function resolveManagedFsOperation(argv) {
|
|
|
921
924
|
if (!('grantee' in parsedAcl))
|
|
922
925
|
return parsedAcl;
|
|
923
926
|
const remote = remoteOrError(positionals[0]);
|
|
924
|
-
if (!('
|
|
927
|
+
if (!('targetAid' in remote))
|
|
925
928
|
return remote;
|
|
926
929
|
return { ok: true, command: {
|
|
927
930
|
kind: 'setfacl', operationId: 'ec.fs.setfacl', canonicalArgv: ['fs', 'setfacl', positionals[0], modify !== undefined ? '-m' : '-x', spec, ...(actorAid ? ['--as', actorAid] : [])],
|
|
928
|
-
actorAid,
|
|
931
|
+
actorAid, targetAid: remote.targetAid, remotePath: remote.remotePath,
|
|
929
932
|
aclGrantee: parsedAcl.grantee, ...(parsedAcl.perms ? { aclPerms: parsedAcl.perms } : {}), aclRemove: remove !== undefined,
|
|
930
933
|
} };
|
|
931
934
|
}
|
|
932
935
|
if (rawKind === 'cp') {
|
|
933
936
|
const destination = remoteOrError(positionals[1]);
|
|
934
|
-
if (!('
|
|
937
|
+
if (!('targetAid' in destination))
|
|
935
938
|
return destination;
|
|
936
939
|
// Windows absolute paths contain a drive-letter colon, but are local
|
|
937
940
|
// upload sources rather than managed group references.
|
|
@@ -939,46 +942,46 @@ export function resolveManagedFsOperation(argv) {
|
|
|
939
942
|
const source = positionals[0].includes(':') && !isWindowsAbsolutePath
|
|
940
943
|
? remoteOrError(positionals[0])
|
|
941
944
|
: undefined;
|
|
942
|
-
if (source && !('
|
|
945
|
+
if (source && !('targetAid' in source))
|
|
943
946
|
return source;
|
|
944
|
-
if (source && source.
|
|
945
|
-
return failFs('INVALID_GROUP', 'managed cp cannot cross
|
|
947
|
+
if (source && source.targetAid !== destination.targetAid)
|
|
948
|
+
return failFs('INVALID_GROUP', 'managed cp cannot cross targets');
|
|
946
949
|
const kind = source ? 'copy' : 'upload';
|
|
947
950
|
return { ok: true, command: {
|
|
948
951
|
kind, operationId: `ec.fs.${kind}`, canonicalArgv: ['fs', 'cp', positionals[0], positionals[1], ...(actorAid ? ['--as', actorAid] : [])],
|
|
949
|
-
actorAid,
|
|
952
|
+
actorAid, targetAid: destination.targetAid, remotePath: destination.remotePath, destinationPath: destination.remotePath,
|
|
950
953
|
...(source ? { remoteSourcePath: source.remotePath } : { localPath: positionals[0] }), overwrite, recursive,
|
|
951
954
|
} };
|
|
952
955
|
}
|
|
953
956
|
if (rawKind === 'mv') {
|
|
954
957
|
const source = remoteOrError(positionals[0]);
|
|
955
958
|
const destination = remoteOrError(positionals[1]);
|
|
956
|
-
if (!('
|
|
959
|
+
if (!('targetAid' in source))
|
|
957
960
|
return source;
|
|
958
|
-
if (!('
|
|
961
|
+
if (!('targetAid' in destination))
|
|
959
962
|
return destination;
|
|
960
|
-
if (source.
|
|
961
|
-
return failFs('INVALID_GROUP', 'managed mv cannot cross
|
|
963
|
+
if (source.targetAid !== destination.targetAid)
|
|
964
|
+
return failFs('INVALID_GROUP', 'managed mv cannot cross targets');
|
|
962
965
|
return { ok: true, command: {
|
|
963
966
|
kind: 'move', operationId: 'ec.fs.move', canonicalArgv: ['fs', 'mv', positionals[0], positionals[1], ...(actorAid ? ['--as', actorAid] : [])],
|
|
964
|
-
actorAid,
|
|
967
|
+
actorAid, targetAid: destination.targetAid, remotePath: destination.remotePath, destinationPath: destination.remotePath,
|
|
965
968
|
remoteSourcePath: source.remotePath, overwrite,
|
|
966
969
|
} };
|
|
967
970
|
}
|
|
968
971
|
const remote = remoteOrError(positionals[0], rawKind === 'df');
|
|
969
|
-
if (!('
|
|
972
|
+
if (!('targetAid' in remote))
|
|
970
973
|
return remote;
|
|
971
974
|
if (rawKind === 'mkdir')
|
|
972
975
|
return { ok: true, command: {
|
|
973
976
|
kind: 'mkdir', operationId: 'ec.fs.mkdir', canonicalArgv: ['fs', 'mkdir', positionals[0], ...(actorAid ? ['--as', actorAid] : [])],
|
|
974
|
-
actorAid,
|
|
977
|
+
actorAid, targetAid: remote.targetAid, remotePath: remote.remotePath, parents: booleanFlags.has('-p') || booleanFlags.has('--parents'),
|
|
975
978
|
} };
|
|
976
979
|
if (rawKind === 'rm' && remote.remotePath === '/')
|
|
977
980
|
return failFs('INVALID_PATH', 'managed rm cannot target the group root');
|
|
978
981
|
if (rawKind === 'rm')
|
|
979
982
|
return { ok: true, command: {
|
|
980
983
|
kind: 'remove', operationId: 'ec.fs.remove', canonicalArgv: ['fs', 'rm', positionals[0], ...(actorAid ? ['--as', actorAid] : [])],
|
|
981
|
-
actorAid,
|
|
984
|
+
actorAid, targetAid: remote.targetAid, remotePath: remote.remotePath, recursive, overwrite,
|
|
982
985
|
} };
|
|
983
986
|
const effectiveKind = rawKind === 'stat' && !follow ? 'lstat' : rawKind;
|
|
984
987
|
const maxBytes = positiveInteger(values.get('--max-bytes'), '--max-bytes', MAX_MANAGED_FS_CAT_BYTES);
|
|
@@ -998,7 +1001,7 @@ export function resolveManagedFsOperation(argv) {
|
|
|
998
1001
|
return failFs('INVALID_ARGUMENT', '--type only supports f|d|l');
|
|
999
1002
|
return { ok: true, command: {
|
|
1000
1003
|
kind: effectiveKind, operationId: `ec.fs.${effectiveKind}`, canonicalArgv: ['fs', effectiveKind, positionals[0], ...(actorAid ? ['--as', actorAid] : [])],
|
|
1001
|
-
actorAid,
|
|
1004
|
+
actorAid, targetAid: remote.targetAid, remotePath: remote.remotePath, maxBytes, headBytes,
|
|
1002
1005
|
name: values.get('--name') ?? values.get('--pattern'), nodeType, size: values.get('--size'), mtime: values.get('--mtime'), page, pageSize,
|
|
1003
1006
|
} };
|
|
1004
1007
|
}
|
|
@@ -1055,9 +1058,104 @@ async function withClient(actorAid, fn) {
|
|
|
1055
1058
|
export async function executeManagedFsOperation(command) {
|
|
1056
1059
|
if (!command.actorAid)
|
|
1057
1060
|
throw new ManagedFsOperationError('INVALID_ACTOR', 'managed fs actor is required');
|
|
1058
|
-
|
|
1061
|
+
if (!command.storageBackend)
|
|
1062
|
+
throw new ManagedFsOperationError('INVALID_TARGET', 'managed fs storage backend must be resolved before execution');
|
|
1063
|
+
return withClient(command.actorAid, client => executeManagedFsOperationWithClient(command, client));
|
|
1064
|
+
}
|
|
1065
|
+
/** Execute against an already authenticated SDK client; used by integration tests. */
|
|
1066
|
+
export async function executeManagedFsOperationWithClient(command, client) {
|
|
1067
|
+
if (!command.actorAid)
|
|
1068
|
+
throw new ManagedFsOperationError('INVALID_ACTOR', 'managed fs actor is required');
|
|
1069
|
+
if (!command.storageBackend)
|
|
1070
|
+
throw new ManagedFsOperationError('INVALID_TARGET', 'managed fs storage backend must be resolved before execution');
|
|
1071
|
+
const ref = `${command.targetAid}:${command.remotePath}`;
|
|
1059
1072
|
try {
|
|
1060
|
-
return await
|
|
1073
|
+
return await (async () => {
|
|
1074
|
+
if (command.storageBackend === 'personal') {
|
|
1075
|
+
const owner = command.targetAid;
|
|
1076
|
+
switch (command.kind) {
|
|
1077
|
+
case 'mkdir':
|
|
1078
|
+
return { ok: true, command: 'mkdir', path: ref,
|
|
1079
|
+
result: await client.storage.mkdir(command.remotePath, { owner, parents: command.parents }) };
|
|
1080
|
+
case 'upload': {
|
|
1081
|
+
if (!command.localPath)
|
|
1082
|
+
throw new ManagedFsOperationError('INVALID_SOURCE', 'managed upload source is required');
|
|
1083
|
+
return { ok: true, command: 'upload', path: ref, localPath: command.localPath,
|
|
1084
|
+
result: await client.storage.uploadFile(command.localPath, command.remotePath, { owner, overwrite: command.overwrite }) };
|
|
1085
|
+
}
|
|
1086
|
+
case 'copy': {
|
|
1087
|
+
if (!command.remoteSourcePath)
|
|
1088
|
+
throw new ManagedFsOperationError('INVALID_SOURCE', 'managed copy source is required');
|
|
1089
|
+
const source = `${owner}:${command.remoteSourcePath}`;
|
|
1090
|
+
return { ok: true, command: 'copy', src: source, path: ref,
|
|
1091
|
+
result: await client.storage.copy(command.remoteSourcePath, command.remotePath, {
|
|
1092
|
+
owner, dstOwner: owner, overwrite: command.overwrite, recursive: command.recursive,
|
|
1093
|
+
}) };
|
|
1094
|
+
}
|
|
1095
|
+
case 'move': {
|
|
1096
|
+
if (!command.remoteSourcePath)
|
|
1097
|
+
throw new ManagedFsOperationError('INVALID_SOURCE', 'managed move source is required');
|
|
1098
|
+
const source = `${owner}:${command.remoteSourcePath}`;
|
|
1099
|
+
return { ok: true, command: 'move', src: source, path: ref,
|
|
1100
|
+
result: await client.storage.rename(command.remoteSourcePath, command.remotePath, { owner, overwrite: command.overwrite }) };
|
|
1101
|
+
}
|
|
1102
|
+
case 'remove':
|
|
1103
|
+
return { ok: true, command: 'remove', path: ref,
|
|
1104
|
+
result: await client.storage.remove(command.remotePath, { owner, recursive: command.recursive }) };
|
|
1105
|
+
case 'ls':
|
|
1106
|
+
return { ok: true, command: 'ls', path: ref,
|
|
1107
|
+
items: await client.storage.list(command.remotePath || '/', { owner }) };
|
|
1108
|
+
case 'stat':
|
|
1109
|
+
return { ok: true, command: 'stat', path: ref,
|
|
1110
|
+
node: await client.storage.stat(command.remotePath, { owner }) };
|
|
1111
|
+
case 'lstat':
|
|
1112
|
+
return { ok: true, command: 'lstat', path: ref,
|
|
1113
|
+
node: await client.storage.lstat(command.remotePath, { owner }) };
|
|
1114
|
+
case 'find':
|
|
1115
|
+
return { ok: true, command: 'find', path: ref,
|
|
1116
|
+
items: await client.storage.find(command.remotePath, {
|
|
1117
|
+
owner, name: command.name, nodeType: command.nodeType, size: command.size,
|
|
1118
|
+
mtime: command.mtime, page: command.page, pageSize: command.pageSize,
|
|
1119
|
+
}) };
|
|
1120
|
+
case 'df':
|
|
1121
|
+
return { ok: true, command: 'df', path: `${owner}:`, usage: await client.storage.df({ owner }) };
|
|
1122
|
+
case 'getfacl':
|
|
1123
|
+
return { ok: true, command: 'getfacl', path: ref, result: await client.storage.listAcl(command.remotePath, { owner }) };
|
|
1124
|
+
case 'setfacl':
|
|
1125
|
+
throw new ManagedFsOperationError('NOT_ALLOWED', 'Managed personal storage ACL changes are not supported');
|
|
1126
|
+
case 'cat': {
|
|
1127
|
+
const node = await client.storage.stat(command.remotePath, { owner });
|
|
1128
|
+
const size = numberFrom(node, ['size', 'sizeBytes', 'size_bytes']);
|
|
1129
|
+
const maxBytes = Math.min(command.maxBytes ?? MAX_MANAGED_FS_CAT_BYTES, MAX_MANAGED_FS_CAT_BYTES);
|
|
1130
|
+
if (size === undefined)
|
|
1131
|
+
throw new ManagedFsOperationError('FS_SIZE_REQUIRED', 'personal file size is required before managed cat');
|
|
1132
|
+
if (size > maxBytes)
|
|
1133
|
+
throw new ManagedFsOperationError('FS_CAT_TOO_LARGE', `managed cat is limited to ${maxBytes} bytes`);
|
|
1134
|
+
const bytes = await client.storage.readBytes(command.remotePath, { owner });
|
|
1135
|
+
if (bytes.byteLength > maxBytes)
|
|
1136
|
+
throw new ManagedFsOperationError('FS_CAT_TOO_LARGE', `managed cat is limited to ${maxBytes} bytes`);
|
|
1137
|
+
const headBytes = Math.min(command.headBytes ?? 256, MAX_MANAGED_FS_HEAD_BYTES, bytes.byteLength);
|
|
1138
|
+
if (bytes.subarray(0, Math.min(bytes.byteLength, 8192)).some(byte => byte === 0)) {
|
|
1139
|
+
return {
|
|
1140
|
+
ok: true,
|
|
1141
|
+
command: 'cat',
|
|
1142
|
+
path: ref,
|
|
1143
|
+
size: bytes.byteLength,
|
|
1144
|
+
binary: true,
|
|
1145
|
+
head: { encoding: 'base64', bytes: headBytes, data: Buffer.from(bytes.subarray(0, headBytes)).toString('base64') },
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
let text;
|
|
1149
|
+
try {
|
|
1150
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
throw new ManagedFsOperationError('FS_NOT_UTF8', 'managed cat only returns UTF-8 text or a bounded binary head');
|
|
1154
|
+
}
|
|
1155
|
+
return { ok: true, command: 'cat', path: ref, size: bytes.byteLength, binary: false, text };
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1061
1159
|
switch (command.kind) {
|
|
1062
1160
|
case 'mkdir':
|
|
1063
1161
|
return { ok: true, command: 'mkdir', path: ref, result: await client.group.fs.mkdir(ref, { parents: command.parents }) };
|
|
@@ -1070,14 +1168,14 @@ export async function executeManagedFsOperation(command) {
|
|
|
1070
1168
|
case 'copy': {
|
|
1071
1169
|
if (!command.remoteSourcePath)
|
|
1072
1170
|
throw new ManagedFsOperationError('INVALID_SOURCE', 'managed copy source is required');
|
|
1073
|
-
const source = `${command.
|
|
1171
|
+
const source = `${command.targetAid}:${command.remoteSourcePath}`;
|
|
1074
1172
|
return { ok: true, command: 'copy', src: source, path: ref,
|
|
1075
1173
|
result: await client.group.fs.cp(source, ref, { recursive: command.recursive, overwrite: command.overwrite, force: command.overwrite }) };
|
|
1076
1174
|
}
|
|
1077
1175
|
case 'move': {
|
|
1078
1176
|
if (!command.remoteSourcePath)
|
|
1079
1177
|
throw new ManagedFsOperationError('INVALID_SOURCE', 'managed move source is required');
|
|
1080
|
-
const source = `${command.
|
|
1178
|
+
const source = `${command.targetAid}:${command.remoteSourcePath}`;
|
|
1081
1179
|
return { ok: true, command: 'move', src: source, path: ref,
|
|
1082
1180
|
result: await client.group.fs.mv(source, ref, { overwrite: command.overwrite, force: command.overwrite }) };
|
|
1083
1181
|
}
|
|
@@ -1109,7 +1207,7 @@ export async function executeManagedFsOperation(command) {
|
|
|
1109
1207
|
return { ok: true, command: 'find', path: ref, items };
|
|
1110
1208
|
}
|
|
1111
1209
|
case 'df':
|
|
1112
|
-
return { ok: true, command: 'df', path: `${command.
|
|
1210
|
+
return { ok: true, command: 'df', path: `${command.targetAid}:`, usage: await client.group.fs.df(`${command.targetAid}:`) };
|
|
1113
1211
|
case 'getfacl':
|
|
1114
1212
|
return { ok: true, command: 'getfacl', path: ref, result: await client.group.fs.getAcl(ref) };
|
|
1115
1213
|
case 'setfacl': {
|
|
@@ -1152,7 +1250,7 @@ export async function executeManagedFsOperation(command) {
|
|
|
1152
1250
|
return { ok: true, command: 'cat', path: ref, size: bytes.byteLength, binary: false, text };
|
|
1153
1251
|
}
|
|
1154
1252
|
}
|
|
1155
|
-
});
|
|
1253
|
+
})();
|
|
1156
1254
|
}
|
|
1157
1255
|
catch (error) {
|
|
1158
1256
|
if (error instanceof ManagedFsOperationError)
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - providerAid = 控制 AID(daemon 进程身份,pureIdentity 常驻 AUN)
|
|
10
10
|
* - aunClient 生命周期陷阱:AUNChannel 重连会销毁重建底层 client,
|
|
11
11
|
* 所以不能把 client 引用直接交给 ServiceProxyClient;传一个动态解引用的
|
|
12
|
-
* facade,每次 .call()/.authenticate()
|
|
12
|
+
* facade,每次 .call()/.authenticate()/getAccessToken() 都读 channel 当前 client。
|
|
13
13
|
* serveForever(persistent) 的指数退避会在 client 缺失时自动等待恢复。
|
|
14
14
|
* - endpoint 发现:source='instance' 时读服务实例登记文件的 port;source='static' 时使用显式 endpoint。
|
|
15
15
|
* - 失败降级:任何异常只 warn,绝不影响 daemon 主流程。
|
|
@@ -143,31 +143,25 @@ export function startServiceProxy(controlChannel, providerAid, configuredService
|
|
|
143
143
|
},
|
|
144
144
|
authenticate: () => {
|
|
145
145
|
const client = controlChannel.getClient();
|
|
146
|
-
if (!client
|
|
146
|
+
if (!client) {
|
|
147
147
|
return Promise.reject(new Error('AUN control client not connected'));
|
|
148
148
|
}
|
|
149
|
+
const accessToken = client.getAccessToken();
|
|
150
|
+
if (accessToken)
|
|
151
|
+
return Promise.resolve({ access_token: accessToken });
|
|
149
152
|
return client.authenticate();
|
|
150
153
|
},
|
|
151
154
|
on: (event, handler) => {
|
|
152
|
-
|
|
153
|
-
return client?.on?.(event, handler);
|
|
154
|
-
},
|
|
155
|
-
get _tokenStore() {
|
|
156
|
-
return controlChannel.getClient()?._tokenStore;
|
|
155
|
+
return controlChannel.getClient()?.on(event, handler);
|
|
157
156
|
},
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// connect 时已 authenticate(client 处于 ready 态),若这些字段不透出,
|
|
161
|
-
// _ensureAccessToken 会回退到 authenticate() 并抛 "authenticate not allowed
|
|
162
|
-
// in state ready",导致隧道永远连不上 proxy-server。动态解引用,规避重连换 client。
|
|
163
|
-
get _identity() {
|
|
164
|
-
return controlChannel.getClient()?._identity;
|
|
157
|
+
getAccessToken: () => {
|
|
158
|
+
return controlChannel.getClient()?.getAccessToken() ?? null;
|
|
165
159
|
},
|
|
166
|
-
get
|
|
167
|
-
return controlChannel.getClient()?.
|
|
160
|
+
get access_token() {
|
|
161
|
+
return controlChannel.getClient()?.getAccessToken() ?? null;
|
|
168
162
|
},
|
|
169
|
-
get
|
|
170
|
-
return controlChannel.getClient()?.
|
|
163
|
+
get currentAid() {
|
|
164
|
+
return controlChannel.getClient()?.currentAid ?? null;
|
|
171
165
|
},
|
|
172
166
|
};
|
|
173
167
|
// endpoint 白名单:只允许回连本机回环(127.0.0.1 / localhost 默认放行)。
|
|
@@ -1,2 +1,31 @@
|
|
|
1
1
|
export { storageUpload, storageUploadBuffer, storageLs, storageRm, storageQuota } from './client.js';
|
|
2
2
|
export { storageDownload } from './download.js';
|
|
3
|
+
import { isValidAid } from '../aid/validation.js';
|
|
4
|
+
export function resolveStorageCommandArgv(argv) {
|
|
5
|
+
if (argv[0] !== 'storage')
|
|
6
|
+
throw new Error('Expected a storage command');
|
|
7
|
+
const positionals = [];
|
|
8
|
+
let isPublic = false;
|
|
9
|
+
for (let index = 1; index < argv.length; index++) {
|
|
10
|
+
const value = argv[index];
|
|
11
|
+
if (value === '--public')
|
|
12
|
+
isPublic = true;
|
|
13
|
+
else if (value === '--format' && argv[index + 1] === 'json')
|
|
14
|
+
index++;
|
|
15
|
+
else if (value.startsWith('--'))
|
|
16
|
+
throw new Error(`Unsupported managed storage option: ${value}`);
|
|
17
|
+
else
|
|
18
|
+
positionals.push(value);
|
|
19
|
+
}
|
|
20
|
+
const [sub, aid, source, destination] = positionals;
|
|
21
|
+
const operation = { upload: 'storage.upload', download: 'storage.download', ls: 'storage.list', rm: 'storage.delete', quota: 'storage.quota' }[sub];
|
|
22
|
+
if (!operation || !aid || !isValidAid(aid))
|
|
23
|
+
throw new Error('Invalid storage command or AID');
|
|
24
|
+
const counts = { upload: [4], download: [3, 4], ls: [2, 3], rm: [3], quota: [2] };
|
|
25
|
+
if (!counts[sub].includes(positionals.length) || (isPublic && sub !== 'upload'))
|
|
26
|
+
throw new Error('Invalid storage arguments');
|
|
27
|
+
return { sub, aid, operation, isPublic, localFile: sub === 'upload' ? source : undefined,
|
|
28
|
+
remotePath: sub === 'upload' ? destination : sub === 'rm' ? source : undefined,
|
|
29
|
+
url: sub === 'download' ? source : undefined, localPath: sub === 'download' ? destination : undefined,
|
|
30
|
+
prefix: sub === 'ls' ? source ?? '' : undefined };
|
|
31
|
+
}
|