evolcore 0.0.26 → 0.0.28

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 (177) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/baseagent.js +10 -5
  5. package/dist/agents/claude-runner.js +123 -22
  6. package/dist/agents/codex/app-server-client.js +70 -26
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex-runner.js +363 -421
  9. package/dist/agents/ecagent-runner.js +170 -25
  10. package/dist/agents/gemini-runner.js +6 -0
  11. package/dist/agents/runner-types.js +50 -2
  12. package/dist/aun/aid/agentmd.js +73 -31
  13. package/dist/aun/aid/client.js +1 -1
  14. package/dist/aun/aid/control-aid.js +180 -1
  15. package/dist/aun/aid/identity.js +3 -3
  16. package/dist/aun/group/history.js +303 -0
  17. package/dist/aun/group/identity.js +43 -0
  18. package/dist/aun/group/index.js +5 -0
  19. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  20. package/dist/aun/{menu-client.js → menu/client.js} +13 -7
  21. package/dist/aun/menu/index.js +2 -0
  22. package/dist/aun/menu/transport.js +49 -0
  23. package/dist/aun/msg/index.js +2 -1
  24. package/dist/aun/msg/managed-operation.js +84 -18
  25. package/dist/aun/msg/mention-schema.js +51 -0
  26. package/dist/aun/msg/p2p.js +13 -8
  27. package/dist/aun/msg/upload.js +43 -88
  28. package/dist/aun/outbox.js +29 -7
  29. package/dist/aun/rpc/client.js +1 -0
  30. package/dist/aun/storage/client.js +39 -50
  31. package/dist/channels/aun.js +544 -239
  32. package/dist/channels/daemon.js +10 -6
  33. package/dist/cli/agent-command.js +108 -6
  34. package/dist/cli/agent-create.js +515 -2
  35. package/dist/cli/agent.js +109 -8
  36. package/dist/cli/aun-commands.js +191 -42
  37. package/dist/cli/config.js +8 -5
  38. package/dist/cli/contact.js +2 -2
  39. package/dist/cli/ctl-command.js +2 -2
  40. package/dist/cli/daemon-commands.js +111 -162
  41. package/dist/cli/diag.js +3 -3
  42. package/dist/cli/fs-command.js +3 -3
  43. package/dist/cli/handoff-command.js +6 -6
  44. package/dist/cli/index.js +8 -2
  45. package/dist/cli/init.js +34 -1
  46. package/dist/cli/managed-command-guard.js +2 -1
  47. package/dist/cli/menu-client.js +13 -5
  48. package/dist/cli/model.js +3 -3
  49. package/dist/cli/queue-command.js +2 -2
  50. package/dist/cli/response.js +2 -2
  51. package/dist/cli/restart-monitor.js +81 -19
  52. package/dist/cli/task-command.js +59 -0
  53. package/dist/cli/task-context.js +97 -49
  54. package/dist/cli/trigger-command.js +20 -10
  55. package/dist/config/builtin-roles.js +10 -0
  56. package/dist/config/config-field-policy.js +11 -0
  57. package/dist/config/config-manager.js +32 -4
  58. package/dist/config/config-operation-service.js +20 -5
  59. package/dist/config/contact-book-store.js +1 -1
  60. package/dist/config/contact-book.js +1 -1
  61. package/dist/config/contact-operation-service.js +1 -1
  62. package/dist/config/peer-role-resolver.js +1 -1
  63. package/dist/config/resolved-config-op.js +3 -0
  64. package/dist/config-store.js +1 -1
  65. package/dist/core/agent-application-service.js +5 -0
  66. package/dist/core/audit/event-key.js +17 -2
  67. package/dist/core/audit/log-integrity.js +34 -8
  68. package/dist/core/auth/agent-delegation.js +48 -14
  69. package/dist/core/auth/auth-gateway.js +2 -0
  70. package/dist/core/auth/authorization-audit.js +34 -38
  71. package/dist/core/auth/operation-authorizer.js +67 -5
  72. package/dist/core/auth/operation-catalog.js +110 -0
  73. package/dist/core/auth/remote-access-store.js +115 -9
  74. package/dist/core/bootstrap-messages.js +66 -7
  75. package/dist/core/bootstrap-service.js +110 -19
  76. package/dist/core/command/agent-control.js +88 -7
  77. package/dist/core/command/agent-storage-menu.js +443 -0
  78. package/dist/core/command/cli-intent-parser.js +19 -0
  79. package/dist/core/command/command-handler.js +105 -40
  80. package/dist/core/command/data-root-resolver.js +273 -0
  81. package/dist/core/command/diag-service.js +292 -27
  82. package/dist/core/command/group-menu.js +7 -2
  83. package/dist/core/command/menu-catalog.js +34 -4
  84. package/dist/core/command/menu-handler.js +364 -96
  85. package/dist/core/command/menu-protocol.js +43 -6
  86. package/dist/core/command/role-menu.js +2 -2
  87. package/dist/core/command/slash-handler.js +61 -37
  88. package/dist/core/event-catalog.js +4 -0
  89. package/dist/core/evolagent-registry.js +28 -2
  90. package/dist/core/group/invite-service.js +924 -0
  91. package/dist/core/interaction-router.js +22 -8
  92. package/dist/core/message/im-renderer.js +94 -16
  93. package/dist/core/message/message-bridge.js +140 -13
  94. package/dist/core/message/message-log.js +5 -3
  95. package/dist/core/message/message-queue.js +73 -41
  96. package/dist/core/message/response-engine.js +729 -118
  97. package/dist/core/permission/approval-gateway.js +36 -3
  98. package/dist/core/permission/ec-command-parser.js +76 -36
  99. package/dist/core/permission/protected-paths.js +134 -16
  100. package/dist/core/permission/sandbox-runtime.js +72 -3
  101. package/dist/core/permission/tool-error-code.js +15 -0
  102. package/dist/core/permission/tool-policy.js +8 -8
  103. package/dist/core/relation/peer-identity.js +57 -6
  104. package/dist/core/relation/shared-data.js +106 -0
  105. package/dist/core/role/runtime-policy.js +64 -0
  106. package/dist/core/runtime-lock.js +10 -26
  107. package/dist/core/session/runner-task-history.js +170 -0
  108. package/dist/core/session/session-manager.js +38 -6
  109. package/dist/core/session/session-mapper.js +41 -0
  110. package/dist/core/session/session-turn-coordinator.js +37 -7
  111. package/dist/core/session/session-turns.js +48 -7
  112. package/dist/core/task/task-recovery-service.js +176 -0
  113. package/dist/core/task/task-resume-operation.js +32 -0
  114. package/dist/eck/group-rules-sync.js +5 -3
  115. package/dist/eck/kit-renderer.js +1 -0
  116. package/dist/index.js +322 -207
  117. package/dist/ipc.js +141 -15
  118. package/dist/paths.js +17 -4
  119. package/dist/product.js +3 -0
  120. package/dist/trigger/event-source.js +19 -0
  121. package/dist/trigger/feedback.js +233 -40
  122. package/dist/trigger/parser.js +55 -5
  123. package/dist/trigger/patch.js +9 -0
  124. package/dist/trigger/scheduler.js +30 -4
  125. package/dist/trigger/validation.js +20 -3
  126. package/dist/utils/atomic-write.js +98 -0
  127. package/dist/utils/cross-platform.js +87 -16
  128. package/dist/utils/ecweb-utils.js +1 -0
  129. package/dist/utils/error-utils.js +40 -2
  130. package/dist/utils/logger.js +23 -13
  131. package/dist/utils/npm-ops.js +119 -4
  132. package/dist/utils/package-version-info.js +244 -0
  133. package/dist/utils/tool-summary.js +59 -3
  134. package/dist/utils/welcome.js +174 -1
  135. package/dist/utils/windows-shell-trust.js +37 -0
  136. package/ecagent/dist/harness/agent-harness.js +3 -0
  137. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  138. package/kits/docs/INDEX.md +0 -6
  139. package/kits/docs/channels/aun.md +23 -2
  140. package/kits/docs/channels/feishu.md +1 -1
  141. package/kits/docs/context-assembly.md +29 -17
  142. package/kits/docs/evolcore/INDEX.md +8 -3
  143. package/kits/docs/evolcore/agent-create.md +39 -7
  144. package/kits/docs/evolcore/agent.md +33 -1
  145. package/kits/docs/evolcore/aid.md +9 -1
  146. package/kits/docs/evolcore/config.md +40 -1
  147. package/kits/docs/evolcore/group-collaboration.md +1 -1
  148. package/kits/docs/evolcore/group.md +117 -2
  149. package/kits/docs/evolcore/menu-client.md +16 -0
  150. package/kits/docs/evolcore/msg.md +3 -0
  151. package/kits/docs/evolcore/storage.md +3 -0
  152. package/kits/docs/evolcore/task.md +14 -0
  153. package/kits/docs/evolcore/trigger.md +51 -3
  154. package/kits/docs/venues/group.md +5 -0
  155. package/kits/eck_manifest.json +27 -0
  156. package/kits/rules/01-overview.md +11 -7
  157. package/kits/rules/04-relation.md +2 -1
  158. package/kits/rules/05-venue.md +1 -1
  159. package/kits/rules/06-channel.md +6 -4
  160. package/kits/schemas/agent-config.schema.12.json +25 -1
  161. package/kits/schemas/daemon.schema.6.json +2 -2
  162. package/kits/schemas/defaults.schema.5.json +1 -1
  163. package/kits/schemas/relation-config.schema.1.json +1 -1
  164. package/kits/schemas/relation-config.schema.5.json +1 -1
  165. package/kits/schemas/role-config.schema.1.json +14 -0
  166. package/kits/templates/roles/member.json +5 -0
  167. package/kits/templates/roles/visitor.json +5 -0
  168. package/kits/templates/system-fragments/baseagent.md +1 -1
  169. package/kits/templates/system-fragments/bootstrap.md +26 -9
  170. package/kits/templates/system-fragments/channel.md +8 -44
  171. package/kits/templates/system-fragments/commands.md +10 -2
  172. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  173. package/kits/templates/system-fragments/reply.md +56 -0
  174. package/package.json +7 -3
  175. package/dist/aun/group-identity.js +0 -9
  176. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  177. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -0,0 +1,443 @@
1
+ import crypto from 'node:crypto';
2
+ import { isValidAid } from '../../aun/aid/validation.js';
3
+ import { isExplicitGroupId } from '../../aun/group/index.js';
4
+ import { listStaticAgentOwners } from '../../config/peer-role-resolver.js';
5
+ import { authorizeOperation } from '../auth/auth-gateway.js';
6
+ import { auditCommandAuthorization } from '../auth/authorization-audit.js';
7
+ import { DataRootError } from './data-root-resolver.js';
8
+ const ROOT_TTL_MS = 10 * 60_000;
9
+ const OPERATIONS = {
10
+ root: 'file.root.storage', list: 'file.storage.list', query: 'file.storage.query',
11
+ fetch: 'file.storage.fetch', quota: 'file.storage.quota',
12
+ };
13
+ function record(value) {
14
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
15
+ }
16
+ function fail(code, message) { throw new DataRootError(code, message); }
17
+ function numeric(value) {
18
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
19
+ }
20
+ function timestamp(value) {
21
+ const n = numeric(value);
22
+ return n === null ? null : n < 10_000_000_000 ? n * 1000 : n;
23
+ }
24
+ function textValue(value) { return typeof value === 'string' && value ? value : null; }
25
+ function nodeType(node) {
26
+ // Fail closed if an upstream compatibility view calls a linked node a file.
27
+ for (const type of [node.type, node.node_type]) {
28
+ if (type === 'symlink' || type === 'mount')
29
+ return type;
30
+ }
31
+ const type = node.type ?? node.node_type;
32
+ if (type === 'dir' || type === 'folder' || type === 'directory')
33
+ return 'dir';
34
+ if (type === 'file' || type === 'object')
35
+ return 'file';
36
+ return type === 'symlink' || type === 'mount' ? type : 'unknown';
37
+ }
38
+ function command(request) {
39
+ return typeof request.cmd === 'string' && request.cmd.trim()
40
+ ? request.cmd.trim().split(/\s+/)[0] : `/${request.name ?? ''}`;
41
+ }
42
+ export function isAgentStorageMenuRequest(request) {
43
+ const cmd = command(request);
44
+ return cmd === '/agent-storage-root' || cmd === '/storage'
45
+ || (cmd === '/file' && typeof record(request.args).rootId === 'string'
46
+ && record(request.args).rootId.startsWith('sr1.'));
47
+ }
48
+ export function normalizeStorageRelativePath(value) {
49
+ if (typeof value !== 'string' || !value.trim() || value.length > 4096) {
50
+ return fail('INVALID_ROOT_PATH', 'A root-relative storage path is required');
51
+ }
52
+ const raw = value.trim();
53
+ // Do not let the SDK normalize traversal, Windows paths or encoded separators away.
54
+ if (/^[\/]/.test(raw) || /[\\:\0%]/.test(raw) || raw.split('/').includes('..')) {
55
+ return fail('INVALID_ROOT_PATH', 'Storage paths must be relative and cannot contain traversal or an AID');
56
+ }
57
+ const segments = raw.split('/').filter(part => part && part !== '.');
58
+ if (segments.length > 64)
59
+ return fail('INVALID_ROOT_PATH', 'Storage path is too deep');
60
+ return segments.join('/') || '.';
61
+ }
62
+ export class AgentStorageMenuService {
63
+ secret;
64
+ now;
65
+ ttlMs;
66
+ rpcTimeoutMs;
67
+ isOwner;
68
+ authorize;
69
+ inflight = new Map();
70
+ constructor(options = {}) {
71
+ this.secret = options.secret ?? crypto.randomBytes(32);
72
+ this.now = options.now ?? Date.now;
73
+ this.ttlMs = options.ttlMs ?? ROOT_TTL_MS;
74
+ this.rpcTimeoutMs = options.rpcTimeoutMs ?? 15_000;
75
+ this.isOwner = options.isOwner ?? ((aid, principalId) => listStaticAgentOwners(aid, { fresh: true }).includes(principalId));
76
+ this.authorize = options.authorize ?? authorizeOperation;
77
+ }
78
+ /** Also run before Menu deduplication, so replay cannot bypass revocation/expiry. */
79
+ preflight(request, subject, audit = true) {
80
+ let operation = 'file.root.storage';
81
+ try {
82
+ const cmd = command(request);
83
+ const args = record(request.args);
84
+ const method = cmd === '/agent-storage-root' && request.type === 'menu.query' ? 'root'
85
+ : cmd === '/storage' && request.type === 'menu.query' && args.view === 'quota' ? 'quota'
86
+ : cmd === '/file' && request.type === 'menu.query' ? 'query'
87
+ : cmd === '/file' && request.type === 'menu.action' && (request.action === 'list' || request.action === 'fetch')
88
+ ? request.action : fail('NOT_SUPPORTED', 'Unsupported Agent storage operation');
89
+ operation = OPERATIONS[method];
90
+ const targetAid = subject.selfAid ?? '';
91
+ if (subject.fromControlChannel || subject.channelType !== 'aun' || subject.chatType !== 'private') {
92
+ fail('NOT_SUPPORTED', 'Use the target Agent AUN private channel for storage');
93
+ }
94
+ if (!subject.menuEncrypted)
95
+ fail('MENU_TOKEN_ENCRYPTION_REQUIRED', 'Agent storage Menu requests require E2EE');
96
+ if (!isValidAid(targetAid) || isExplicitGroupId(targetAid)
97
+ || !isValidAid(subject.principalId ?? '') || isExplicitGroupId(subject.principalId ?? '')
98
+ || subject.role !== 'owner' || !this.isOwner(targetAid, subject.principalId)) {
99
+ fail('ROOT_FORBIDDEN', 'Agent cloud storage requires the static Agent owner');
100
+ }
101
+ const allowedKeys = method === 'root' ? [] : method === 'quota' ? ['rootId', 'view']
102
+ : method === 'list' ? ['rootId', 'path', 'cursor', 'limit', 'includeHidden', 'offset'] : ['rootId', 'path'];
103
+ if (request.agent !== undefined
104
+ || (request.args !== undefined && (!request.args || Array.isArray(request.args) || typeof request.args !== 'object'))
105
+ || Object.keys(args).some(key => !allowedKeys.includes(key))) {
106
+ fail('INVALID_ARGUMENT', 'Unsupported Agent storage arguments');
107
+ }
108
+ const root = method === 'root' ? undefined : this.resolveRoot(args.rootId, subject);
109
+ if (method === 'list' || method === 'query' || method === 'fetch') {
110
+ const path = normalizeStorageRelativePath(args.path ?? (method === 'list' ? '.' : undefined));
111
+ if (method === 'list')
112
+ this.listOptions(args, path);
113
+ }
114
+ const decision = this.authorize({
115
+ source: 'menu', subject, audit: false,
116
+ intent: { operation, scope: 'agent', source: 'menu', args: { self: targetAid } },
117
+ });
118
+ if (!decision.allow)
119
+ fail('PERMISSION_DENIED', 'Agent storage operation is not allowed by the current policy');
120
+ if (audit)
121
+ this.audit(subject, operation, 'allow', 'authorized');
122
+ return { method, operation, args, targetAid, root };
123
+ }
124
+ catch (error) {
125
+ if (audit)
126
+ this.audit(subject, operation, 'deny', 'blocked', error);
127
+ throw error;
128
+ }
129
+ }
130
+ async execute(request, subject, resolveClient) {
131
+ const authorized = this.preflight(request, subject);
132
+ let relativePath;
133
+ try {
134
+ const client = resolveClient?.(subject.channel, authorized.targetAid);
135
+ if (!client || client.aid !== authorized.targetAid)
136
+ fail('TEMPORARILY_UNAVAILABLE', 'Target Agent storage connection is unavailable');
137
+ const { method, args, targetAid, root } = authorized;
138
+ let data;
139
+ if (method === 'root' || method === 'quota') {
140
+ const quota = await this.quota(client, targetAid);
141
+ if (method === 'quota')
142
+ data = { root: this.summary(root), backend: 'aun-storage', ...quota };
143
+ else {
144
+ const claims = {
145
+ v: 1, kind: 'agent-storage', targetAid, principalId: subject.principalId,
146
+ storageOwnerAid: targetAid, bucket: 'default', expiresAt: this.now() + this.ttlMs,
147
+ nonce: crypto.randomBytes(12).toString('base64url'),
148
+ };
149
+ data = {
150
+ ...this.summary(claims), rootId: this.sign('sr1', claims), backend: 'aun-storage',
151
+ storageOwnerAid: targetAid, bucket: 'default', virtualPath: `${targetAid}:/`, readOnly: true,
152
+ capabilities: { list: true, query: true, fetch: true, quota: true }, quota,
153
+ };
154
+ }
155
+ }
156
+ else {
157
+ relativePath = normalizeStorageRelativePath(args.path ?? (method === 'list' ? '.' : undefined));
158
+ const node = await this.nativePath(client, targetAid, relativePath);
159
+ if (method === 'list') {
160
+ if (nodeType(node) !== 'dir')
161
+ fail('NOT_A_DIRECTORY', 'Storage path is not a directory');
162
+ data = await this.list(client, root, relativePath, args);
163
+ }
164
+ else if (method === 'query') {
165
+ if (nodeType(node) === 'file') {
166
+ const head = await this.rpc(client, targetAid, 'storage.head_object', { object_key: relativePath });
167
+ this.assertOwnedNode(head, targetAid);
168
+ data = this.metadata({ ...head, type: 'file' }, relativePath);
169
+ }
170
+ else
171
+ data = this.metadata(node, relativePath);
172
+ }
173
+ else {
174
+ if (nodeType(node) !== 'file')
175
+ fail('NOT_SUPPORTED', 'Only ordinary storage files can be downloaded');
176
+ data = await this.fetch(client, root, relativePath);
177
+ }
178
+ data = { ...data, backend: 'aun-storage', root: this.summary(root) };
179
+ }
180
+ // An owner or root can be revoked while an RPC is in flight.
181
+ this.preflight(request, subject, false);
182
+ this.audit(subject, authorized.operation, 'allow', 'completed', undefined, relativePath);
183
+ return data;
184
+ }
185
+ catch (error) {
186
+ this.audit(subject, authorized.operation, 'allow', 'failed', error, relativePath);
187
+ throw error;
188
+ }
189
+ }
190
+ resolveRoot(value, subject) {
191
+ if (value === undefined)
192
+ fail('MISSING_ROOT_ID', 'A storage rootId is required');
193
+ const claims = this.verify('sr1', value, 'INVALID_ROOT_ID');
194
+ if (claims.v !== 1 || claims.kind !== 'agent-storage' || claims.bucket !== 'default'
195
+ || typeof claims.targetAid !== 'string' || claims.targetAid !== claims.storageOwnerAid
196
+ || typeof claims.principalId !== 'string' || !Number.isFinite(claims.expiresAt)) {
197
+ return fail('INVALID_ROOT_ID', 'Invalid storage root claims');
198
+ }
199
+ if (claims.targetAid !== subject.selfAid)
200
+ fail('ROOT_SCOPE_MISMATCH', 'Storage root belongs to another Agent');
201
+ if (claims.principalId !== subject.principalId)
202
+ fail('ROOT_FORBIDDEN', 'Storage root belongs to another principal');
203
+ if (claims.expiresAt <= this.now())
204
+ fail('ROOT_EXPIRED', 'Storage root expired; query the root again');
205
+ return claims;
206
+ }
207
+ sign(prefix, value) {
208
+ const payload = Buffer.from(JSON.stringify(value)).toString('base64url');
209
+ const signature = crypto.createHmac('sha256', this.secret).update(`${prefix}.${payload}`).digest('base64url');
210
+ return `${prefix}.${payload}.${signature}`;
211
+ }
212
+ verify(prefix, value, code) {
213
+ if (typeof value !== 'string' || value.length > 16_384)
214
+ return fail(code, 'Invalid storage credential');
215
+ const parts = value.split('.');
216
+ if (parts.length !== 3 || parts[0] !== prefix || !/^[\w-]+$/.test(parts[2]))
217
+ return fail(code, 'Invalid storage credential');
218
+ const expected = crypto.createHmac('sha256', this.secret).update(`${prefix}.${parts[1]}`).digest();
219
+ const actual = Buffer.from(parts[2], 'base64url');
220
+ if (actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected))
221
+ return fail(code, 'Invalid storage credential signature');
222
+ try {
223
+ const parsed = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
224
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
225
+ throw new Error();
226
+ return parsed;
227
+ }
228
+ catch {
229
+ return fail(code, 'Invalid storage credential payload');
230
+ }
231
+ }
232
+ summary(root) {
233
+ return { scope: 'agent-storage', targetAid: root.targetAid, expiresAt: root.expiresAt };
234
+ }
235
+ async quota(client, targetAid) {
236
+ const raw = await this.rpc(client, targetAid, 'storage.get_quota', {});
237
+ this.assertOwnedNode(raw, targetAid);
238
+ const usedBytes = numeric(raw.used_bytes ?? raw.quota_used_bytes);
239
+ const quotaBytes = numeric(raw.quota_bytes ?? raw.quota_total_bytes);
240
+ return {
241
+ quotaBytes: quotaBytes === 0 ? null : quotaBytes, usedBytes,
242
+ availableBytes: quotaBytes && usedBytes !== null ? Math.max(0, quotaBytes - usedBytes) : null,
243
+ objectCount: numeric(raw.object_count),
244
+ };
245
+ }
246
+ assertOwnedNode(raw, targetAid) {
247
+ const owner = raw.owner_aid ?? raw.owner ?? raw.owner_principal;
248
+ if ((typeof owner === 'string' && owner !== targetAid) || (raw.bucket !== undefined && raw.bucket !== 'default')) {
249
+ fail('ROOT_SCOPE_MISMATCH', 'Storage node is outside the Agent storage root');
250
+ }
251
+ }
252
+ async nativePath(client, targetAid, relativePath) {
253
+ // lstat does not follow the final link. Check every prefix, not only the leaf.
254
+ const parts = relativePath === '.' ? [] : relativePath.split('/');
255
+ let node = {};
256
+ for (let index = 0; index <= parts.length; index++) {
257
+ const raw = await this.rpc(client, targetAid, 'storage.fs.lstat', { path: parts.slice(0, index).join('/') });
258
+ node = record(raw.node ?? raw);
259
+ this.assertOwnedNode(node, targetAid);
260
+ const type = nodeType(node);
261
+ if (type === 'mount' || type === 'symlink' || type === 'unknown') {
262
+ fail('NOT_SUPPORTED', 'Storage links and mounts are display-only');
263
+ }
264
+ if (index < parts.length && type !== 'dir')
265
+ fail('NOT_A_DIRECTORY', 'Storage ancestor is not a directory');
266
+ }
267
+ return node;
268
+ }
269
+ metadata(node, relativePath) {
270
+ const type = nodeType(node);
271
+ return {
272
+ path: relativePath, type: type === 'dir' ? 'directory' : type, nodeType: type,
273
+ navigable: type === 'dir', fetchable: type === 'file',
274
+ size: type === 'file' ? numeric(node.size ?? node.size_bytes) : null,
275
+ mtime: timestamp(node.mtime ?? node.updated_at),
276
+ contentType: textValue(node.content_type), sha256: textValue(node.sha256),
277
+ etag: textValue(node.etag), version: numeric(node.version),
278
+ isPublic: typeof node.is_private === 'boolean' ? !node.is_private : null,
279
+ };
280
+ }
281
+ listOptions(args, relativePath) {
282
+ const limit = args.limit ?? 100;
283
+ if (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1 || limit > 200
284
+ || (args.offset !== undefined && args.offset !== 0)
285
+ || (args.includeHidden !== undefined && typeof args.includeHidden !== 'boolean')) {
286
+ return fail('INVALID_ARGUMENT', 'Storage list requires limit 1..200, offset 0 and a boolean includeHidden');
287
+ }
288
+ const includeHidden = args.includeHidden === true;
289
+ const rootHash = crypto.createHash('sha256').update(String(args.rootId)).digest('hex');
290
+ let page = 1;
291
+ let marker;
292
+ if (args.cursor !== undefined && args.cursor !== null) {
293
+ const cursor = this.verify('sc1', args.cursor, 'INVALID_ARGUMENT');
294
+ if (cursor.rootHash !== rootHash || cursor.path !== relativePath || cursor.limit !== limit
295
+ || cursor.includeHidden !== includeHidden || !Number.isSafeInteger(cursor.page) || cursor.page < 1
296
+ || (cursor.marker !== undefined && typeof cursor.marker !== 'string')) {
297
+ fail('INVALID_ARGUMENT', 'Storage cursor does not match this directory request');
298
+ }
299
+ ({ page, marker } = cursor);
300
+ }
301
+ return { rootHash, path: relativePath, limit, includeHidden, page, marker };
302
+ }
303
+ async list(client, root, relativePath, args) {
304
+ const { rootHash, limit, includeHidden, page, marker } = this.listOptions(args, relativePath);
305
+ const raw = await this.rpc(client, root.targetAid, 'storage.fs.list', {
306
+ path: relativePath === '.' ? '' : relativePath, page, size: limit, ...(marker ? { marker } : {}),
307
+ });
308
+ this.assertOwnedNode(raw, root.targetAid);
309
+ const items = raw.nodes ?? raw.items;
310
+ if (!Array.isArray(items))
311
+ fail('INVALID_RESPONSE', 'Storage list did not return nodes');
312
+ const entries = items.map(value => {
313
+ const node = record(value);
314
+ const name = textValue(node.name);
315
+ if (!name || name === '.' || name === '..' || /[\/\\:\0%]/.test(name)) {
316
+ return fail('INVALID_RESPONSE', 'Storage list returned an invalid entry name');
317
+ }
318
+ // Links/mounts may describe a foreign source; return only their local name and type.
319
+ const type = nodeType(node);
320
+ const native = type === 'dir' || type === 'file';
321
+ if (native)
322
+ this.assertOwnedNode(node, root.targetAid);
323
+ const entryPath = relativePath === '.' ? name : `${relativePath}/${name}`;
324
+ return { name, ...this.metadata(native ? node : { type }, entryPath) };
325
+ }).filter(entry => includeHidden || !entry.name.startsWith('.'));
326
+ const nextMarker = textValue(raw.next_marker);
327
+ const total = numeric(raw.total);
328
+ const size = numeric(raw.size) || limit;
329
+ const markerAbsent = raw.next_marker === undefined || raw.next_marker === null;
330
+ if ((raw.next_marker != null && typeof raw.next_marker !== 'string')
331
+ || (nextMarker && nextMarker.length > 8000)
332
+ || (raw.total != null && (total === null || !Number.isSafeInteger(total)))
333
+ || (raw.size != null && (!Number.isSafeInteger(raw.size) || Number(raw.size) < 1 || Number(raw.size) > limit))
334
+ || (raw.page != null && raw.page !== page)
335
+ || items.length > size) {
336
+ fail('INVALID_RESPONSE', 'Storage returned invalid pagination metadata');
337
+ }
338
+ const hasMore = !!nextMarker || (markerAbsent && total !== null && page * size < total);
339
+ if (nextMarker && nextMarker === marker)
340
+ fail('INVALID_RESPONSE', 'Storage returned a non-advancing cursor');
341
+ if (markerAbsent && total === null && items.length >= size) {
342
+ fail('INVALID_RESPONSE', 'Storage omitted pagination metadata');
343
+ }
344
+ return {
345
+ path: relativePath, entries, limit, total: includeHidden ? total : null, hasMore,
346
+ nextCursor: hasMore ? this.sign('sc1', {
347
+ rootHash, path: relativePath, limit, includeHidden, page: page + 1,
348
+ ...(nextMarker ? { marker: nextMarker } : {}),
349
+ }) : null,
350
+ };
351
+ }
352
+ async fetch(client, root, relativePath) {
353
+ // Head and ticket address native objects in this owner's bucket, not another VFS owner.
354
+ const head = await this.rpc(client, root.targetAid, 'storage.head_object', { object_key: relativePath });
355
+ this.assertOwnedNode(head, root.targetAid);
356
+ const requestedAt = this.now();
357
+ const remaining = Math.floor((root.expiresAt - requestedAt) / 1000);
358
+ if (remaining < 1)
359
+ fail('ROOT_EXPIRED', 'Storage root expired; query the root again');
360
+ const ttl = Math.min(60, remaining);
361
+ const ticket = await this.rpc(client, root.targetAid, 'storage.create_download_ticket', {
362
+ object_key: relativePath, expire_in_seconds: ttl,
363
+ });
364
+ this.assertOwnedNode(ticket, root.targetAid);
365
+ const urlValue = textValue(ticket.url ?? ticket.download_url);
366
+ let url;
367
+ try {
368
+ url = new URL(urlValue ?? '');
369
+ }
370
+ catch {
371
+ return fail('INVALID_RESPONSE', 'Storage did not return a valid download URL');
372
+ }
373
+ if (url.protocol !== 'https:' || url.username || url.password)
374
+ fail('INVALID_RESPONSE', 'Storage download requires HTTPS');
375
+ const isPublic = head.is_private === false && !url.searchParams.has('t');
376
+ const expiresAt = isPublic ? null : timestamp(ticket.expire_at);
377
+ if (!isPublic && (!url.searchParams.get('t') || expiresAt === null || expiresAt <= this.now()
378
+ || expiresAt > root.expiresAt || expiresAt > requestedAt + ttl * 1000 + 1000)) {
379
+ fail('DEPENDENCY_UNAVAILABLE', 'Storage did not issue a valid short-lived private download ticket');
380
+ }
381
+ return {
382
+ ...this.metadata({ ...head, ...ticket, type: 'file' }, relativePath),
383
+ isPublic,
384
+ download: { mode: 'url', method: 'GET', url: urlValue, expiresAt },
385
+ };
386
+ }
387
+ async rpc(client, targetAid, method, args) {
388
+ if ((this.inflight.get(targetAid) ?? 0) >= 4)
389
+ fail('RATE_LIMITED', 'Too many Agent storage requests');
390
+ this.inflight.set(targetAid, (this.inflight.get(targetAid) ?? 0) + 1);
391
+ let timer;
392
+ const pending = Promise.resolve().then(() => client.call(method, {
393
+ ...args, owner_aid: targetAid,
394
+ // get_quota is AID-wide and has no bucket parameter in the RPC contract.
395
+ ...(method === 'storage.get_quota' ? {} : { bucket: 'default' }),
396
+ }))
397
+ .finally(() => {
398
+ const count = (this.inflight.get(targetAid) ?? 1) - 1;
399
+ if (count)
400
+ this.inflight.set(targetAid, count);
401
+ else
402
+ this.inflight.delete(targetAid);
403
+ });
404
+ try {
405
+ const value = await Promise.race([pending, new Promise((_, reject) => {
406
+ timer = setTimeout(() => reject(new DataRootError('EXECUTION_TIMEOUT', 'Agent storage request timed out')), this.rpcTimeoutMs);
407
+ })]);
408
+ if (!value || typeof value !== 'object' || Array.isArray(value))
409
+ fail('INVALID_RESPONSE', 'Storage returned an invalid response');
410
+ return value;
411
+ }
412
+ catch (error) {
413
+ if (error instanceof DataRootError)
414
+ throw error;
415
+ const raw = record(error);
416
+ const codes = {
417
+ ENOENT: 'NOT_FOUND', '-32008': 'NOT_FOUND', '404': 'NOT_FOUND', '4040': 'NOT_FOUND',
418
+ EACCES: 'PERMISSION_DENIED', '-32004': 'PERMISSION_DENIED', '403': 'PERMISSION_DENIED', '4030': 'PERMISSION_DENIED',
419
+ ENOTDIR: 'NOT_A_DIRECTORY', EISDIR: 'NOT_SUPPORTED', ELOOP: 'NOT_SUPPORTED', EDANGLING: 'NOT_SUPPORTED',
420
+ ECONFLICT: 'CONFLICT', '-32009': 'CONFLICT', ETIMEDOUT: 'EXECUTION_TIMEOUT', TIMEOUT: 'EXECUTION_TIMEOUT',
421
+ '-32601': 'NOT_SUPPORTED', '-32602': 'INVALID_ARGUMENT',
422
+ };
423
+ // Never forward upstream URLs, tokens or server internals in exception messages.
424
+ return fail(codes[String(raw.code)] ?? 'TEMPORARILY_UNAVAILABLE', 'Agent storage request failed');
425
+ }
426
+ finally {
427
+ if (timer)
428
+ clearTimeout(timer);
429
+ }
430
+ }
431
+ audit(subject, operation, decision, executionState, error, relativePath) {
432
+ auditCommandAuthorization({
433
+ ts: this.now(), source: 'menu', scope: 'agent', dangerous: false, operation, decision, executionState,
434
+ eventPhase: executionState === 'authorized' || executionState === 'blocked' ? 'preflight' : 'final',
435
+ executed: executionState === 'completed', actorId: subject.principalId, selfAid: subject.selfAid,
436
+ agentAid: subject.selfAid, requestId: subject.requestId, sessionId: subject.sessionId,
437
+ channel: subject.channel, channelId: subject.channelId, role: subject.role,
438
+ errorCode: error instanceof DataRootError ? error.code : error ? 'INTERNAL_ERROR' : undefined,
439
+ ...(relativePath ? { argsSummary: { path: relativePath } } : {}),
440
+ });
441
+ }
442
+ }
443
+ export const agentStorageMenuService = new AgentStorageMenuService();
@@ -1,3 +1,4 @@
1
+ import { parseAgentWelcomeArgs, parseInitializationArgs } from '../../cli/agent-create.js';
1
2
  import { normalizeCliArgv } from '../../cli/cli-argv.js';
2
3
  import { resolveConfigCommand, } from '../../config/resolved-config-op.js';
3
4
  export { normalizeCliArgv, parseLegacyCliCommand, validateCliArgv } from '../../cli/cli-argv.js';
@@ -156,6 +157,15 @@ function parseStatsCommand(argv, source) {
156
157
  }
157
158
  function parseAgentCommand(argv, source) {
158
159
  const subcmd = argv[1];
160
+ if ((subcmd === 'new' && argv.includes('--initialize')) || subcmd === 'initialize') {
161
+ try {
162
+ const parsed = parseInitializationArgs(argv.slice(1));
163
+ return recognized('agent.initialize', 'control', source, { aid: parsed.aid }, argv, true);
164
+ }
165
+ catch {
166
+ return raw(source, argv, 'Invalid initialization arguments');
167
+ }
168
+ }
159
169
  if (subcmd === 'persona') {
160
170
  const tail = argv.at(-2) === '--format' && argv.at(-1) === 'json' ? argv.slice(0, -2) : argv;
161
171
  const isShow = tail.length === 3 && tail[2] === 'show';
@@ -164,6 +174,15 @@ function parseAgentCommand(argv, source) {
164
174
  ? recognized('agent.persona', 'agent', source, {}, argv)
165
175
  : raw(source, argv, 'Invalid agent persona command');
166
176
  }
177
+ if (subcmd === 'welcome') {
178
+ try {
179
+ const { action } = parseAgentWelcomeArgs(argv.slice(2));
180
+ return recognized('agent.welcome', 'agent', source, { action }, argv);
181
+ }
182
+ catch {
183
+ return raw(source, argv, 'Invalid agent welcome command');
184
+ }
185
+ }
167
186
  const parsed = parseArgs(argv.slice(2));
168
187
  const args = parsed.args;
169
188
  if (parsed.positionals.length > 0)