agents-can-communicate 0.10.4 → 0.11.0
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 +29 -21
- package/bin/acc-indicator.mjs +5 -0
- package/docs/CAPABILITIES.md +3 -0
- package/docs/CLI.md +34 -7
- package/docs/MCP.md +42 -2
- package/docs/PROTOCOL.md +19 -0
- package/docs/STATUS_INDICATOR.md +128 -0
- package/docs/WHY_ACC.md +14 -8
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +3 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-install.mjs +80 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-render.mjs +8 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +7 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/hooks/indicator.mjs +67 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +16 -5
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +4 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/indicator-install.mjs +30 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/indicator.mjs +23 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +9 -5
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +4 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator-install.mjs +30 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator.mjs +17 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +12 -6
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +5 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-command.mjs +14 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-text.mjs +13 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-order.mjs +12 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-writer.mjs +37 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation.mjs +47 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-indicator.mjs +141 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-scan.mjs +99 -0
- package/node_modules/@agents-can-communicate/cli/package.json +2 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/claim-command.mjs +64 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +15 -8
- package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-command.mjs +65 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-previous-command.mjs +84 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-previous-windows.mjs +8 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator.mjs +112 -0
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +4 -0
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +3 -53
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +19 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/claim-selectors.mjs +62 -0
- package/node_modules/@agents-can-communicate/core/src/claims.mjs +88 -81
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +14 -5
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +3 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +2 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +11 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/claim-tools.mjs +56 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +9 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +7 -8
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +27 -9
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +13 -0
- package/package.json +3 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { AccError, EXIT, SCHEMA_VERSION,
|
|
2
|
-
|
|
1
|
+
import { AccError, EXIT, SCHEMA_VERSION, createId, validateRecord }
|
|
2
|
+
from "@agents-can-communicate/protocol";
|
|
3
3
|
|
|
4
|
+
import { claimResources, releaseSelectors, selectReleaseClaims } from "./claim-selectors.mjs";
|
|
4
5
|
import { ensureMaterialised } from "./materialisation.mjs";
|
|
5
6
|
import { classifySessionPresence } from "./sessions.mjs";
|
|
6
7
|
|
|
@@ -33,10 +34,11 @@ const isLive = (claim, now) => Date.parse(claim.expiresAt) > Date.parse(now);
|
|
|
33
34
|
export function createClaimService(ports, sessions) {
|
|
34
35
|
const { store, clock, ids, pidIsAlive } = ports;
|
|
35
36
|
|
|
36
|
-
async function requireOwner(input, action, tx) {
|
|
37
|
+
async function requireOwner(input, action, tx, allowEphemeral = false) {
|
|
37
38
|
const current = tx === undefined
|
|
38
39
|
? (await sessions.locateSession(input.sessionId, input.workspaceId))?.record
|
|
39
|
-
: tx.get("session", input.sessionId)
|
|
40
|
+
: tx.get("session", input.sessionId)
|
|
41
|
+
?? (allowEphemeral ? await store.ephemeral.get("session", input.sessionId) : null);
|
|
40
42
|
if (current == null || current.state !== "open") {
|
|
41
43
|
throw new AccError(EXIT.CONFLICT, `cannot ${action} from a session that is not open`,
|
|
42
44
|
{ sessionId: input.sessionId });
|
|
@@ -65,63 +67,66 @@ export function createClaimService(ports, sessions) {
|
|
|
65
67
|
});
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
async function
|
|
70
|
+
async function acquire(input, resources, batch) {
|
|
69
71
|
const session = await requireOwner(input, "claim");
|
|
70
|
-
|
|
71
|
-
// claim taken over MCP and one taken from the CLI mean the same thing - and
|
|
72
|
-
// a shape that could never match is refused rather than stored.
|
|
73
|
-
assertMatchableResource(input.resource, message => {
|
|
74
|
-
throw new AccError(EXIT.USAGE, message, { resource: input.resource });
|
|
75
|
-
});
|
|
76
|
-
const resource = normaliseResource(input.resource);
|
|
72
|
+
resources = claimResources(resources);
|
|
77
73
|
const workspaceId = session.workspaceId;
|
|
78
74
|
await ensureMaterialised(ports, { workspaceId, descriptor: input.descriptor,
|
|
79
75
|
reason: "durable_object" });
|
|
80
|
-
|
|
81
|
-
const expiresAt = new Date(Date.parse(now) + (input.leaseSeconds ?? 1800) * 1000)
|
|
82
|
-
.toISOString();
|
|
83
|
-
// Only to name the owner of a conflicting claim in the error.
|
|
84
|
-
const snapshot = await store.snapshot(workspaceId, { kinds: ["session"] });
|
|
85
|
-
|
|
86
|
-
let record = null;
|
|
87
|
-
await store.transaction(async tx => {
|
|
76
|
+
return store.transaction(async tx => {
|
|
88
77
|
await requireOwner(input, "claim", tx);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
78
|
+
const now = clock.now();
|
|
79
|
+
const expiresAt = new Date(Date.parse(now) + (input.leaseSeconds ?? 1800) * 1000)
|
|
80
|
+
.toISOString();
|
|
81
|
+
const existing = tx.list("claim");
|
|
82
|
+
const live = existing.filter(claim => isLive(claim, now));
|
|
83
|
+
const mode = input.mode ?? "exclusive";
|
|
84
|
+
const conflicts = resources.flatMap(resource => live
|
|
85
|
+
.filter(claim => overlaps(claim.resource, resource)
|
|
86
|
+
&& claim.ownerSessionId !== session.sessionId
|
|
87
|
+
&& (claim.mode === "exclusive" || mode === "exclusive"))
|
|
88
|
+
.map(claim => ({ requestedResource: resource,
|
|
89
|
+
...conflictWith(claim, session, now, tx.list("session")).details })));
|
|
90
|
+
if (conflicts.length > 0) {
|
|
91
|
+
if (!batch) {
|
|
92
|
+
const { requestedResource, ...details } = conflicts[0];
|
|
93
|
+
throw new AccError(EXIT.CONFLICT, "the resource is already claimed", details);
|
|
94
|
+
}
|
|
95
|
+
const message = "batch claim conflicts:\n" + conflicts.map(conflict =>
|
|
96
|
+
`${conflict.requestedResource} overlaps ${conflict.resource}, held by ${conflict.ownerSessionId}`)
|
|
97
|
+
.join("\n");
|
|
98
|
+
throw new AccError(EXIT.CONFLICT, message, { conflicts });
|
|
99
|
+
}
|
|
100
|
+
// Validate every record before staging a write, including renewals.
|
|
101
|
+
const records = resources.map(resource => {
|
|
102
|
+
const mine = existing.find(claim => claim.ownerSessionId === session.sessionId
|
|
103
|
+
&& claim.resource === resource);
|
|
104
|
+
const claimId = mine?.claimId ?? createId("claim");
|
|
105
|
+
return { mine, record: validateRecord("claim", {
|
|
106
|
+
schemaVersion: SCHEMA_VERSION,
|
|
107
|
+
claimId,
|
|
108
|
+
workspaceId,
|
|
109
|
+
ownerSessionId: session.sessionId,
|
|
110
|
+
resource,
|
|
111
|
+
mode,
|
|
112
|
+
enforcement: input.enforcement ?? "advisory",
|
|
113
|
+
reason: input.reason,
|
|
114
|
+
// Reuse a lapsed record, but start a new period of holding it.
|
|
115
|
+
acquiredAt: mine !== undefined && isLive(mine, now) ? mine.acquiredAt : now,
|
|
116
|
+
expiresAt,
|
|
117
|
+
generation: ids.next("generation"),
|
|
118
|
+
}) };
|
|
117
119
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
for (const { mine, record } of records) {
|
|
121
|
+
const { claimId, resource } = record;
|
|
122
|
+
tx.put("claim", claimId, record, tx.generationOf("claim", claimId));
|
|
123
|
+
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"), workspaceId,
|
|
124
|
+
actorSessionId: session.sessionId, type: mine === undefined ? "claim.acquired"
|
|
125
|
+
: "claim.renewed", occurredAt: now,
|
|
126
|
+
payload: { claimId, resource, mode: record.mode } });
|
|
127
|
+
}
|
|
128
|
+
return records.map(item => item.record);
|
|
123
129
|
}, { kinds: ["session", "claim"] });
|
|
124
|
-
return record;
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
async function renewClaim(input) {
|
|
@@ -146,40 +151,42 @@ export function createClaimService(ports, sessions) {
|
|
|
146
151
|
return record;
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
async function
|
|
154
|
+
async function removeClaims(input, selectors, { authority = null, reason = null } = {}) {
|
|
150
155
|
const session = await requireOwner(input, "release");
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
return store.transaction(async tx => {
|
|
157
|
+
await requireOwner(input, "release", tx, true);
|
|
158
|
+
const now = clock.now();
|
|
159
|
+
const selected = selectReleaseClaims(tx, selectors, session.sessionId);
|
|
160
|
+
for (const existing of selected) {
|
|
161
|
+
const owned = existing.ownerSessionId === session.sessionId;
|
|
162
|
+
if (!owned && authority !== "human" && authority !== "policy") {
|
|
163
|
+
throw new AccError(EXIT.CONFLICT,
|
|
164
|
+
"force release requires human or policy authority",
|
|
165
|
+
{ claimId: existing.claimId, authority });
|
|
166
|
+
}
|
|
157
167
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
for (const existing of selected) {
|
|
169
|
+
const owned = existing.ownerSessionId === session.sessionId;
|
|
170
|
+
// Remove the live record; per-resource events retain its history.
|
|
171
|
+
tx.remove("claim", existing.claimId, tx.generationOf("claim", existing.claimId));
|
|
172
|
+
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
173
|
+
workspaceId: existing.workspaceId, actorSessionId: session.sessionId,
|
|
174
|
+
type: owned ? "claim.released" : "claim.force_released", occurredAt: now,
|
|
175
|
+
payload: { claimId: existing.claimId, authority,
|
|
176
|
+
reason, replacedGeneration: existing.generation } });
|
|
165
177
|
}
|
|
166
|
-
|
|
167
|
-
// not a claim, and every consumer of a snapshot would otherwise have to
|
|
168
|
-
// re-derive liveness. The event log keeps the history.
|
|
169
|
-
tx.remove("claim", input.claimId, tx.generationOf("claim", input.claimId));
|
|
170
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
171
|
-
workspaceId: existing.workspaceId, actorSessionId: session.sessionId,
|
|
172
|
-
type: owned ? "claim.released" : "claim.force_released", occurredAt: now,
|
|
173
|
-
payload: { claimId: input.claimId, authority: authority ?? null,
|
|
174
|
-
reason: reason ?? null, replacedGeneration: existing.generation } });
|
|
178
|
+
return selected;
|
|
175
179
|
}, { kinds: ["session", "claim"] });
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
return {
|
|
179
|
-
acquireClaim,
|
|
183
|
+
acquireClaim: async input => (await acquire(input, [input.resource], false))[0],
|
|
184
|
+
acquireClaims: input => acquire(input, input.resources, true),
|
|
180
185
|
renewClaim,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
releaseClaims: async input => removeClaims(input, releaseSelectors(input)),
|
|
187
|
+
releaseClaim: async input => { await removeClaims(input, { claimIds: [input.claimId] }); },
|
|
188
|
+
forceReleaseClaim: async input => { await removeClaims(input, { claimIds: [input.claimId] },
|
|
189
|
+
{ authority: input.authority, reason: input.reason });
|
|
190
|
+
},
|
|
184
191
|
};
|
|
185
192
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { NATIVE_REASON_CODES, evaluateVersionContract, isLaunchOption }
|
|
1
|
+
import { NATIVE_REASON_CODES, evaluateVersionContract, isLaunchOption, OFFER_ERRORS, storeOfferObservation }
|
|
2
2
|
from "@agents-can-communicate/adapter-sdk";
|
|
3
3
|
|
|
4
4
|
import { lastNativeReason } from "./native-reason.mjs";
|
|
5
5
|
import { refreshExpiredBinding } from "./refresh-binding.mjs";
|
|
6
6
|
|
|
7
|
-
const SAFE_ERRORS = new Set(
|
|
8
|
-
"recipient_busy", "recipient_unavailable", "transport_error", "transport_rejected", "transport_permission_denied",
|
|
9
|
-
"unsupported_client_version"]);
|
|
7
|
+
const SAFE_ERRORS = new Set(OFFER_ERRORS);
|
|
10
8
|
const NAMED_LIVE_TRANSPORTS = new Set(["claude-inbox", "codex-app-server"]);
|
|
11
9
|
|
|
12
10
|
const adaptersById = adapters => adapters instanceof Map
|
|
@@ -50,6 +48,14 @@ function safeTransport(value, opaqueEndpointRef) {
|
|
|
50
48
|
export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
51
49
|
readNativeReason = input => lastNativeReason({ runtimeDir: service.store?.root, ...input }) }) {
|
|
52
50
|
const registry = adaptersById(adapters);
|
|
51
|
+
const observeOffer = (binding, reasonCode = null) => {
|
|
52
|
+
// Capture time and order now, but never await optional diagnostic I/O on
|
|
53
|
+
// the delivery path. The SDK bounds its own lifetime for short-lived CLIs.
|
|
54
|
+
void storeOfferObservation({
|
|
55
|
+
runtimeDir: service.store?.root, workspaceId: service.store?.workspaceId,
|
|
56
|
+
sessionId: binding.sessionId, generation: binding.generation, at: clock.now(),
|
|
57
|
+
state: reasonCode === null ? "active" : "degraded", reasonCode }).catch(() => {});
|
|
58
|
+
};
|
|
53
59
|
|
|
54
60
|
// A session that bound no live transport may have said why: the reason its
|
|
55
61
|
// own last native attempt recorded, from the closed vocabulary only, and the
|
|
@@ -82,6 +88,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
82
88
|
targetGeneration: binding.generation,
|
|
83
89
|
transport: safeTransport(transport, binding.opaqueEndpointRef), adapterId: binding.adapterId,
|
|
84
90
|
clientVersion: binding.clientVersion, safeErrorCode }).catch(() => null);
|
|
91
|
+
observeOffer(binding, safeErrorCode);
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
async function offerTo(message, participantId, now) {
|
|
@@ -135,7 +142,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
135
142
|
if (!refreshed.refreshed) {
|
|
136
143
|
if (refreshed.errorCode === "transport_permission_denied") {
|
|
137
144
|
await recordFailure(binding, message, participantId, "live-adapter", refreshed.errorCode);
|
|
138
|
-
}
|
|
145
|
+
} else observeOffer(binding, refreshed.errorCode);
|
|
139
146
|
return durable(participantId, refreshed.errorCode);
|
|
140
147
|
}
|
|
141
148
|
const current = (await service.listDeliveryBindings({
|
|
@@ -216,6 +223,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
216
223
|
// shown and leave it out of the very turn the wake started. The receipt
|
|
217
224
|
// stays queued; the hook records the offer once its stdout carried the body.
|
|
218
225
|
if (adapter.nativeDelivery?.offerKind === "wake") {
|
|
226
|
+
observeOffer(binding, response.pendingApproval === true ? "inbound_approval_required" : null);
|
|
219
227
|
// The client took the wake but holds it for its user's approval.
|
|
220
228
|
return { recipientParticipantId: participantId, outcome: "woken", transport,
|
|
221
229
|
...(response.pendingApproval === true ? { pendingApproval: true } : {}) };
|
|
@@ -229,6 +237,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
229
237
|
await recordFailure(binding, message, participantId, transport, "transport_error");
|
|
230
238
|
return durable(participantId, "transport_error");
|
|
231
239
|
}
|
|
240
|
+
observeOffer(binding);
|
|
232
241
|
return { recipientParticipantId: participantId, outcome: "offered", transport };
|
|
233
242
|
}
|
|
234
243
|
|
|
@@ -39,6 +39,7 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
39
39
|
const createdDirectories = await missingArtifactParents({ home: context.home,
|
|
40
40
|
artifacts: operation.artifacts });
|
|
41
41
|
const installContext = { ...context,
|
|
42
|
+
indicator: operation.indicator,
|
|
42
43
|
clientVersion: operation.clientVersion, platform: operation.platform,
|
|
43
44
|
requestedLivePolicy: operation.livePolicy ?? "off",
|
|
44
45
|
livePolicy: operation.configuredLivePolicy ?? operation.effectiveLivePolicy ?? "off" };
|
|
@@ -71,10 +72,12 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
71
72
|
version: operation.clientVersion ?? null, accVersion,
|
|
72
73
|
artifacts: operation.artifacts, createdDirectories,
|
|
73
74
|
deliveryPolicy: operation.livePolicy, deliveryDecision: operation.deliveryDecision,
|
|
75
|
+
indicator: operation.indicator,
|
|
74
76
|
nativeActivation: native });
|
|
75
77
|
results.operations.push({ ...operation, applied: true,
|
|
76
78
|
needsAction: outcome.needsAction ?? [],
|
|
77
79
|
changes: outcome.changes ?? [], diagnostics: [
|
|
80
|
+
...(operation.indicatorDiagnostic === undefined ? [] : [operation.indicatorDiagnostic]),
|
|
78
81
|
...(operation.deliveryDiagnostic === undefined
|
|
79
82
|
? [] : [operation.deliveryDiagnostic]),
|
|
80
83
|
...(outcome.diagnostics ?? []),
|
|
@@ -121,7 +121,7 @@ async function saveOwnership({ dataHome, record }) {
|
|
|
121
121
|
*/
|
|
122
122
|
export async function recordInstall({ dataHome, adapterId, version, accVersion = null,
|
|
123
123
|
artifacts, createdDirectories = [], deliveryPolicy, deliveryDecision,
|
|
124
|
-
nativeActivation = null }) {
|
|
124
|
+
nativeActivation = null, indicator }) {
|
|
125
125
|
const stamped = await Promise.all(artifacts.map(async artifact => ({
|
|
126
126
|
path: artifact.path,
|
|
127
127
|
kind: artifact.kind ?? "file",
|
|
@@ -141,6 +141,7 @@ export async function recordInstall({ dataHome, adapterId, version, accVersion =
|
|
|
141
141
|
{ adapterId, version, accVersion, artifacts: stamped,
|
|
142
142
|
...(directories.length === 0 ? {} : { createdDirectories: directories }),
|
|
143
143
|
...(deliveryPolicy === undefined ? {} : { deliveryPolicy }),
|
|
144
|
+
...(indicator === undefined ? {} : { indicator }),
|
|
144
145
|
...(retainedDeliveryDecision === undefined ? {} : {
|
|
145
146
|
deliveryDecision: retainedDeliveryDecision,
|
|
146
147
|
}),
|
|
@@ -14,7 +14,7 @@ import { LIVE_POLICIES, describeActivation, describeDeactivation, planActivation
|
|
|
14
14
|
*/
|
|
15
15
|
export function planInstallation({ adapters, detected, context, action = "install",
|
|
16
16
|
recorded = [], accVersion = null, allowDowngrade = false, requested = [],
|
|
17
|
-
deliveryByAdapter = {}, deliveryDecisionByAdapter = {}, allowServiceSetup = false }) {
|
|
17
|
+
deliveryByAdapter = {}, deliveryDecisionByAdapter = {}, allowServiceSetup = false, indicator }) {
|
|
18
18
|
if (!["install", "uninstall"].includes(action)) {
|
|
19
19
|
throw new AccError(EXIT.USAGE, `unknown installation action: ${action}`, { action });
|
|
20
20
|
}
|
|
@@ -157,7 +157,13 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
157
157
|
: null;
|
|
158
158
|
const deliverySummary = action === "install"
|
|
159
159
|
? describeInstallDelivery(entry, delivery, effectiveLivePolicy) : null;
|
|
160
|
-
const
|
|
160
|
+
const indicatorPreference = indicator ?? recordedById.get(entry.adapterId)?.indicator ?? "off";
|
|
161
|
+
const indicatorDiagnostic = indicatorPreference !== "on" ? null : !adapter.statusIndicator
|
|
162
|
+
? `${adapter.displayName} indicator: not supported in this client's native status line`
|
|
163
|
+
: entry.version == null || isOlder(entry.version, adapter.statusIndicator.minimumVersion)
|
|
164
|
+
? `${adapter.displayName} indicator requires client ${adapter.statusIndicator.minimumVersion} or later`
|
|
165
|
+
: `${adapter.displayName} indicator: enabled; ${adapter.statusIndicator.reload}`;
|
|
166
|
+
const installContext = { ...context, indicator: indicatorPreference, requestedLivePolicy: delivery,
|
|
161
167
|
livePolicy: configuredLivePolicy, clientVersion: entry.version, platform: entry.platform };
|
|
162
168
|
const nativeServiceSetup = action === "install" && allowServiceSetup && delivery !== "off"
|
|
163
169
|
&& deliveryDecision.completeSetup === true ? entry.nativeServiceSetup : undefined;
|
|
@@ -192,6 +198,8 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
192
198
|
clientPresent: entry.present === true,
|
|
193
199
|
alreadyInstalled: entry.installed === true,
|
|
194
200
|
livePolicy: delivery,
|
|
201
|
+
indicator: indicatorPreference,
|
|
202
|
+
...(indicatorDiagnostic === null ? {} : { indicatorDiagnostic }),
|
|
195
203
|
effectiveLivePolicy,
|
|
196
204
|
...(action === "install" ? { deliveryDecision } : {}),
|
|
197
205
|
...(retainedActivation ? { configuredLivePolicy, retainedNativeActivation: retainedActivation } : {}),
|
|
@@ -205,6 +213,7 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
205
213
|
// Said in the operator's terms, not in paths: which files ACC creates
|
|
206
214
|
// outright and which belong to the user and are only edited.
|
|
207
215
|
summary: [
|
|
216
|
+
...(indicatorDiagnostic === null ? [] : [indicatorDiagnostic]),
|
|
208
217
|
...(entry.present ? [] : [`${adapter.displayName ?? adapter.id} is no longer on `
|
|
209
218
|
+ "this machine; removing what ACC recorded writing"]),
|
|
210
219
|
...(deliverySummary === null ? [] : [deliverySummary]),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AccError, EXIT, assertMatchableResource, assertPortableId,
|
|
2
|
+
assertResourceUri, normaliseResource } from "@agents-can-communicate/protocol";
|
|
3
|
+
|
|
4
|
+
const invalid = message => { throw new AccError(EXIT.USAGE, message); };
|
|
5
|
+
|
|
6
|
+
function resourceSelector(resource) {
|
|
7
|
+
assertMatchableResource(resource, invalid);
|
|
8
|
+
return assertResourceUri(normaliseResource(resource), "resource");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function claimIdSelector(claimId, field) {
|
|
12
|
+
try { return assertPortableId(claimId, field); }
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (!(error instanceof AccError)) throw error;
|
|
15
|
+
invalid(`${field} must be a portable identifier`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// URI normalization and identifier validation must precede resolving a session:
|
|
20
|
+
// an invalid selector must not create presence or a persistent session binding.
|
|
21
|
+
export function validateClaimSelectors(name, args) {
|
|
22
|
+
if (name !== "acc_claim" && name !== "acc_release") return args;
|
|
23
|
+
const result = { ...args };
|
|
24
|
+
if (args.resource !== undefined) result.resource = resourceSelector(args.resource);
|
|
25
|
+
if (args.resources !== undefined) {
|
|
26
|
+
result.resources = args.resources.map(resourceSelector);
|
|
27
|
+
if (new Set(result.resources).size !== result.resources.length) {
|
|
28
|
+
invalid("arguments.resources contains duplicate selectors after normalization");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (args.claimId !== undefined) claimIdSelector(args.claimId, "arguments.claimId");
|
|
32
|
+
args.claimIds?.forEach((id, index) => claimIdSelector(id, `arguments.claimIds[${index}]`));
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function claimTool(service, owner, args) {
|
|
37
|
+
if (args.action === "renew") return service.renewClaim({ ...owner,
|
|
38
|
+
claimId: args.claimId, leaseSeconds: args.leaseSeconds });
|
|
39
|
+
const input = { ...owner, mode: args.mode ?? "exclusive", enforcement: "advisory",
|
|
40
|
+
reason: args.reason ?? "unspecified", leaseSeconds: args.leaseSeconds };
|
|
41
|
+
if (args.resources !== undefined) {
|
|
42
|
+
return { claims: await service.acquireClaims({ ...input, resources: args.resources }) };
|
|
43
|
+
}
|
|
44
|
+
return service.acquireClaim({ ...input, resource: args.resource });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function releaseTool(service, owner, args) {
|
|
48
|
+
if (args.claimId !== undefined) {
|
|
49
|
+
await service.releaseClaim({ ...owner, claimId: args.claimId });
|
|
50
|
+
return { released: args.claimId };
|
|
51
|
+
}
|
|
52
|
+
const selected = args.resource !== undefined ? { resources: [args.resource] } : args;
|
|
53
|
+
const claims = await service.releaseClaims({ ...owner, ...selected });
|
|
54
|
+
const released = claims.map(claim => claim.claimId);
|
|
55
|
+
return { released: args.resource !== undefined ? released[0] : released };
|
|
56
|
+
}
|
|
@@ -43,6 +43,12 @@ function validateSchema(schema, value, path) {
|
|
|
43
43
|
}
|
|
44
44
|
} else if (schema.type === "array") {
|
|
45
45
|
if (!Array.isArray(value)) fail(path, "must be an array");
|
|
46
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
47
|
+
fail(path, `must contain at least ${schema.minItems} item(s)`);
|
|
48
|
+
}
|
|
49
|
+
if (schema.uniqueItems === true && new Set(value).size !== value.length) {
|
|
50
|
+
fail(path, "must contain unique items");
|
|
51
|
+
}
|
|
46
52
|
if (schema.items !== undefined) {
|
|
47
53
|
value.forEach((item, index) => validateSchema(schema.items, item, `${path}[${index}]`));
|
|
48
54
|
}
|
|
@@ -53,6 +59,9 @@ function validateSchema(schema, value, path) {
|
|
|
53
59
|
} else if (schema.type === "integer" && !Number.isInteger(value)) {
|
|
54
60
|
fail(path, "must be an integer");
|
|
55
61
|
}
|
|
62
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
63
|
+
fail(path, `must contain at least ${schema.minLength} character(s)`);
|
|
64
|
+
}
|
|
56
65
|
|
|
57
66
|
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
58
67
|
fail(path, `must be at least ${schema.minimum}`);
|
|
@@ -5,6 +5,7 @@ import { clearSessionBinding, loadSessionBinding, storeSessionBinding }
|
|
|
5
5
|
|
|
6
6
|
import { readResource } from "./resources.mjs";
|
|
7
7
|
import { validateToolInput } from "./input-validator.mjs";
|
|
8
|
+
import { claimTool, releaseTool, validateClaimSelectors } from "./claim-tools.mjs";
|
|
8
9
|
import { PUBLIC_TOOLS, RESOURCES } from "./tools.mjs";
|
|
9
10
|
import { createProtocol } from "./protocol.mjs";
|
|
10
11
|
|
|
@@ -133,14 +134,9 @@ async function callTool(name, args, context) {
|
|
|
133
134
|
return service.setIntent({ ...owner, summary: args.summary, mode: args.mode,
|
|
134
135
|
state: args.state, resourceHints: args.resourceHints ?? [] });
|
|
135
136
|
case "acc_claim":
|
|
136
|
-
|
|
137
|
-
claimId: args.claimId, leaseSeconds: args.leaseSeconds });
|
|
138
|
-
return service.acquireClaim({ ...owner, resource: args.resource,
|
|
139
|
-
mode: args.mode ?? "exclusive", enforcement: "advisory",
|
|
140
|
-
reason: args.reason ?? "unspecified", leaseSeconds: args.leaseSeconds });
|
|
137
|
+
return claimTool(service, owner, args);
|
|
141
138
|
case "acc_release":
|
|
142
|
-
|
|
143
|
-
return { released: args.claimId };
|
|
139
|
+
return releaseTool(service, owner, args);
|
|
144
140
|
case "acc_message": {
|
|
145
141
|
const kind = args.kind ?? "note";
|
|
146
142
|
const toParticipantIds = args.to ?? [];
|
|
@@ -219,13 +215,16 @@ async function handle(message, context) {
|
|
|
219
215
|
throw new AccError(EXIT.USAGE, `unknown tool: ${params.name}`, { name: params.name });
|
|
220
216
|
}
|
|
221
217
|
validateToolInput(tool.inputSchema, args);
|
|
222
|
-
const value = await callTool(params.name, args, context);
|
|
218
|
+
const value = await callTool(params.name, validateClaimSelectors(params.name, args), context);
|
|
223
219
|
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
224
220
|
structuredContent: value };
|
|
225
221
|
} catch (error) {
|
|
226
222
|
// A failing operation is a tool result, not a transport failure: the
|
|
227
223
|
// model must see it and be able to react.
|
|
224
|
+
const batchConflict = params.name === "acc_claim"
|
|
225
|
+
&& params.arguments?.resources !== undefined && error.details?.conflicts !== undefined;
|
|
228
226
|
return { isError: true,
|
|
227
|
+
...(batchConflict ? { structuredContent: { code: error.code, details: error.details } } : {}),
|
|
229
228
|
content: [{ type: "text", text: `${params.name}: ${error.message}` }] };
|
|
230
229
|
}
|
|
231
230
|
}
|
|
@@ -19,6 +19,10 @@ const object = (properties, required = []) => ({
|
|
|
19
19
|
|
|
20
20
|
const string = description => ({ type: "string", description });
|
|
21
21
|
const stringList = description => ({ type: "array", items: { type: "string" }, description });
|
|
22
|
+
const selector = description => ({ ...string(description), minLength: 1 });
|
|
23
|
+
const selectors = description => ({ type: "array", items: { type: "string", minLength: 1 },
|
|
24
|
+
minItems: 1, uniqueItems: true, description });
|
|
25
|
+
const releaseFields = ["claimId", "claimIds", "resource", "resources", "all"];
|
|
22
26
|
|
|
23
27
|
export const PUBLIC_TOOLS = Object.freeze([
|
|
24
28
|
{
|
|
@@ -68,31 +72,45 @@ export const PUBLIC_TOOLS = Object.freeze([
|
|
|
68
72
|
},
|
|
69
73
|
{
|
|
70
74
|
name: "acc_claim",
|
|
71
|
-
description: `Acquire or
|
|
75
|
+
description: `Acquire one resource or atomically acquire a nonempty resources array; renew by claimId. `
|
|
76
|
+
+ `resource returns a Claim; resources returns {claims: [...]}, including one-item arrays. `
|
|
77
|
+
+ `A conflicting batch acquires nothing and lists every conflicting holder in structuredContent.details.conflicts. Claims are `
|
|
72
78
|
+ `workspace-wide and advisory here: this client has no write guard, so a claim `
|
|
73
79
|
+ `informs peers rather than preventing an edit. ${MUTATION_POLL}`,
|
|
74
80
|
inputSchema: { ...object({
|
|
75
|
-
resource:
|
|
81
|
+
resource: selector("One resource URI, exclusive with resources; for example file:packages/core/**."),
|
|
82
|
+
resources: selectors("Resource URIs for one atomic acquisition; unique after normalization and exclusive with resource."),
|
|
76
83
|
action: { type: "string", enum: ["acquire", "renew"] },
|
|
77
84
|
mode: { type: "string", enum: ["shared", "exclusive"] },
|
|
78
85
|
reason: string("Why the resource is being claimed."),
|
|
79
86
|
leaseSeconds: { type: "integer", minimum: 1,
|
|
80
87
|
description: "Lease length; the claim expires without renewal." },
|
|
81
|
-
claimId:
|
|
88
|
+
claimId: selector("Required for renew; renew accepts one claimId only."),
|
|
82
89
|
}, ["action"]), oneOf: [
|
|
83
90
|
{ properties: { action: { const: "acquire" } }, required: ["resource"],
|
|
84
|
-
not: {
|
|
91
|
+
not: { anyOf: ["resources", "claimId"].map(field => ({ required: [field] })) } },
|
|
92
|
+
{ properties: { action: { const: "acquire" } }, required: ["resources"],
|
|
93
|
+
not: { anyOf: ["resource", "claimId"].map(field => ({ required: [field] })) } },
|
|
85
94
|
{ properties: { action: { const: "renew" } }, required: ["claimId"],
|
|
86
|
-
not: { anyOf: ["resource", "mode", "reason"]
|
|
95
|
+
not: { anyOf: ["resource", "resources", "mode", "reason"]
|
|
87
96
|
.map(field => ({ required: [field] })) } },
|
|
88
97
|
] },
|
|
89
98
|
},
|
|
90
99
|
{
|
|
91
100
|
name: "acc_release",
|
|
92
|
-
description: `Release
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
description: `Release only claims this session owns, with exactly one of claimId, claimIds, resource, `
|
|
102
|
+
+ `resources, or all: true. Arrays release atomically. Scalar selectors return {released: id}; `
|
|
103
|
+
+ `arrays and all return {released: [ids]}, including an empty all result. `
|
|
104
|
+
+ `all keeps this session's presence and intent open. ${MUTATION_POLL}`,
|
|
105
|
+
inputSchema: { ...object({
|
|
106
|
+
claimId: selector("One claim to release."),
|
|
107
|
+
claimIds: selectors("Claim IDs to release atomically."),
|
|
108
|
+
resource: selector("One exact resource URI this session holds."),
|
|
109
|
+
resources: selectors("Exact resource URIs to release atomically; unique after normalization."),
|
|
110
|
+
all: { type: "boolean", const: true, description: "Release every claim owned by this session." },
|
|
111
|
+
}), oneOf: releaseFields.map(field => ({ required: [field],
|
|
112
|
+
not: { anyOf: releaseFields.filter(other => other !== field)
|
|
113
|
+
.map(other => ({ required: [other] })) } })) },
|
|
96
114
|
},
|
|
97
115
|
{
|
|
98
116
|
name: "acc_message",
|
|
@@ -11,5 +11,6 @@ export { CONFIG_FILENAME, CONFIG_SCHEMA_VERSION, RUNTIME_KEYS, defaultProjectCon
|
|
|
11
11
|
validateProjectConfig } from "./config.mjs";
|
|
12
12
|
export { RECEIPT_STATES, advanceReceipt } from "./states.mjs";
|
|
13
13
|
export { assertMatchableResource, normaliseResource } from "./resources.mjs";
|
|
14
|
+
export { resourceUri as assertResourceUri } from "./fields.mjs";
|
|
14
15
|
export { assertDecisionChange } from "./decision-changes.mjs";
|
|
15
16
|
export { TRANSACTION_INDEXES, assertIndexTuple, indexKeysFor } from "./transaction-indexes.mjs";
|
|
@@ -3,6 +3,6 @@ export { openFilesystemStore, storePaths, ZERO_CURSOR } from "./store.mjs";
|
|
|
3
3
|
export { diagnoseFilesystemStore, repairFilesystemStore } from "./recovery.mjs";
|
|
4
4
|
export { readStoreIdentity, requireStoreIdentity, STORE_VERSION } from "./identity.mjs";
|
|
5
5
|
export { withWriterMutex } from "./writer-mutex.mjs";
|
|
6
|
-
export { readSessionRecord } from "./session-read.mjs";
|
|
6
|
+
export { readSessionRecord, readDeliveryBindingRecord } from "./session-read.mjs";
|
|
7
7
|
export { openNoFollow } from "./portable-fs.mjs";
|
|
8
8
|
export { migrateFilesystemStore } from "./store-migration.mjs";
|