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.
Files changed (56) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/agents/codex-runner.js +28 -5
  3. package/dist/agents/ecagent-runner.js +4 -0
  4. package/dist/agents/gemini-runner.js +4 -0
  5. package/dist/aun/aid/store.js +0 -81
  6. package/dist/aun/group/fs-download.js +1 -10
  7. package/dist/aun/msg/managed-operation.js +130 -32
  8. package/dist/aun/service-proxy.js +12 -18
  9. package/dist/aun/storage/index.js +29 -0
  10. package/dist/channels/aun.js +10 -135
  11. package/dist/cli/agent-command.js +6 -7
  12. package/dist/cli/agent-create.js +43 -24
  13. package/dist/cli/agent.js +15 -69
  14. package/dist/cli/aun-commands.js +149 -5
  15. package/dist/cli/ctl-command.js +1 -2
  16. package/dist/cli/daemon-commands.js +1 -1
  17. package/dist/cli/diag.js +2 -2
  18. package/dist/cli/handoff-command.js +15 -5
  19. package/dist/cli/memory-command.js +85 -11
  20. package/dist/cli/model.js +14 -138
  21. package/dist/cli/queue-command.js +46 -0
  22. package/dist/cli/response.js +5 -54
  23. package/dist/cli/skill-command.js +9 -3
  24. package/dist/cli/stats.js +10 -4
  25. package/dist/cli/task-command.js +1 -0
  26. package/dist/cli/task-context.js +15 -0
  27. package/dist/cli/trigger-command.js +73 -49
  28. package/dist/config/builtin-roles.js +8 -7
  29. package/dist/core/agent-ipc-registration.js +65 -22
  30. package/dist/core/auth/agent-delegation.js +17 -1
  31. package/dist/core/auth/delegation-authorization.js +1 -0
  32. package/dist/core/auth/operation-catalog.js +11 -3
  33. package/dist/core/command/command-handler.js +164 -21
  34. package/dist/core/command/diag-service.js +5 -1
  35. package/dist/core/handoff/ipc-registration.js +38 -0
  36. package/dist/core/ipc-registration.js +71 -7
  37. package/dist/core/message/aun-ipc-sender.js +48 -4
  38. package/dist/core/message/message-queue.js +131 -3
  39. package/dist/core/message/response-engine.js +30 -2
  40. package/dist/core/permission/sandbox-runtime.js +14 -1
  41. package/dist/core/task/task-recovery-service.js +2 -0
  42. package/dist/core/task/task-resume-operation.js +11 -0
  43. package/dist/index.js +2 -2
  44. package/dist/ipc.js +146 -8
  45. package/dist/trigger/authorization.js +2 -1
  46. package/dist/trigger/ipc-service.js +49 -35
  47. package/dist/trigger/script-sandbox.js +58 -77
  48. package/kits/docs/AUTHORING.md +1 -1
  49. package/kits/docs/channels/aun.md +8 -0
  50. package/kits/docs/evolcore/INDEX.md +2 -2
  51. package/kits/docs/evolcore/task.md +2 -0
  52. package/kits/docs/evolcore/trigger.md +2 -2
  53. package/kits/rules/01-overview.md +0 -9
  54. package/kits/rules/06-channel.md +7 -1
  55. package/kits/templates/system-fragments/commands.md +1 -1
  56. 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 = [];
@@ -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 anyClient = client;
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 group path');
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 groupId;
814
+ let targetAid;
815
815
  const remotePath = value.slice(separator + 1);
816
+ const rawId = value.slice(0, separator).trim();
816
817
  try {
817
- groupId = normalizeGroupAid(value.slice(0, separator).trim());
818
+ targetAid = normalizeGroupAid(rawId);
818
819
  }
819
820
  catch (error) {
820
- return failFs('INVALID_GROUP', `invalid group AID: ${error.message}`);
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 { groupId, remotePath: '' };
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 group path must be a bounded absolute path');
829
+ return failFs('INVALID_PATH', 'remote storage path must be a bounded absolute path');
827
830
  }
828
- return { groupId, remotePath };
831
+ return { targetAid, remotePath };
829
832
  }
830
- /** Parse the daemon-owned, current-group filesystem grammar. */
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 (!('groupId' in remote))
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, groupId: remote.groupId, remotePath: remote.remotePath,
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 (!('groupId' in remote))
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, groupId: remote.groupId, remotePath: remote.remotePath,
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 (!('groupId' in destination))
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 && !('groupId' in source))
945
+ if (source && !('targetAid' in source))
943
946
  return source;
944
- if (source && source.groupId !== destination.groupId)
945
- return failFs('INVALID_GROUP', 'managed cp cannot cross groups');
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, groupId: destination.groupId, remotePath: destination.remotePath, destinationPath: destination.remotePath,
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 (!('groupId' in source))
959
+ if (!('targetAid' in source))
957
960
  return source;
958
- if (!('groupId' in destination))
961
+ if (!('targetAid' in destination))
959
962
  return destination;
960
- if (source.groupId !== destination.groupId)
961
- return failFs('INVALID_GROUP', 'managed mv cannot cross groups');
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, groupId: destination.groupId, remotePath: destination.remotePath, destinationPath: destination.remotePath,
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 (!('groupId' in remote))
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, groupId: remote.groupId, remotePath: remote.remotePath, parents: booleanFlags.has('-p') || booleanFlags.has('--parents'),
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, groupId: remote.groupId, remotePath: remote.remotePath, recursive, overwrite,
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, groupId: remote.groupId, remotePath: remote.remotePath, maxBytes, headBytes,
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
- const ref = `${command.groupId}:${command.remotePath}`;
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 withClient(command.actorAid, async (client) => {
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.groupId}:${command.remoteSourcePath}`;
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.groupId}:${command.remoteSourcePath}`;
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.groupId}:`, usage: await client.group.fs.df(`${command.groupId}:`) };
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()/._tokenStore 都读 channel 当前 client。
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 || typeof client.authenticate !== 'function') {
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
- const client = controlChannel.getClient();
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
- // ServiceProxyClient._resolveCachedAccessToken 依次读取 _identity / _auth /
159
- // _tokenStore + deviceId 来复用 AUNChannel 已签发的 access_token。控制 AID 在
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 _auth() {
167
- return controlChannel.getClient()?._auth;
160
+ get access_token() {
161
+ return controlChannel.getClient()?.getAccessToken() ?? null;
168
162
  },
169
- get _deviceId() {
170
- return controlChannel.getClient()?._deviceId;
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
+ }