@wowyuarm/dsh-agent-team 0.1.5 → 0.1.6
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/README.md +15 -1
- package/README.zh.md +15 -1
- package/package.json +7 -2
- package/packages/agent-team/README.md +6 -0
- package/packages/agent-team/README.zh.md +6 -0
- package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +41 -0
- package/packages/agent-team/core-skills/member-skill-manager/references/auth-and-config.md +30 -0
- package/packages/agent-team/core-skills/member-skill-manager/references/writing-great-skills.md +17 -0
- package/packages/agent-team/lib/index.js +255 -2
- package/packages/agent-team/lib/ledger.js +139 -5
- package/packages/agent-team/lib/member-context.js +2 -2
- package/packages/agent-team/lib/member-skills.js +94 -0
- package/packages/agent-team/lib/spec.js +26 -0
- package/packages/agent-team/lib/typert.host.js +131 -28
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +99 -23
- package/packages/agent-team/lib/types/index.d.ts +69 -1
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +29 -1
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-skills.d.ts +45 -0
- package/packages/agent-team/lib/types/member-skills.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +87 -2
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +7 -4
- package/packages/client-agent-team/README.md +1 -1
- package/packages/client-agent-team/README.zh.md +1 -1
- package/packages/client-agent-team/lib/client.js +217 -299
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -4
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +11 -23
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +4 -8
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +7 -40
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +61 -20
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +1 -1
- package/packages/client-agent-team/lib/types/client/locales.d.ts +4 -16
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +4 -16
- package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts +5 -7
- package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/timeline-scroll.js +9 -22
- package/packages/tool-agent-team/lib/index.js +25 -5
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
|
@@ -23,6 +23,18 @@ function emptyProjection() {
|
|
|
23
23
|
function assertUnhandledKind(operation) {
|
|
24
24
|
throw new Error(`agent-team ledger does not handle operation kind '${operation.kind}'`);
|
|
25
25
|
}
|
|
26
|
+
/** Deep-freeze a Member capability overlay; absent stays absent. */
|
|
27
|
+
function freezeCapabilities(capabilities) {
|
|
28
|
+
if (capabilities === undefined)
|
|
29
|
+
return {};
|
|
30
|
+
const freezeAllow = (allow) => allow === undefined ? {} : { allow: Object.freeze([...allow]) };
|
|
31
|
+
return {
|
|
32
|
+
capabilities: Object.freeze({
|
|
33
|
+
...(capabilities.tools === undefined ? {} : { tools: Object.freeze(freezeAllow(capabilities.tools.allow)) }),
|
|
34
|
+
...(capabilities.skills === undefined ? {} : { skills: Object.freeze(freezeAllow(capabilities.skills.allow)) }),
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
26
38
|
/** Replay and append logic behind the Agent Team service interface. */
|
|
27
39
|
export class AgentTeamLedger {
|
|
28
40
|
table;
|
|
@@ -141,9 +153,11 @@ export class AgentTeamLedger {
|
|
|
141
153
|
this.requireChannel(request.workspaceId, channelRef);
|
|
142
154
|
this.assertHandleAvailable(request.workspaceId, handle);
|
|
143
155
|
this.assertModelSelection(request.member.model);
|
|
156
|
+
this.assertCapabilities(request.member.capabilities);
|
|
144
157
|
const member = Object.freeze({
|
|
145
158
|
...request.member, handle, description, presetId, state: 'enabled',
|
|
146
159
|
...(request.member.model === undefined ? {} : { model: Object.freeze({ ...request.member.model }) }),
|
|
160
|
+
...freezeCapabilities(request.member.capabilities),
|
|
147
161
|
});
|
|
148
162
|
const operation = Object.freeze({
|
|
149
163
|
...this.operationBase(request, this.nextSequence()), kind: 'team/member-added',
|
|
@@ -173,12 +187,15 @@ export class AgentTeamLedger {
|
|
|
173
187
|
if (handle !== prior.handle)
|
|
174
188
|
this.assertHandleAvailable(prior.workspaceId, handle, prior.memberId);
|
|
175
189
|
this.assertModelSelection(request.model);
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
190
|
+
this.assertCapabilities(request.capabilities);
|
|
191
|
+
// An absent model or capabilities field must CLEAR any override
|
|
192
|
+
// (inherit the Host default / full standard capability surface);
|
|
193
|
+
// spreading `prior` verbatim would silently keep the pinned value.
|
|
194
|
+
const { model: _priorModel, capabilities: _priorCapabilities, ...priorWithoutOverlays } = prior;
|
|
179
195
|
const member = Object.freeze({
|
|
180
|
-
...
|
|
196
|
+
...priorWithoutOverlays, handle, description,
|
|
181
197
|
...(request.model === undefined ? {} : { model: Object.freeze({ ...request.model }) }),
|
|
198
|
+
...freezeCapabilities(request.capabilities),
|
|
182
199
|
});
|
|
183
200
|
const operation = Object.freeze({
|
|
184
201
|
...this.operationBase(request, this.nextSequence()), kind: 'team/member-updated',
|
|
@@ -623,6 +640,74 @@ export class AgentTeamLedger {
|
|
|
623
640
|
return this.committed(this.attentionResult(operation));
|
|
624
641
|
});
|
|
625
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Append one Member-to-Member direct message as an audit-only operation.
|
|
645
|
+
* A DM is pure delivery: no Channel, Thread, revision, attention, or
|
|
646
|
+
* markers change, so apply() is a marker and no projection state moves.
|
|
647
|
+
*/
|
|
648
|
+
sendDm(request) {
|
|
649
|
+
return this.enqueue(async () => {
|
|
650
|
+
const existing = this.state.byRequest.get(request.requestId);
|
|
651
|
+
if (existing !== undefined) {
|
|
652
|
+
this.assertSameDm(existing, request);
|
|
653
|
+
return this.resolved(this.dmResult(existing));
|
|
654
|
+
}
|
|
655
|
+
// Only an enabled Member in this Workspace may send a DM; the Human is
|
|
656
|
+
// not a sendable peer, and Members in other Workspaces are unreachable.
|
|
657
|
+
const sender = this.assertActorForWorkspace(request.actor, request.workspaceId);
|
|
658
|
+
if (sender.kind !== 'member')
|
|
659
|
+
throw new Error('agent-team DM requires Member authority');
|
|
660
|
+
const body = request.body.trim();
|
|
661
|
+
if (body === '')
|
|
662
|
+
throw new Error('DM body must not be empty');
|
|
663
|
+
const recipient = this.requireMember(request.recipientMemberId);
|
|
664
|
+
if (recipient.workspaceId !== request.workspaceId)
|
|
665
|
+
throw new Error(`Agent Member '${recipient.memberId}' is not in Workspace '${request.workspaceId}'`);
|
|
666
|
+
if (recipient.state !== 'enabled')
|
|
667
|
+
throw new Error(`Agent Member '${recipient.memberId}' is ${recipient.state}; DM delivery requires an enabled Member`);
|
|
668
|
+
if (recipient.memberId === AGENT_TEAM_HUMAN_MEMBER_ID || !recipient.sessionId)
|
|
669
|
+
throw new Error('DM recipient must be an Agent Member');
|
|
670
|
+
if (recipient.memberId === sender.memberId)
|
|
671
|
+
throw new Error('Members cannot DM themselves');
|
|
672
|
+
const operation = Object.freeze({
|
|
673
|
+
...this.operationBase(request, this.nextSequence()), kind: 'team/dm-sent',
|
|
674
|
+
data: Object.freeze({ workspaceId: request.workspaceId, senderMemberId: sender.memberId,
|
|
675
|
+
recipientMemberId: recipient.memberId, body }),
|
|
676
|
+
});
|
|
677
|
+
await this.table.put(operation.operationId, operation);
|
|
678
|
+
this.apply(operation);
|
|
679
|
+
return this.committed(this.dmResult(operation));
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Bounded adjacent context for a DM relay: the truncated body of the most
|
|
684
|
+
* recent earlier DM between the two sessions, newest first. Returns
|
|
685
|
+
* undefined when this is their first exchange. `excluding` skips the
|
|
686
|
+
* operation id of the DM being delivered right now, so the context line is
|
|
687
|
+
* the adjacent prior exchange rather than a self-reference. Direction
|
|
688
|
+
* labels are from the reader's perspective: the reader is the recipient of
|
|
689
|
+
* the DM being delivered, so messages the reader sent are `you → them`.
|
|
690
|
+
*/
|
|
691
|
+
dmHistoryBetween(senderSessionId, recipientMemberId, excluding) {
|
|
692
|
+
const sender = [...this.state.members.values()].find(member => member.sessionId === senderSessionId);
|
|
693
|
+
if (sender === undefined)
|
|
694
|
+
return undefined;
|
|
695
|
+
for (let index = this.state.ordered.length - 1; index >= 0; index -= 1) {
|
|
696
|
+
const operation = this.state.ordered[index];
|
|
697
|
+
if (operation.kind !== 'team/dm-sent' || operation.operationId === excluding)
|
|
698
|
+
continue;
|
|
699
|
+
const pair = operation.data.senderMemberId === sender.memberId && operation.data.recipientMemberId === recipientMemberId;
|
|
700
|
+
const mirror = operation.data.senderMemberId === recipientMemberId && operation.data.recipientMemberId === sender.memberId;
|
|
701
|
+
if (!pair && !mirror)
|
|
702
|
+
continue;
|
|
703
|
+
// pair: the other Member sent it to the reader → (them → you);
|
|
704
|
+
// mirror: the reader sent it to the other Member → (you → them).
|
|
705
|
+
const direction = pair ? 'them → you' : 'you → them';
|
|
706
|
+
const truncated = operation.data.body.length > 160 ? `${operation.data.body.slice(0, 160)}…` : operation.data.body;
|
|
707
|
+
return `(${direction}) ${truncated}`;
|
|
708
|
+
}
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
626
711
|
attentionStatus(actor, request) {
|
|
627
712
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
628
713
|
const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
|
|
@@ -929,6 +1014,10 @@ export class AgentTeamLedger {
|
|
|
929
1014
|
// A read advances only the reader's private watermark; no projection
|
|
930
1015
|
// visible to other participants changes, so nobody is woken.
|
|
931
1016
|
return [];
|
|
1017
|
+
case 'team/dm-sent':
|
|
1018
|
+
// A DM changes no shared projection: delivery is a session-level
|
|
1019
|
+
// runtime effect, so no change waiter has anything to refetch.
|
|
1020
|
+
return [];
|
|
932
1021
|
default:
|
|
933
1022
|
return assertUnhandledKind(operation);
|
|
934
1023
|
}
|
|
@@ -987,6 +1076,7 @@ export class AgentTeamLedger {
|
|
|
987
1076
|
case 'team/member-updated':
|
|
988
1077
|
case 'team/channel-member-added':
|
|
989
1078
|
case 'team/thread-read':
|
|
1079
|
+
case 'team/dm-sent':
|
|
990
1080
|
return [];
|
|
991
1081
|
default:
|
|
992
1082
|
return assertUnhandledKind(operation);
|
|
@@ -1362,6 +1452,18 @@ export class AgentTeamLedger {
|
|
|
1362
1452
|
this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
|
|
1363
1453
|
return;
|
|
1364
1454
|
}
|
|
1455
|
+
if (operation.kind === 'team/dm-sent') {
|
|
1456
|
+
const sender = assertMember();
|
|
1457
|
+
const recipient = projection.members.get(operation.data.recipientMemberId);
|
|
1458
|
+
if (recipient === undefined || recipient.workspaceId !== operation.data.workspaceId
|
|
1459
|
+
|| recipient.state !== 'enabled' || recipient.memberId === AGENT_TEAM_HUMAN_MEMBER_ID
|
|
1460
|
+
|| operation.data.senderMemberId !== sender.memberId
|
|
1461
|
+
|| operation.data.recipientMemberId === sender.memberId
|
|
1462
|
+
|| operation.data.body.trim() === '' || operation.data.body !== operation.data.body.trim()) {
|
|
1463
|
+
throw new Error('invalid DM operation');
|
|
1464
|
+
}
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1365
1467
|
assertUnhandledKind(operation);
|
|
1366
1468
|
}
|
|
1367
1469
|
validateInboxDelta(delta, projection, _refs, additionalThreadRefs = [], additionalMessages = [], additionalActivities = []) {
|
|
@@ -1633,6 +1735,11 @@ export class AgentTeamLedger {
|
|
|
1633
1735
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
1634
1736
|
return;
|
|
1635
1737
|
}
|
|
1738
|
+
if (operation.kind === 'team/dm-sent') {
|
|
1739
|
+
// Audit-only: delivery is a transient runtime effect, so the durable
|
|
1740
|
+
// projection deliberately does not change.
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1636
1743
|
assertUnhandledKind(operation);
|
|
1637
1744
|
}
|
|
1638
1745
|
/** Facts arrive in ledger sequence order, so global and per-thread lists stay sorted by append only. */
|
|
@@ -2206,6 +2313,20 @@ export class AgentTeamLedger {
|
|
|
2206
2313
|
if (model.provider.trim() === '' || model.model.trim() === '')
|
|
2207
2314
|
throw new Error('member model selection must name a provider route and a model id');
|
|
2208
2315
|
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Capability allow-list entries are exact identifiers (skill names,
|
|
2318
|
+
* reserved tool names); only whitespace-only values are rejected. Unknown
|
|
2319
|
+
* names are intent, not errors — committing must stay replayable across
|
|
2320
|
+
* Harness upgrades, so divergence is derived at activation instead.
|
|
2321
|
+
*/
|
|
2322
|
+
assertCapabilities(capabilities) {
|
|
2323
|
+
for (const surface of [capabilities?.tools?.allow, capabilities?.skills?.allow]) {
|
|
2324
|
+
if (surface === undefined)
|
|
2325
|
+
continue;
|
|
2326
|
+
if (surface.some(name => name.trim() === ''))
|
|
2327
|
+
throw new Error('capability allow-list names must not be empty');
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2209
2330
|
initialization() {
|
|
2210
2331
|
const operation = this.state.ordered[0];
|
|
2211
2332
|
if (operation === undefined)
|
|
@@ -2234,6 +2355,7 @@ export class AgentTeamLedger {
|
|
|
2234
2355
|
|| operation.data.member.workspaceId !== request.workspaceId || operation.data.member.handle !== request.handle.trim()
|
|
2235
2356
|
|| operation.data.member.description !== request.description.trim() || operation.data.member.presetId !== request.presetId.trim()
|
|
2236
2357
|
|| !isDeepStrictEqual(operation.data.member.model ?? undefined, request.member.model ?? undefined)
|
|
2358
|
+
|| !isDeepStrictEqual(operation.data.member.capabilities ?? undefined, request.member.capabilities ?? undefined)
|
|
2237
2359
|
|| !this.sameList(operation.data.channelRefs, this.normalizeUnique(request.channelRefs, 'initial Member Channels')))
|
|
2238
2360
|
this.throwRequestCollision(request.requestId);
|
|
2239
2361
|
}
|
|
@@ -2257,7 +2379,8 @@ export class AgentTeamLedger {
|
|
|
2257
2379
|
if (operation.kind !== 'team/member-updated' || !this.sameActor(operation.actor, request.actor)
|
|
2258
2380
|
|| operation.data.member.memberId !== request.memberId || operation.data.member.handle !== request.handle.trim()
|
|
2259
2381
|
|| operation.data.member.description !== request.description.trim()
|
|
2260
|
-
|| !isDeepStrictEqual(operation.data.member.model ?? undefined, request.model ?? undefined)
|
|
2382
|
+
|| !isDeepStrictEqual(operation.data.member.model ?? undefined, request.model ?? undefined)
|
|
2383
|
+
|| !isDeepStrictEqual(operation.data.member.capabilities ?? undefined, request.capabilities ?? undefined))
|
|
2261
2384
|
this.throwRequestCollision(request.requestId);
|
|
2262
2385
|
}
|
|
2263
2386
|
assertSameChannelJoin(operation, request) {
|
|
@@ -2270,6 +2393,17 @@ export class AgentTeamLedger {
|
|
|
2270
2393
|
|| operation.data.workspaceId !== request.workspaceId || operation.data.channelRef !== request.channelRef || operation.data.memberId !== request.memberId)
|
|
2271
2394
|
this.throwRequestCollision(request.requestId);
|
|
2272
2395
|
}
|
|
2396
|
+
assertSameDm(operation, request) {
|
|
2397
|
+
if (operation.kind !== 'team/dm-sent' || !this.sameActor(operation.actor, request.actor)
|
|
2398
|
+
|| operation.data.workspaceId !== request.workspaceId
|
|
2399
|
+
|| operation.data.senderMemberId !== request.actor.memberId
|
|
2400
|
+
|| operation.data.recipientMemberId !== request.recipientMemberId
|
|
2401
|
+
|| operation.data.body !== request.body.trim())
|
|
2402
|
+
this.throwRequestCollision(request.requestId);
|
|
2403
|
+
}
|
|
2404
|
+
dmResult(operation) {
|
|
2405
|
+
return Object.freeze({ receipt: this.receipt(operation), recipient: this.requireMember(operation.data.recipientMemberId) });
|
|
2406
|
+
}
|
|
2273
2407
|
assertSameMessage(operation, request, recipients) {
|
|
2274
2408
|
if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
|
|
2275
2409
|
|| operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
|
|
@@ -59,10 +59,10 @@ export function renderMemberMemory(raw, privateMemoryPath = '<private-memory-pat
|
|
|
59
59
|
const warning = overBudget
|
|
60
60
|
? '\n\n[Maintenance warning: memory.md exceeds the 8 KiB context budget. Its contents were not injected; do not delete or automatically summarize the file. Maintain a smaller index explicitly.]'
|
|
61
61
|
: '';
|
|
62
|
-
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nThese paths are outside the Workspace cwd. Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
|
|
62
|
+
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
|
|
63
63
|
}
|
|
64
64
|
function renderUnavailableMemory(privateMemoryPath, reason) {
|
|
65
|
-
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nThese paths are outside the Workspace cwd. Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;
|
|
65
|
+
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;
|
|
66
66
|
}
|
|
67
67
|
function escape(value) {
|
|
68
68
|
return value.replaceAll(BEGIN, '[escaped begin marker]').replaceAll(END, '[escaped end marker]');
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-Member private skill provider: the Team-owned wrapper around the
|
|
3
|
+
* Harness filesystem provider that scopes one Member's skill discovery to
|
|
4
|
+
* exactly the plugin's bundled read-only core skills plus that Member's
|
|
5
|
+
* private `skills/` directory (default roots excluded), and filters `list()`
|
|
6
|
+
* output by that Member's selection ref.
|
|
7
|
+
*
|
|
8
|
+
* Deliberate interface reservation: this per-Member provider seam is the
|
|
9
|
+
* primitive future Runtime Revision manifests orchestrate — do not remove
|
|
10
|
+
* during cleanup.
|
|
11
|
+
*
|
|
12
|
+
* @module @wowyuarm/dsh-agent-team/member-skills
|
|
13
|
+
*/
|
|
14
|
+
import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
|
|
15
|
+
/**
|
|
16
|
+
* Register the Member-private skill provider on one Member's agent scope and
|
|
17
|
+
* return its disposer. The provider files into that agent's exact layer, so
|
|
18
|
+
* no sibling Member or ordinary session can observe the catalog; discovery,
|
|
19
|
+
* watching, and invalidation flow through the same Harness provider below.
|
|
20
|
+
*
|
|
21
|
+
* Registration goes through the traceable service resolved FROM the agent
|
|
22
|
+
* context (the same shape as `tools.restrict()`), never through a plugin
|
|
23
|
+
* mount: a plugin mounted from Host activation code lands on the Host's
|
|
24
|
+
* async-trace fiber instead of the agent scope, and a plugin `inject` would
|
|
25
|
+
* hold Member activation open while the Host is still starting. A deployment
|
|
26
|
+
* without the skill registry keeps its Members; they simply carry no private
|
|
27
|
+
* skill catalog.
|
|
28
|
+
*/
|
|
29
|
+
export function mountMemberSkillProvider(agentCtx, config) {
|
|
30
|
+
const selection = config.selection;
|
|
31
|
+
// A no-op swap stands in for deployments without the skill registry, so
|
|
32
|
+
// the Host's edit path never sees a half-initialized ref.
|
|
33
|
+
selection.swap = () => { };
|
|
34
|
+
const skills = agentCtx.get('skills');
|
|
35
|
+
if (skills === undefined)
|
|
36
|
+
return () => { };
|
|
37
|
+
return skills.registerProvider(control => {
|
|
38
|
+
const provider = new MemberPrivateSkillProvider(agentCtx, control, config, selection);
|
|
39
|
+
// The ref swap updates the live filter and drops cached catalogs through
|
|
40
|
+
// the provider's own registration, so the next catalog query (and the
|
|
41
|
+
// next step's durable replacement catalog) sees the new selection.
|
|
42
|
+
selection.swap = (allow) => {
|
|
43
|
+
selection.current = allow;
|
|
44
|
+
control.invalidate();
|
|
45
|
+
};
|
|
46
|
+
return provider;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** One Member's bundled-plus-private skill roots wrapped as a filtered registry provider. */
|
|
50
|
+
class MemberPrivateSkillProvider {
|
|
51
|
+
selection;
|
|
52
|
+
name;
|
|
53
|
+
wrapped;
|
|
54
|
+
constructor(ctx, control, config, selection) {
|
|
55
|
+
this.selection = selection;
|
|
56
|
+
this.name = `member-private:${config.skillsDirectory}`;
|
|
57
|
+
// Two Team-owned roots, no project/user/global leakage: the bundled
|
|
58
|
+
// read-only core skills first, then the Member's writable private
|
|
59
|
+
// directory. Within one provider both roots share the custom rank, and
|
|
60
|
+
// discovery keeps the first root's same-name candidate, so a bundled
|
|
61
|
+
// skill stays stable across plugin upgrades while a Member installs
|
|
62
|
+
// its own additions under their own names.
|
|
63
|
+
const customSkillDirs = config.bundledSkillsDirectory === undefined
|
|
64
|
+
? [config.skillsDirectory]
|
|
65
|
+
: [config.bundledSkillsDirectory, config.skillsDirectory];
|
|
66
|
+
this.wrapped = new FileSystemSkillProvider(ctx, control, {
|
|
67
|
+
providerName: this.name,
|
|
68
|
+
includeDefaultRoots: false,
|
|
69
|
+
customSkillDirs,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Filter the wrapped discovery to the Member's selection. Filtering is
|
|
74
|
+
* list()-output only — the directory is still scanned and watched, so this
|
|
75
|
+
* is a performance/visibility semantic, not a security boundary.
|
|
76
|
+
*/
|
|
77
|
+
async list(options) {
|
|
78
|
+
const output = await this.wrapped.list(options);
|
|
79
|
+
const allow = this.selection.current;
|
|
80
|
+
if (allow === undefined)
|
|
81
|
+
return output;
|
|
82
|
+
const allowed = new Set(allow);
|
|
83
|
+
return filterObservation(output, candidate => allowed.has(candidate.name));
|
|
84
|
+
}
|
|
85
|
+
async get(candidate, options) {
|
|
86
|
+
return this.wrapped.get(candidate, options);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Apply one name filter to either provider observation shape. */
|
|
90
|
+
function filterObservation(output, keep) {
|
|
91
|
+
if (!('candidates' in output))
|
|
92
|
+
return output.filter(keep);
|
|
93
|
+
return { candidates: output.candidates.filter(keep), complete: output.complete };
|
|
94
|
+
}
|
|
@@ -32,6 +32,19 @@ const modelSelectionSchema = z.object({
|
|
|
32
32
|
model,
|
|
33
33
|
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
34
34
|
}));
|
|
35
|
+
// Pure intent: allow-list entries are plain strings. They are never
|
|
36
|
+
// validated against a known-name set here — committing must survive Harness
|
|
37
|
+
// upgrades that rename or remove tools, so divergence is derived at
|
|
38
|
+
// activation instead (see AgentTeamCapabilityWarning).
|
|
39
|
+
const memberCapabilitiesSchema = z.object({
|
|
40
|
+
// Deliberate interface reservation, no UI writes it today: Runtime Revision
|
|
41
|
+
// manifests depend on this seam — do not remove during cleanup.
|
|
42
|
+
tools: z.object({ allow: z.array(z.string().min(1)) }).strict().transform(omitUndefined).optional(),
|
|
43
|
+
skills: z.object({ allow: z.array(z.string().min(1)) }).strict().transform(omitUndefined).optional(),
|
|
44
|
+
}).strict().transform(({ tools, skills }) => ({
|
|
45
|
+
...(tools === undefined ? {} : { tools }),
|
|
46
|
+
...(skills === undefined ? {} : { skills }),
|
|
47
|
+
}));
|
|
35
48
|
const memberSchema = z.object({
|
|
36
49
|
memberId: memberIdSchema,
|
|
37
50
|
sessionId: sessionIdSchema,
|
|
@@ -41,6 +54,8 @@ const memberSchema = z.object({
|
|
|
41
54
|
presetId: z.string().min(1),
|
|
42
55
|
// Ledgers written before per-Member model selection existed omit the field.
|
|
43
56
|
model: modelSelectionSchema.optional(),
|
|
57
|
+
// Ledgers written before member capabilities existed omit the field.
|
|
58
|
+
capabilities: memberCapabilitiesSchema.optional(),
|
|
44
59
|
privateMemoryPath: z.string().min(1),
|
|
45
60
|
state: z.union([z.literal('enabled'), z.literal('suspended'), z.literal('inactive')]),
|
|
46
61
|
}).strict();
|
|
@@ -384,6 +399,17 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
|
|
|
384
399
|
inbox: inboxDeltaSchema,
|
|
385
400
|
}).strict(),
|
|
386
401
|
}).strict(),
|
|
402
|
+
z.object({
|
|
403
|
+
...operationBase,
|
|
404
|
+
previousOperationId: operationIdSchema.nullable(),
|
|
405
|
+
kind: z.literal('team/dm-sent'),
|
|
406
|
+
data: z.object({
|
|
407
|
+
workspaceId: workspaceIdSchema,
|
|
408
|
+
senderMemberId: memberIdSchema,
|
|
409
|
+
recipientMemberId: memberIdSchema,
|
|
410
|
+
body: z.string().min(1),
|
|
411
|
+
}).strict(),
|
|
412
|
+
}).strict(),
|
|
387
413
|
]);
|
|
388
414
|
/** Durable validator for the closed Agent Team operation union; ledgers written before message occurredAt existed normalize on load. */
|
|
389
415
|
export const agentTeamOperationSchema = storedAgentTeamOperationSchema.transform(stampOperationMessages);
|