agents-can-communicate 0.10.3 → 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/TROUBLESHOOTING.md +10 -0
- package/docs/UPGRADING.md +16 -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 +48 -5
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +9 -6
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +16 -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/native-vocabulary.mjs +2 -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 +8 -54
- 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/cli/src/native-session-diagnostics.mjs +2 -0
- 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/refresh-binding.mjs +9 -6
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +19 -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/delivery-diagnostics.mjs +1 -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
|
@@ -6,6 +6,7 @@ import { AccError, CONFIG_FILENAME, EXIT, GENERIC_MESSAGE_KINDS, VALID_OBLIGATIO
|
|
|
6
6
|
failure, ok }
|
|
7
7
|
from "@agents-can-communicate/protocol";
|
|
8
8
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
9
|
+
import { shellQuote } from "@agents-can-communicate/adapter-sdk";
|
|
9
10
|
import { describeNativeReason } from "@agents-can-communicate/installer";
|
|
10
11
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
11
12
|
|
|
@@ -22,7 +23,7 @@ import { runDoctor } from "./doctor-command.mjs";
|
|
|
22
23
|
import { runManagementDoctor } from "./managed-runtime/diagnostics.mjs";
|
|
23
24
|
import { runStoreMigration } from "./store-migration-command.mjs";
|
|
24
25
|
import { createGitProbe } from "./git-probe.mjs";
|
|
25
|
-
import {
|
|
26
|
+
import { runClaimCommand, runReleaseCommand } from "./claim-command.mjs";
|
|
26
27
|
import { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
|
|
27
28
|
import { resolveOwner } from "./session-owner.mjs";
|
|
28
29
|
import { discoverWorkspace } from "./workspace-discovery.mjs";
|
|
@@ -30,39 +31,6 @@ import { resolveSavedHookWorkspace } from "./hook-workspace.mjs";
|
|
|
30
31
|
|
|
31
32
|
const DEFAULT_CADENCE_MS = 30_000;
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
-
* The claim a caller means when they name a resource instead of an id.
|
|
35
|
-
*
|
|
36
|
-
* `acc claim` takes `--resource`; `acc release` took `--claim`. The asymmetry
|
|
37
|
-
* cost every caller - a person and an agent alike - a round trip through
|
|
38
|
-
* `acc status --json` to find an id they never chose. The id is still accepted,
|
|
39
|
-
* and is still the precise answer when two sessions hold the same resource and
|
|
40
|
-
* an authority is releasing someone else's.
|
|
41
|
-
*/
|
|
42
|
-
async function claimOn(options, context) {
|
|
43
|
-
if (options.resource === undefined) {
|
|
44
|
-
throw new AccError(EXIT.USAGE,
|
|
45
|
-
"release needs the claim to give back: --resource is what you claimed, "
|
|
46
|
-
+ "--claim is its id");
|
|
47
|
-
}
|
|
48
|
-
const resource = await canonicalClaim(options.resource, context.descriptor);
|
|
49
|
-
const { claims } = await context.service.collectStatus({});
|
|
50
|
-
// An authority releasing another session's claim is not looking for its own.
|
|
51
|
-
const mine = claims.filter(claim => claim.resource === resource
|
|
52
|
-
&& (options.authority !== undefined || claim.ownerSessionId === options.session));
|
|
53
|
-
|
|
54
|
-
if (mine.length === 0) {
|
|
55
|
-
throw new AccError(EXIT.DATA, `no claim on ${resource} to release`,
|
|
56
|
-
{ resource, held: claims.map(claim => claim.resource) });
|
|
57
|
-
}
|
|
58
|
-
if (mine.length > 1) {
|
|
59
|
-
throw new AccError(EXIT.DATA,
|
|
60
|
-
`${mine.length} claims on ${resource}; name the one you mean with --claim`,
|
|
61
|
-
{ resource, claims: mine.map(claim => claim.claimId) });
|
|
62
|
-
}
|
|
63
|
-
return mine[0].claimId;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
34
|
/**
|
|
67
35
|
* Where this workspace's state lives, without reading a byte of it.
|
|
68
36
|
*
|
|
@@ -162,7 +130,10 @@ export function recordedText(message, delivery) {
|
|
|
162
130
|
? `woke ${item.recipientParticipantId} via ${item.transport}; the message arrives with its next turn`
|
|
163
131
|
: item.outcome === "queued"
|
|
164
132
|
? item.errorCode === "transport_permission_denied"
|
|
165
|
-
? "live offer blocked by sender permissions; message remains queued;
|
|
133
|
+
? "live offer blocked by sender permissions; message remains queued; retry the same command "
|
|
134
|
+
+ "through approved execution"
|
|
135
|
+
+ (typeof message.clientMessageId === "string"
|
|
136
|
+
? ` with --client-message-id ${shellQuote(message.clientMessageId)}` : " with its original retry key")
|
|
166
137
|
: item.errorCode === "no_live_transport"
|
|
167
138
|
? `${item.recipientParticipantId} has no live transport`
|
|
168
139
|
+ `${item.nativeReasonCode ? ` (${describeNativeReason(item.nativeReasonCode,
|
|
@@ -323,25 +294,8 @@ const HANDLERS = Object.freeze({
|
|
|
323
294
|
return { data: intent, text: `intent: ${intent.summary}` };
|
|
324
295
|
},
|
|
325
296
|
|
|
326
|
-
claim:
|
|
327
|
-
|
|
328
|
-
generation: options.generation,
|
|
329
|
-
resource: await canonicalClaim(options.resource, context.descriptor),
|
|
330
|
-
mode: options.mode ?? "exclusive", enforcement: options.enforcement ?? "advisory",
|
|
331
|
-
reason: options.reason ?? "unspecified",
|
|
332
|
-
leaseSeconds: options.lease ? positiveNumber(options.lease, "lease") : undefined,
|
|
333
|
-
descriptor: context.descriptor });
|
|
334
|
-
return { data: claim, text: `claimed ${claim.resource}` };
|
|
335
|
-
},
|
|
336
|
-
|
|
337
|
-
release: async ({ options, context }) => {
|
|
338
|
-
const claimId = options.claim ?? await claimOn(options, context);
|
|
339
|
-
const request = { claimId, sessionId: options.session,
|
|
340
|
-
generation: options.generation, authority: options.authority, reason: options.reason };
|
|
341
|
-
if (options.authority === undefined) await context.service.releaseClaim(request);
|
|
342
|
-
else await context.service.forceReleaseClaim(request);
|
|
343
|
-
return { data: { claimId }, text: `released ${claimId}` };
|
|
344
|
-
},
|
|
297
|
+
claim: runClaimCommand,
|
|
298
|
+
release: runReleaseCommand,
|
|
345
299
|
|
|
346
300
|
message: async ({ options, context }) => {
|
|
347
301
|
const kind = options.type ?? "note";
|
|
@@ -32,6 +32,25 @@ export function invokedDirectly(url) {
|
|
|
32
32
|
catch { return false; }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** The display is an observation, not an admitted runtime user. A stable
|
|
36
|
+
* launcher resolves the active generation on every refresh, so an old plugin
|
|
37
|
+
* never retains a path that a same-contract update may reclaim. A concurrent
|
|
38
|
+
* replacement fails open and the next display refresh tries the new pointer. */
|
|
39
|
+
export async function runIndicatorEntry({ managerRoot }) {
|
|
40
|
+
try {
|
|
41
|
+
const root = await canonicalManagerRoot(managerRoot);
|
|
42
|
+
const control = await readControl(root);
|
|
43
|
+
if (!control) throw new Error("indicator runtime missing");
|
|
44
|
+
await import(pathToFileURL(path.join(control.active.root, "bin", "acc-indicator.mjs")).href);
|
|
45
|
+
} catch {
|
|
46
|
+
const report = { health: "problem", reception: null, reasonCode: "indicator_unavailable",
|
|
47
|
+
label: "ACC !", detail: "ACC's installed indicator is unavailable.",
|
|
48
|
+
action: "Run acc update to finish runtime activation, or acc install --indicator on to repair it." };
|
|
49
|
+
process.stdout.write(process.argv.includes("--json") ? JSON.stringify(report) + "\n"
|
|
50
|
+
: "ACC ! · acc doctor\n");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
function unavailable(kind) {
|
|
36
55
|
// Never reflect control file content or payloads into a client diagnostic.
|
|
37
56
|
if (kind === "acc-hook" || kind === "acc-antigravity-relay") {
|
|
@@ -16,6 +16,7 @@ async function durableFile(file, bytes, mode = 0o600) {
|
|
|
16
16
|
export const stablePaths = root => ({
|
|
17
17
|
cli: path.join(root, "bin", "acc.mjs"), runner: path.join(root, "bin", "acc-hook.mjs"),
|
|
18
18
|
antigravityRelay: path.join(root, "bin", "acc-antigravity-relay.mjs"),
|
|
19
|
+
indicatorRunner: path.join(root, "bin", "acc-indicator.mjs"),
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
/** Publish immutable launcher modules, then atomically replace each tiny entry.
|
|
@@ -47,9 +48,10 @@ export async function writeLaunchers(root, packageRoot) {
|
|
|
47
48
|
} finally { await removeTree(staging); }
|
|
48
49
|
const bin = path.join(root, "bin");
|
|
49
50
|
await managedDirectory(bin, { create: true });
|
|
50
|
-
for (const kind of ENTRY_KINDS) {
|
|
51
|
-
const
|
|
52
|
-
|
|
51
|
+
for (const kind of [...ENTRY_KINDS, "acc-indicator"]) {
|
|
52
|
+
const entry = kind === "acc-indicator" ? "runIndicatorEntry" : "runEntry";
|
|
53
|
+
const bytes = `#!/usr/bin/env node\nimport { ${entry} } from "../launchers/${id}/entry.mjs";\n`
|
|
54
|
+
+ `await ${entry}(${JSON.stringify({ kind, packageRoot, managerRoot: root, managedRequired: true })});\n`;
|
|
53
55
|
const temporary = path.join(bin, `.${randomUUID()}.tmp`);
|
|
54
56
|
try {
|
|
55
57
|
await durableFile(temporary, bytes, 0o700);
|
|
@@ -97,6 +97,8 @@ export async function updateNativeSessions(adapters, { service, status, root, no
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const failures = {
|
|
100
|
+
transport_permission_denied: "this command cannot access the local delivery channel; "
|
|
101
|
+
+ "check the sender's command permissions",
|
|
100
102
|
client_process_unknown: "the client process could not be identified; start a new client session",
|
|
101
103
|
handshake_failed: "the session handshake failed; check the client's integration/channel setup",
|
|
102
104
|
handshake_timeout: "the session handshake timed out; check the local channel and retry on the next turn",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AccError, EXIT, assertMatchableResource, assertPortableId,
|
|
2
|
+
assertResourceUri, normaliseResource } from "@agents-can-communicate/protocol";
|
|
3
|
+
|
|
4
|
+
const usage = message => { throw new AccError(EXIT.USAGE, message); };
|
|
5
|
+
|
|
6
|
+
export function uniqueList(value, name, normalise = item => item) {
|
|
7
|
+
if (!Array.isArray(value) || value.length === 0) usage(`${name} must be a nonempty array`);
|
|
8
|
+
const result = value.map(item => {
|
|
9
|
+
if (typeof item !== "string" || item.length === 0) usage(`${name} must contain nonempty strings`);
|
|
10
|
+
return normalise(item);
|
|
11
|
+
});
|
|
12
|
+
if (new Set(result).size !== result.length) usage(`${name} contains duplicate selectors`);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function claimResources(value) {
|
|
17
|
+
return uniqueList(value, "resources", resource => {
|
|
18
|
+
assertMatchableResource(resource, usage);
|
|
19
|
+
const normalised = normaliseResource(resource);
|
|
20
|
+
try { return assertResourceUri(normalised, "resource"); }
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (!(error instanceof AccError)) throw error;
|
|
23
|
+
throw new AccError(EXIT.USAGE, error.message, error.details);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function releaseSelectors(input) {
|
|
29
|
+
const present = ["claimIds", "resources", "all"].filter(key => input[key] !== undefined);
|
|
30
|
+
if (present.length !== 1 || (present[0] === "all" && input.all !== true)) {
|
|
31
|
+
usage("release needs exactly one of claimIds, resources, or all: true");
|
|
32
|
+
}
|
|
33
|
+
if (input.authority !== undefined) usage("batch release does not accept force-release authority");
|
|
34
|
+
if (input.all === true) return { all: true };
|
|
35
|
+
if (input.resources !== undefined) return { resources: claimResources(input.resources) };
|
|
36
|
+
return { claimIds: uniqueList(input.claimIds, "claimIds", id => {
|
|
37
|
+
try { return assertPortableId(id, "claim id"); }
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (!(error instanceof AccError)) throw error;
|
|
40
|
+
throw new AccError(EXIT.USAGE, error.message, error.details);
|
|
41
|
+
}
|
|
42
|
+
}) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function selectReleaseClaims(tx, selectors, sessionId) {
|
|
46
|
+
if (selectors.all === true) return tx.list("claim", claim => claim.ownerSessionId === sessionId);
|
|
47
|
+
if (selectors.claimIds !== undefined) return selectors.claimIds.map(claimId => {
|
|
48
|
+
const claim = tx.get("claim", claimId);
|
|
49
|
+
if (claim === null) throw new AccError(EXIT.CONFLICT, "the claim does not exist", { claimId });
|
|
50
|
+
return claim;
|
|
51
|
+
});
|
|
52
|
+
const claims = tx.list("claim");
|
|
53
|
+
return selectors.resources.map(resource => {
|
|
54
|
+
const mine = claims.filter(claim => claim.resource === resource && claim.ownerSessionId === sessionId);
|
|
55
|
+
if (mine.length === 0) throw new AccError(EXIT.DATA, `no claim on ${resource} to release`,
|
|
56
|
+
{ resource, held: claims.map(claim => claim.resource) });
|
|
57
|
+
if (mine.length > 1) throw new AccError(EXIT.DATA,
|
|
58
|
+
`${mine.length} claims on ${resource}; name the one you mean with --claim`,
|
|
59
|
+
{ resource, claims: mine.map(claim => claim.claimId) });
|
|
60
|
+
return mine[0];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -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,6 +1,7 @@
|
|
|
1
1
|
import { validateNativeHandshake } from "@agents-can-communicate/adapter-sdk";
|
|
2
2
|
|
|
3
3
|
const MAX_REFRESH_LEASE_MS = 120_000;
|
|
4
|
+
const refused = (errorCode = "recipient_unavailable") => ({ refreshed: false, errorCode });
|
|
4
5
|
|
|
5
6
|
function sameBinding(current, expected, leaseUntil) {
|
|
6
7
|
return current !== null && current !== undefined
|
|
@@ -15,24 +16,26 @@ function sameBinding(current, expected, leaseUntil) {
|
|
|
15
16
|
|
|
16
17
|
export async function refreshExpiredBinding({ service, adapter, binding, runtimeDir,
|
|
17
18
|
clock, timeoutMs = 750 }) {
|
|
18
|
-
if (typeof adapter?.refreshNativeSession !== "function") return
|
|
19
|
+
if (typeof adapter?.refreshNativeSession !== "function") return refused();
|
|
19
20
|
const now = Date.parse(clock.now());
|
|
20
|
-
if (!Number.isFinite(now)) return
|
|
21
|
+
if (!Number.isFinite(now)) return refused();
|
|
21
22
|
try {
|
|
22
23
|
const handshake = await adapter.refreshNativeSession({ binding, runtimeDir, timeoutMs });
|
|
23
24
|
const validated = validateNativeHandshake(adapter, {
|
|
24
25
|
clientVersion: binding.clientVersion, handshake,
|
|
25
26
|
});
|
|
26
|
-
if (!validated.ok
|
|
27
|
+
if (!validated.ok) return refused(validated.reasonCode === "transport_permission_denied"
|
|
28
|
+
? "transport_permission_denied" : "recipient_unavailable");
|
|
29
|
+
if (validated.opaqueEndpointRef !== binding.opaqueEndpointRef) return refused();
|
|
27
30
|
const requestedLease = Date.parse(validated.leaseUntil);
|
|
28
|
-
if (!Number.isFinite(requestedLease) || requestedLease <= now) return
|
|
31
|
+
if (!Number.isFinite(requestedLease) || requestedLease <= now) return refused();
|
|
29
32
|
const leaseUntil = new Date(Math.min(requestedLease,
|
|
30
33
|
now + MAX_REFRESH_LEASE_MS)).toISOString();
|
|
31
34
|
await service.refreshDeliveryBinding({ sessionId: binding.sessionId,
|
|
32
35
|
generation: binding.generation, leaseUntil });
|
|
33
36
|
const current = await service.store?.ephemeral?.get("deliveryBinding", binding.sessionId);
|
|
34
|
-
return sameBinding(current, binding, leaseUntil);
|
|
37
|
+
return sameBinding(current, binding, leaseUntil) ? { refreshed: true, errorCode: null } : refused();
|
|
35
38
|
} catch {
|
|
36
|
-
return
|
|
39
|
+
return refused();
|
|
37
40
|
}
|
|
38
41
|
}
|
|
@@ -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) {
|
|
@@ -132,7 +139,12 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
132
139
|
if (Date.parse(binding.leaseUntil) <= Date.parse(now)) {
|
|
133
140
|
const refreshed = await refreshExpiredBinding({ service, adapter, binding,
|
|
134
141
|
runtimeDir: service.store?.root, clock });
|
|
135
|
-
if (!refreshed)
|
|
142
|
+
if (!refreshed.refreshed) {
|
|
143
|
+
if (refreshed.errorCode === "transport_permission_denied") {
|
|
144
|
+
await recordFailure(binding, message, participantId, "live-adapter", refreshed.errorCode);
|
|
145
|
+
} else observeOffer(binding, refreshed.errorCode);
|
|
146
|
+
return durable(participantId, refreshed.errorCode);
|
|
147
|
+
}
|
|
136
148
|
const current = (await service.listDeliveryBindings({
|
|
137
149
|
participantId, now: clock.now() })).filter(item => item.sessionId === binding.sessionId
|
|
138
150
|
&& item.generation === binding.generation
|
|
@@ -211,6 +223,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
211
223
|
// shown and leave it out of the very turn the wake started. The receipt
|
|
212
224
|
// stays queued; the hook records the offer once its stdout carried the body.
|
|
213
225
|
if (adapter.nativeDelivery?.offerKind === "wake") {
|
|
226
|
+
observeOffer(binding, response.pendingApproval === true ? "inbound_approval_required" : null);
|
|
214
227
|
// The client took the wake but holds it for its user's approval.
|
|
215
228
|
return { recipientParticipantId: participantId, outcome: "woken", transport,
|
|
216
229
|
...(response.pendingApproval === true ? { pendingApproval: true } : {}) };
|
|
@@ -224,6 +237,7 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
224
237
|
await recordFailure(binding, message, participantId, transport, "transport_error");
|
|
225
238
|
return durable(participantId, "transport_error");
|
|
226
239
|
}
|
|
240
|
+
observeOffer(binding);
|
|
227
241
|
return { recipientParticipantId: participantId, outcome: "offered", transport };
|
|
228
242
|
}
|
|
229
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 ?? []),
|
|
@@ -10,6 +10,7 @@ const REASONS = Object.freeze({
|
|
|
10
10
|
known_bad_version: "this client version has a known native delivery failure",
|
|
11
11
|
native_endpoint_unavailable: "the client's local delivery service is unavailable",
|
|
12
12
|
native_session_unavailable: "the local delivery service has no loaded client session",
|
|
13
|
+
transport_permission_denied: "the calling process lacks permission to access the local delivery channel",
|
|
13
14
|
feature_probe_failed: "the native protocol probe did not succeed",
|
|
14
15
|
probe_timeout: "the native protocol probe timed out",
|
|
15
16
|
probe_version_mismatch: "the local service and CLI versions differ",
|
|
@@ -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
|
}),
|