sentinelayer-cli 0.42.0 → 0.44.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 +47 -1
- package/package.json +1 -1
- package/src/commands/session.js +435 -12
- package/src/mcp/cli-command-tools.js +3 -0
- package/src/mcp/registry.js +4 -4
- package/src/mcp/session-stdio-server.js +76 -6
- package/src/session/admission-access.js +284 -0
- package/src/session/admission-auth.js +8 -1
- package/src/session/admission.js +60 -4
- package/src/session/control-events.js +2 -0
- package/src/session/reactions.js +258 -0
- package/src/session/sync.js +41 -11
package/README.md
CHANGED
|
@@ -163,10 +163,56 @@ sl session join <session-id> --agent <agent-name>
|
|
|
163
163
|
sl session action <session-id> working_on --target-sequence <n>
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
For a room that requires agent admission, the agent declares its goal, requested
|
|
167
|
+
actions, and TTL. The CLI creates an agent-held Ed25519 identity and waits for a
|
|
168
|
+
human decision. When the API reports identity readiness on the approved
|
|
169
|
+
admission, the CLI also waits for the minted AIdenID email and verified signed
|
|
170
|
+
purpose receipt before it claims; when the API does not report it, the approved
|
|
171
|
+
grant is claimed directly. The claim uses proof of possession, then the agent
|
|
172
|
+
joins. The private key and scoped credential are stored locally and are never
|
|
173
|
+
printed:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Agent: request and wait for the owner's decision (maximum grant TTL is 24h).
|
|
177
|
+
sl session join <session-id> --agent review-agent \
|
|
178
|
+
--goal "Review the release and report findings" \
|
|
179
|
+
--scope session.read,session.post --ttl 24h
|
|
180
|
+
|
|
181
|
+
# Or request without waiting, for an asynchronous handoff.
|
|
182
|
+
sl session access request <session-id> --agent review-agent \
|
|
183
|
+
--goal "Review the release and report findings" \
|
|
184
|
+
--scope session.read,session.post --ttl 24h --json
|
|
185
|
+
|
|
186
|
+
# Owner/admin: inspect the untrusted goal claim, then approve, narrow, or deny.
|
|
187
|
+
sl session access mode <session-id> required
|
|
188
|
+
sl session access list <session-id> --status pending
|
|
189
|
+
sl session access approve <session-id> <admission-id> \
|
|
190
|
+
--scope session.read,session.post --ttl 24h
|
|
191
|
+
# sl session access deny <session-id> <admission-id> --note "reason"
|
|
192
|
+
# sl session access revoke <session-id> <admission-id> --reason "work complete"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`session access list --json` reports the `email`, `purpose`, and Jev status the
|
|
196
|
+
API returns; the CLI does not invent successful issuance. On an API that gates
|
|
197
|
+
the claim on identity evidence, approval queues the AIdenID email and
|
|
198
|
+
purpose-receipt work and the claim challenge is withheld until both are ready.
|
|
199
|
+
On an API that does not, approval issues the claim challenge directly.
|
|
200
|
+
The agent must re-run `session join` with the same goal, scope, and TTL after an
|
|
201
|
+
asynchronous request so it can claim and verify its live session-bound receipt.
|
|
202
|
+
A room's admission mode is whatever the API stored for it; `session access list`
|
|
203
|
+
shows it, and an owner changes it with `session access mode <session-id>
|
|
204
|
+
required` (or `legacy`).
|
|
205
|
+
|
|
206
|
+
The owner actions (`approve`, `deny`, `revoke`, `mode`) run on the owner's own
|
|
207
|
+
session. They are unavailable in an agent context, meaning `SENTINELAYER_AGENT_ID`
|
|
208
|
+
is set or agent admission credentials are stored on the machine; owners use the
|
|
209
|
+
web dashboard from such machines. `session access` commands are not exposed
|
|
210
|
+
through the MCP CLI bridge.
|
|
211
|
+
|
|
166
212
|
Sentinelayer includes a deterministic session coordination surface for multi-agent coding loops:
|
|
167
213
|
|
|
168
214
|
- session event stream and replay (`start`, `join`, `say`, `read`, `status`, `leave`, `list`, `kill`)
|
|
169
|
-
- low-noise message actions (`react ack|like|dislike`, `action working_on|disregard`, `reply`/`comment`, `actions`) plus one monotonic per-agent read cursor (`view`)
|
|
215
|
+
- low-noise message actions (`react ack|like|dislike|unlike|undislike`, `action working_on|disregard`, `reply`/`comment`, `actions`) plus one monotonic per-agent read cursor (`view`)
|
|
170
216
|
- agent lifecycle controls (join/ephemeral TTL presence/leave/kill); listener liveness never enters the durable transcript
|
|
171
217
|
- recap and context briefing for late-joining agents
|
|
172
218
|
- analytics + lineage artifacts at session closeout
|
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -98,12 +98,24 @@ import {
|
|
|
98
98
|
parseTtlSeconds as parseAdmissionTtlSeconds,
|
|
99
99
|
runAdmissionJoin,
|
|
100
100
|
} from "../session/admission.js";
|
|
101
|
+
import {
|
|
102
|
+
decideSessionAdmission,
|
|
103
|
+
getSessionAdmission,
|
|
104
|
+
listSessionAdmissions,
|
|
105
|
+
revokeSessionAdmission,
|
|
106
|
+
setSessionAdmissionMode,
|
|
107
|
+
} from "../session/admission-access.js";
|
|
101
108
|
import {
|
|
102
109
|
assertCanonicalAgentId,
|
|
103
110
|
canonicalAgentId,
|
|
104
111
|
currentAdmittedAgent,
|
|
105
112
|
withAgentAdmission,
|
|
106
113
|
} from "../session/admission-auth.js";
|
|
114
|
+
import {
|
|
115
|
+
SESSION_REACTION_TYPES,
|
|
116
|
+
isSessionReactionType,
|
|
117
|
+
submitSessionReaction,
|
|
118
|
+
} from "../session/reactions.js";
|
|
107
119
|
import {
|
|
108
120
|
claimTicket,
|
|
109
121
|
listTickets,
|
|
@@ -557,10 +569,16 @@ const SESSION_MESSAGE_ACTION_TYPES = new Set([
|
|
|
557
569
|
"reply",
|
|
558
570
|
"like",
|
|
559
571
|
"dislike",
|
|
572
|
+
"unlike",
|
|
573
|
+
"undislike",
|
|
560
574
|
"disregard",
|
|
561
575
|
"view",
|
|
562
576
|
]);
|
|
563
577
|
|
|
578
|
+
// like/dislike/unlike/undislike all go through src/session/reactions.js, the path the
|
|
579
|
+
// local MCP server shares; `ack` keeps the plain message-action path.
|
|
580
|
+
const SESSION_REACT_COMMAND_TYPES = Object.freeze(["ack", ...SESSION_REACTION_TYPES]);
|
|
581
|
+
|
|
564
582
|
const SESSION_MESSAGE_ACTION_ALIASES = new Map([
|
|
565
583
|
["comment", "reply"],
|
|
566
584
|
]);
|
|
@@ -592,6 +610,16 @@ const SESSION_MESSAGE_ACTION_DESCRIPTIONS = Object.freeze([
|
|
|
592
610
|
command: "sl session react <id> dislike --target-sequence <n>",
|
|
593
611
|
description: "Negative lightweight feedback. Use --target-action-id <uuid> to react to a threaded reply.",
|
|
594
612
|
},
|
|
613
|
+
{
|
|
614
|
+
type: "unlike",
|
|
615
|
+
command: "sl session react <id> unlike --target-sequence <n>",
|
|
616
|
+
description: "Retract your own active like. Refused when you have none; needs a server with reaction undo.",
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
type: "undislike",
|
|
620
|
+
command: "sl session react <id> undislike --target-sequence <n>",
|
|
621
|
+
description: "Retract your own active dislike. Refused when you have none; needs a server with reaction undo.",
|
|
622
|
+
},
|
|
595
623
|
{
|
|
596
624
|
type: "disregard",
|
|
597
625
|
command: "sl session action <id> disregard --target-sequence <n>",
|
|
@@ -623,7 +651,7 @@ function shortSha256(value) {
|
|
|
623
651
|
|
|
624
652
|
function actionEventType(actionType) {
|
|
625
653
|
if (actionType === "reply") return "session_reply";
|
|
626
|
-
if (actionType
|
|
654
|
+
if (isSessionReactionType(actionType)) return "session_reaction";
|
|
627
655
|
return "session_action";
|
|
628
656
|
}
|
|
629
657
|
|
|
@@ -679,6 +707,7 @@ function buildSessionActionEvent(sessionId, action = {}) {
|
|
|
679
707
|
}),
|
|
680
708
|
);
|
|
681
709
|
const actorId = actionActorId(action);
|
|
710
|
+
const collapsedIntoActionId = normalizeString(action.collapsedIntoActionId ?? action.collapsed_into_action_id);
|
|
682
711
|
const event = createAgentEvent({
|
|
683
712
|
event: actionEventType(actionType),
|
|
684
713
|
agent: {
|
|
@@ -703,6 +732,8 @@ function buildSessionActionEvent(sessionId, action = {}) {
|
|
|
703
732
|
metadata: action.metadata && typeof action.metadata === "object" ? action.metadata : undefined,
|
|
704
733
|
message: actionDisplayMessage(action),
|
|
705
734
|
source: "session_action",
|
|
735
|
+
// A retained no-op reaction key (sentinelayer-api#914): evidence, not a reaction.
|
|
736
|
+
...(collapsedIntoActionId ? { collapsedIntoActionId } : {}),
|
|
706
737
|
},
|
|
707
738
|
});
|
|
708
739
|
event.eventId = `session-action-${id}`;
|
|
@@ -3488,17 +3519,24 @@ export function registerSessionCommand(program) {
|
|
|
3488
3519
|
wait: options.wait !== false,
|
|
3489
3520
|
waitTimeoutMs: (Number.isFinite(waitSeconds) && waitSeconds >= 0 ? waitSeconds : 900) * 1000,
|
|
3490
3521
|
targetPath,
|
|
3491
|
-
onPending: ({ approveUrl }) => {
|
|
3522
|
+
onPending: ({ approveUrl, phase }) => {
|
|
3492
3523
|
// stderr, so --json stdout stays a single parseable document.
|
|
3493
|
-
const line =
|
|
3524
|
+
const line = phase === "identity"
|
|
3525
|
+
? "Approval received; waiting for the AIdenID email and signed purpose receipt."
|
|
3526
|
+
: `Waiting for a room owner to approve this agent: ${terminalText(approveUrl, 1024)}`;
|
|
3494
3527
|
if (emitJson) process.stderr.write(`${line}\n`);
|
|
3495
|
-
|
|
3528
|
+
// Keep the line containing a remote URL free of all terminal control
|
|
3529
|
+
// bytes. `terminalText()` strips controls from the value itself; adding
|
|
3530
|
+
// local ANSI colour around it would make that guarantee untestable and
|
|
3531
|
+
// lets OSC-looking input hide inside a coloured terminal span.
|
|
3532
|
+
else if (phase === "identity") console.log(pc.yellow(line));
|
|
3533
|
+
else console.log(line);
|
|
3496
3534
|
},
|
|
3497
3535
|
});
|
|
3498
3536
|
if (admission.status !== "active") {
|
|
3499
3537
|
// Only an ACTIVE admission joins. Pending, denied, cancelled or expired never
|
|
3500
3538
|
// registers the agent or materializes the room for it.
|
|
3501
|
-
process.exitCode = admission.status
|
|
3539
|
+
process.exitCode = ["pending", "approved"].includes(admission.status) ? 3 : 4;
|
|
3502
3540
|
const payload = {
|
|
3503
3541
|
command: "session join",
|
|
3504
3542
|
joined: false,
|
|
@@ -3510,9 +3548,12 @@ export function registerSessionCommand(program) {
|
|
|
3510
3548
|
if (emitJson) {
|
|
3511
3549
|
console.log(JSON.stringify(payload, null, 2));
|
|
3512
3550
|
} else if (admission.status === "pending") {
|
|
3513
|
-
|
|
3551
|
+
const approval = terminalText(admission.approveUrl, 1024) || "(see the session page)";
|
|
3552
|
+
console.log(`Admission pending. A room owner approves it here: ${approval}`);
|
|
3553
|
+
} else if (admission.status === "approved") {
|
|
3554
|
+
console.log(pc.yellow("Admission approved; AIdenID identity evidence is still provisioning. The agent was not joined."));
|
|
3514
3555
|
} else {
|
|
3515
|
-
console.log(pc.red(`Admission ${admission.status}. The agent was not joined.`));
|
|
3556
|
+
console.log(pc.red(`Admission ${terminalText(admission.status, 16) || "unknown"}. The agent was not joined.`));
|
|
3516
3557
|
}
|
|
3517
3558
|
return;
|
|
3518
3559
|
}
|
|
@@ -4265,6 +4306,21 @@ export function registerSessionCommand(program) {
|
|
|
4265
4306
|
);
|
|
4266
4307
|
}
|
|
4267
4308
|
const agentId = identity.agentId;
|
|
4309
|
+
if (isSessionReactionType(normalizedActionType)) {
|
|
4310
|
+
return runReactionCommand({
|
|
4311
|
+
sessionId: normalizedSessionId,
|
|
4312
|
+
reaction: normalizedActionType,
|
|
4313
|
+
agentId,
|
|
4314
|
+
targetPath,
|
|
4315
|
+
targetSequenceId,
|
|
4316
|
+
targetCursor,
|
|
4317
|
+
targetActionId,
|
|
4318
|
+
note,
|
|
4319
|
+
options,
|
|
4320
|
+
command,
|
|
4321
|
+
commandName,
|
|
4322
|
+
});
|
|
4323
|
+
}
|
|
4268
4324
|
const idempotencyKey =
|
|
4269
4325
|
normalizeString(options.idempotencyKey) ||
|
|
4270
4326
|
defaultActionIdempotencyKey({
|
|
@@ -4347,6 +4403,89 @@ export function registerSessionCommand(program) {
|
|
|
4347
4403
|
return payload;
|
|
4348
4404
|
}
|
|
4349
4405
|
|
|
4406
|
+
// like/dislike/unlike/undislike: the shared reaction path (src/session/reactions.js)
|
|
4407
|
+
// decides admission, the operation key and the outcome; this only mirrors and prints.
|
|
4408
|
+
// `--idempotency-key` is a RETRY of the intent that minted that key.
|
|
4409
|
+
async function runReactionCommand({
|
|
4410
|
+
sessionId,
|
|
4411
|
+
reaction,
|
|
4412
|
+
agentId,
|
|
4413
|
+
targetPath,
|
|
4414
|
+
targetSequenceId,
|
|
4415
|
+
targetCursor,
|
|
4416
|
+
targetActionId,
|
|
4417
|
+
note,
|
|
4418
|
+
options,
|
|
4419
|
+
command,
|
|
4420
|
+
commandName,
|
|
4421
|
+
}) {
|
|
4422
|
+
const outcome = await submitSessionReaction({
|
|
4423
|
+
surface: "cli",
|
|
4424
|
+
sessionId,
|
|
4425
|
+
agentId,
|
|
4426
|
+
reaction,
|
|
4427
|
+
targetSequenceId,
|
|
4428
|
+
targetCursor,
|
|
4429
|
+
targetActionId,
|
|
4430
|
+
note,
|
|
4431
|
+
idempotencyKey: options.idempotencyKey,
|
|
4432
|
+
targetPath,
|
|
4433
|
+
});
|
|
4434
|
+
let event = null;
|
|
4435
|
+
let localAppend = { appended: false, reason: "not_recorded" };
|
|
4436
|
+
if (outcome.action) {
|
|
4437
|
+
const appended = await appendActionEventIfMissing(
|
|
4438
|
+
sessionId,
|
|
4439
|
+
buildSessionActionEvent(sessionId, outcome.action),
|
|
4440
|
+
{ targetPath },
|
|
4441
|
+
);
|
|
4442
|
+
event = appended.event;
|
|
4443
|
+
localAppend = { appended: Boolean(appended.appended), reason: appended.reason || "" };
|
|
4444
|
+
}
|
|
4445
|
+
let evidenceEvent = null;
|
|
4446
|
+
if (outcome.collapsedActionId && outcome.action) {
|
|
4447
|
+
// Mirror the server's retained no-op row: this request's key, collapsed into
|
|
4448
|
+
// the active reaction it repeated.
|
|
4449
|
+
const appended = await appendActionEventIfMissing(
|
|
4450
|
+
sessionId,
|
|
4451
|
+
buildSessionActionEvent(sessionId, {
|
|
4452
|
+
id: outcome.collapsedActionId,
|
|
4453
|
+
actionType: reaction,
|
|
4454
|
+
targetSequenceId: outcome.action.targetSequenceId,
|
|
4455
|
+
targetCursor: outcome.action.targetCursor,
|
|
4456
|
+
targetActionId: outcome.action.targetActionId,
|
|
4457
|
+
actorKind: outcome.action.actorKind,
|
|
4458
|
+
actorId: outcome.action.actorId,
|
|
4459
|
+
actorRole: outcome.action.actorRole,
|
|
4460
|
+
idempotencyKey: outcome.operationKey,
|
|
4461
|
+
collapsedIntoActionId: outcome.action.id,
|
|
4462
|
+
}),
|
|
4463
|
+
{ targetPath },
|
|
4464
|
+
);
|
|
4465
|
+
evidenceEvent = appended.event;
|
|
4466
|
+
}
|
|
4467
|
+
const payload = {
|
|
4468
|
+
command: commandName,
|
|
4469
|
+
targetPath,
|
|
4470
|
+
...outcome,
|
|
4471
|
+
event,
|
|
4472
|
+
evidenceEvent,
|
|
4473
|
+
localAppend,
|
|
4474
|
+
};
|
|
4475
|
+
if (!outcome.ok) process.exitCode = 1;
|
|
4476
|
+
if (shouldEmitJson(options, command)) {
|
|
4477
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4478
|
+
return payload;
|
|
4479
|
+
}
|
|
4480
|
+
if (!outcome.ok) {
|
|
4481
|
+
console.error(pc.yellow(outcome.message));
|
|
4482
|
+
return payload;
|
|
4483
|
+
}
|
|
4484
|
+
if (event) console.log(formatEventLine(event));
|
|
4485
|
+
if (outcome.outcome !== "applied" || !event) console.log(pc.gray(outcome.message));
|
|
4486
|
+
return payload;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4350
4489
|
session
|
|
4351
4490
|
.command("actions")
|
|
4352
4491
|
.description("List supported low-noise message actions with examples")
|
|
@@ -4373,7 +4512,7 @@ export function registerSessionCommand(program) {
|
|
|
4373
4512
|
session
|
|
4374
4513
|
.command("action <sessionId> <actionType>")
|
|
4375
4514
|
.description(
|
|
4376
|
-
"Create a message action for a target session event (ack, working_on, reply/comment, like, dislike, disregard, view)",
|
|
4515
|
+
"Create a message action for a target session event (ack, working_on, reply/comment, like, dislike, unlike, undislike, disregard, view)",
|
|
4377
4516
|
)
|
|
4378
4517
|
.option("--target-sequence <n>", "Target event sequence id")
|
|
4379
4518
|
.option("--target-cursor <cursor>", "Target event cursor")
|
|
@@ -4389,18 +4528,23 @@ export function registerSessionCommand(program) {
|
|
|
4389
4528
|
|
|
4390
4529
|
session
|
|
4391
4530
|
.command("react <sessionId> <reaction>")
|
|
4392
|
-
.description(
|
|
4531
|
+
.description(
|
|
4532
|
+
"React to or acknowledge a target session event with ack, like, or dislike; retract your own like/dislike with unlike/undislike",
|
|
4533
|
+
)
|
|
4393
4534
|
.option("--target-sequence <n>", "Target event sequence id")
|
|
4394
4535
|
.option("--target-cursor <cursor>", "Target event cursor")
|
|
4395
4536
|
.option("--target-action-id <uuid>", "Target a threaded reply/action by action UUID")
|
|
4396
4537
|
.option("--agent <id>", "Agent id authoring the action (defaults to the joined session agent)")
|
|
4397
|
-
.option(
|
|
4538
|
+
.option(
|
|
4539
|
+
"--idempotency-key <key>",
|
|
4540
|
+
"Retry an earlier reaction as the SAME intent by resending its operationKey (without it, each call is a new intent)",
|
|
4541
|
+
)
|
|
4398
4542
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4399
4543
|
.option("--json", "Emit machine-readable output")
|
|
4400
4544
|
.action(async (sessionId, reaction, options, command) => {
|
|
4401
4545
|
const normalizedReaction = normalizeSessionMessageActionType(reaction);
|
|
4402
|
-
if (!
|
|
4403
|
-
throw new Error(
|
|
4546
|
+
if (!SESSION_REACT_COMMAND_TYPES.includes(normalizedReaction)) {
|
|
4547
|
+
throw new Error(`reaction must be one of: ${SESSION_REACT_COMMAND_TYPES.join(", ")}.`);
|
|
4404
4548
|
}
|
|
4405
4549
|
await runMessageActionCommand({
|
|
4406
4550
|
sessionId,
|
|
@@ -6787,6 +6931,285 @@ export function registerSessionCommand(program) {
|
|
|
6787
6931
|
}
|
|
6788
6932
|
});
|
|
6789
6933
|
|
|
6934
|
+
// ------------------------------------------------------- agent access
|
|
6935
|
+
// Human control plane for the Agent Admission Contract. The request command
|
|
6936
|
+
// delegates to the established agent-side flow; list/decision/revoke use the
|
|
6937
|
+
// matching owner-only REST routes. Goal text is an agent claim and is always
|
|
6938
|
+
// rendered as bounded, untrusted data.
|
|
6939
|
+
const access = session
|
|
6940
|
+
.command("access")
|
|
6941
|
+
.description("Request and govern AIdenID-backed, scoped agent access to a session");
|
|
6942
|
+
|
|
6943
|
+
function accessOutput(options, command, payload, render) {
|
|
6944
|
+
if (shouldEmitJson(options, command)) {
|
|
6945
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
6946
|
+
return;
|
|
6947
|
+
}
|
|
6948
|
+
render();
|
|
6949
|
+
}
|
|
6950
|
+
|
|
6951
|
+
function terminalText(value, maxLength = 240) {
|
|
6952
|
+
let result = "";
|
|
6953
|
+
for (const ch of normalizeString(value).normalize("NFC")) {
|
|
6954
|
+
// Refuse terminal control/invisible formatting from remote values,
|
|
6955
|
+
// including ANSI/OSC initiators and bidi overrides/isolates.
|
|
6956
|
+
result += /[\p{Cc}\p{Cf}\p{Cs}\p{Co}\p{Cn}\p{Zl}\p{Zp}]/u.test(ch) ? " " : ch;
|
|
6957
|
+
if (result.length >= maxLength) break;
|
|
6958
|
+
}
|
|
6959
|
+
return result.replace(/\s+/gu, " ").trim();
|
|
6960
|
+
}
|
|
6961
|
+
|
|
6962
|
+
function renderAdmissionLine(admission) {
|
|
6963
|
+
const requested = Array.isArray(admission?.requestedScope?.actions)
|
|
6964
|
+
? terminalText(admission.requestedScope.actions.join(","), 180)
|
|
6965
|
+
: "scope unavailable";
|
|
6966
|
+
const ttl = Number(admission?.requestedScope?.ttlSeconds);
|
|
6967
|
+
const duration = Number.isFinite(ttl) ? ` for ${ttl}s` : "";
|
|
6968
|
+
const identity = terminalText(admission?.displayName, 120) || terminalText(admission?.agentId, 64) || "unknown-agent";
|
|
6969
|
+
console.log(`${terminalText(admission?.admissionId, 64) || "unknown"} ${terminalText(admission?.status, 16) || "unknown"} ${identity}`);
|
|
6970
|
+
console.log(pc.gray(` requested: ${requested}${duration}`));
|
|
6971
|
+
const summary = terminalText(admission?.goal?.summary);
|
|
6972
|
+
if (summary) console.log(pc.gray(` claimed goal (untrusted): ${JSON.stringify(summary)}`));
|
|
6973
|
+
if (admission?.email) {
|
|
6974
|
+
// Human terminal output is routinely copied into logs and support
|
|
6975
|
+
// transcripts. Keep the address in the explicit --json response only.
|
|
6976
|
+
const inboxStatus = terminalText(admission.email.status, 32) || "unknown";
|
|
6977
|
+
console.log(pc.gray(` AIdenID inbox: ${inboxStatus}`));
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
|
|
6981
|
+
access
|
|
6982
|
+
.command("mode <sessionId> <mode>")
|
|
6983
|
+
.description("Set whether this room requires admission for agent-attributed access (required or legacy)")
|
|
6984
|
+
.option("--idempotency-key <key>", "Retry the same logical mode change safely")
|
|
6985
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
6986
|
+
.option("--json", "Emit machine-readable output")
|
|
6987
|
+
.action(async (sessionId, mode, options, command) => {
|
|
6988
|
+
const sid = normalizeString(sessionId);
|
|
6989
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6990
|
+
const changed = await setSessionAdmissionMode(sid, mode, {
|
|
6991
|
+
idempotencyKey: options.idempotencyKey,
|
|
6992
|
+
targetPath,
|
|
6993
|
+
});
|
|
6994
|
+
accessOutput(
|
|
6995
|
+
options,
|
|
6996
|
+
command,
|
|
6997
|
+
{
|
|
6998
|
+
command: "session access mode",
|
|
6999
|
+
idempotencyKey: changed.idempotencyKey,
|
|
7000
|
+
...changed.result,
|
|
7001
|
+
},
|
|
7002
|
+
() => console.log(
|
|
7003
|
+
`Agent admission mode for ${terminalText(sid, 64)}: ${terminalText(changed.result.agentAdmissionMode, 16) || "unknown"}.`,
|
|
7004
|
+
),
|
|
7005
|
+
);
|
|
7006
|
+
});
|
|
7007
|
+
|
|
7008
|
+
access
|
|
7009
|
+
.command("request <sessionId>")
|
|
7010
|
+
.description("Request agent access; a human must approve before the agent can claim or join")
|
|
7011
|
+
.requiredOption("--agent <id>", "Canonical agent id requesting access")
|
|
7012
|
+
.requiredOption("--goal <text>", "What the agent is coming to do")
|
|
7013
|
+
.option("--display-name <name>", "Human-readable agent name")
|
|
7014
|
+
.option("--deliverable <text>", "An output this agent will produce (repeatable)", collectOption, [])
|
|
7015
|
+
.option("--stop-when <text>", "A condition that ends this agent's work (repeatable)", collectOption, [])
|
|
7016
|
+
.option(
|
|
7017
|
+
"--scope <actions>",
|
|
7018
|
+
"Comma-separated actions: session.read, session.post, session.react, tickets.read, tickets.work",
|
|
7019
|
+
)
|
|
7020
|
+
.option("--ttl <duration>", "Requested grant lifetime, e.g. 90m or 24h (5m..24h)", "2h")
|
|
7021
|
+
.option("--model <model>", "Agent model hint", "cli")
|
|
7022
|
+
.option("--provider <name>", "Model provider hint")
|
|
7023
|
+
.option("--wait", "Wait for approval and claim the grant on this machine")
|
|
7024
|
+
.option("--wait-seconds <n>", "Maximum approval wait when --wait is set", "900")
|
|
7025
|
+
.option("--path <path>", "Workspace path for local admission state", ".")
|
|
7026
|
+
.option("--json", "Emit machine-readable output")
|
|
7027
|
+
.action(async (sessionId, options, command) => {
|
|
7028
|
+
const sid = normalizeString(sessionId);
|
|
7029
|
+
const agentId = canonicalAgentId(options.agent);
|
|
7030
|
+
assertCanonicalAgentId(options.agent, "--agent");
|
|
7031
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7032
|
+
const waitSeconds = Number.parseInt(String(options.waitSeconds ?? "900"), 10);
|
|
7033
|
+
if (!Number.isFinite(waitSeconds) || waitSeconds < 0) {
|
|
7034
|
+
throw new Error("--wait-seconds must be a non-negative integer.");
|
|
7035
|
+
}
|
|
7036
|
+
const admission = await runAdmissionJoin(sid, {
|
|
7037
|
+
agentId,
|
|
7038
|
+
displayName: options.displayName,
|
|
7039
|
+
model: options.model,
|
|
7040
|
+
provider: options.provider,
|
|
7041
|
+
goal: options.goal,
|
|
7042
|
+
deliverables: options.deliverable || [],
|
|
7043
|
+
stopConditions: options.stopWhen || [],
|
|
7044
|
+
actions: parseAdmissionScope(options.scope),
|
|
7045
|
+
ttlSeconds: parseAdmissionTtlSeconds(options.ttl),
|
|
7046
|
+
wait: Boolean(options.wait),
|
|
7047
|
+
waitTimeoutMs: waitSeconds * 1000,
|
|
7048
|
+
targetPath,
|
|
7049
|
+
onPending: ({ approveUrl, phase }) => {
|
|
7050
|
+
const line = phase === "identity"
|
|
7051
|
+
? "Approval received; waiting for the AIdenID email and signed purpose receipt."
|
|
7052
|
+
: `Waiting for a room owner to approve this agent: ${terminalText(approveUrl, 1024)}`;
|
|
7053
|
+
if (shouldEmitJson(options, command)) process.stderr.write(`${line}\n`);
|
|
7054
|
+
// A line carrying a remote URL must remain free of every terminal
|
|
7055
|
+
// control byte, including trusted colour wrappers. This mirrors the
|
|
7056
|
+
// guarded `session join` path and keeps raw support logs auditable.
|
|
7057
|
+
else if (phase === "identity") console.log(pc.yellow(line));
|
|
7058
|
+
else console.log(line);
|
|
7059
|
+
},
|
|
7060
|
+
});
|
|
7061
|
+
const payload = {
|
|
7062
|
+
command: "session access request",
|
|
7063
|
+
sessionId: sid,
|
|
7064
|
+
agentId,
|
|
7065
|
+
admission,
|
|
7066
|
+
readyToJoin: admission.status === "active",
|
|
7067
|
+
};
|
|
7068
|
+
accessOutput(options, command, payload, () => {
|
|
7069
|
+
if (admission.status === "pending") {
|
|
7070
|
+
console.log(`Access requested for ${agentId}; waiting for a room owner.`);
|
|
7071
|
+
if (admission.approveUrl) console.log(`Approval: ${terminalText(admission.approveUrl, 1024)}`);
|
|
7072
|
+
} else if (admission.status === "active") {
|
|
7073
|
+
console.log(`Access active for ${agentId}. The scoped credential is stored locally and was not printed.`);
|
|
7074
|
+
console.log(pc.gray("Run `sl session join` again with the same goal, scope, and TTL to attach this agent."));
|
|
7075
|
+
} else if (admission.status === "approved") {
|
|
7076
|
+
console.log("Access approved; waiting for the AIdenID inbox and signed purpose receipt before claim.");
|
|
7077
|
+
} else {
|
|
7078
|
+
console.log(`Access ${terminalText(admission.status, 16) || "unknown"} for ${agentId}.`);
|
|
7079
|
+
}
|
|
7080
|
+
});
|
|
7081
|
+
});
|
|
7082
|
+
|
|
7083
|
+
access
|
|
7084
|
+
.command("list <sessionId>")
|
|
7085
|
+
.description("List admission requests visible to a room owner or admin")
|
|
7086
|
+
.option("--status <status>", "Filter by admission status")
|
|
7087
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
7088
|
+
.option("--json", "Emit machine-readable output")
|
|
7089
|
+
.action(async (sessionId, options, command) => {
|
|
7090
|
+
const sid = normalizeString(sessionId);
|
|
7091
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7092
|
+
const result = await listSessionAdmissions(sid, {
|
|
7093
|
+
status: options.status,
|
|
7094
|
+
targetPath,
|
|
7095
|
+
});
|
|
7096
|
+
const payload = { command: "session access list", ...result };
|
|
7097
|
+
accessOutput(options, command, payload, () => {
|
|
7098
|
+
console.log(pc.gray(`Admission mode: ${terminalText(result.agentAdmissionMode, 16) || "unknown"}`));
|
|
7099
|
+
if (!Array.isArray(result.admissions) || result.admissions.length === 0) {
|
|
7100
|
+
console.log("No admission requests.");
|
|
7101
|
+
return;
|
|
7102
|
+
}
|
|
7103
|
+
for (const admission of result.admissions) renderAdmissionLine(admission);
|
|
7104
|
+
});
|
|
7105
|
+
});
|
|
7106
|
+
|
|
7107
|
+
access
|
|
7108
|
+
.command("status <sessionId> <admissionId>")
|
|
7109
|
+
.description("Show the requesting delegator's current admission status")
|
|
7110
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
7111
|
+
.option("--json", "Emit machine-readable output")
|
|
7112
|
+
.action(async (sessionId, admissionId, options, command) => {
|
|
7113
|
+
const sid = normalizeString(sessionId);
|
|
7114
|
+
const aid = normalizeString(admissionId);
|
|
7115
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7116
|
+
const result = await getSessionAdmission(sid, aid, { targetPath });
|
|
7117
|
+
accessOutput(
|
|
7118
|
+
options,
|
|
7119
|
+
command,
|
|
7120
|
+
{ command: "session access status", sessionId: sid, ...result },
|
|
7121
|
+
() => {
|
|
7122
|
+
console.log(`${terminalText(result.admissionId, 64) || aid} ${terminalText(result.status, 16) || "unknown"} ${terminalText(result.agentId, 64) || "unknown-agent"}`);
|
|
7123
|
+
if (result.approveUrl) console.log(`Approval: ${terminalText(result.approveUrl, 1024)}`);
|
|
7124
|
+
},
|
|
7125
|
+
);
|
|
7126
|
+
});
|
|
7127
|
+
|
|
7128
|
+
access
|
|
7129
|
+
.command("approve <sessionId> <admissionId>")
|
|
7130
|
+
.description("Approve a pending request, optionally narrowing its scope or TTL")
|
|
7131
|
+
.option("--scope <actions>", "Comma-separated granted actions; must be a subset of the request")
|
|
7132
|
+
.option("--ttl <duration>", "Granted lifetime; may only shorten the request (5m..24h)")
|
|
7133
|
+
.option("--note <text>", "Approval note (max 500 characters)")
|
|
7134
|
+
.option("--idempotency-key <key>", "Retry the same logical approval safely")
|
|
7135
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
7136
|
+
.option("--json", "Emit machine-readable output")
|
|
7137
|
+
.action(async (sessionId, admissionId, options, command) => {
|
|
7138
|
+
const sid = normalizeString(sessionId);
|
|
7139
|
+
const aid = normalizeString(admissionId);
|
|
7140
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7141
|
+
const decision = await decideSessionAdmission(sid, aid, {
|
|
7142
|
+
decision: "approve",
|
|
7143
|
+
grantedActions: options.scope === undefined ? undefined : parseAdmissionScope(options.scope),
|
|
7144
|
+
ttlSeconds: options.ttl === undefined ? undefined : parseAdmissionTtlSeconds(options.ttl),
|
|
7145
|
+
note: options.note,
|
|
7146
|
+
idempotencyKey: options.idempotencyKey,
|
|
7147
|
+
targetPath,
|
|
7148
|
+
});
|
|
7149
|
+
const result = decision.result;
|
|
7150
|
+
accessOutput(
|
|
7151
|
+
options,
|
|
7152
|
+
command,
|
|
7153
|
+
{ command: "session access approve", sessionId: sid, idempotencyKey: decision.idempotencyKey, admission: result },
|
|
7154
|
+
() => {
|
|
7155
|
+
console.log(`Approved ${terminalText(result.admissionId, 64) || aid} for ${terminalText(result.agentId, 64) || "the agent"}.`);
|
|
7156
|
+
if (result.email) {
|
|
7157
|
+
const inboxStatus = terminalText(result.email.status, 32) || "queued";
|
|
7158
|
+
console.log(pc.gray(`AIdenID inbox: ${inboxStatus}. Use --json to inspect its address.`));
|
|
7159
|
+
}
|
|
7160
|
+
},
|
|
7161
|
+
);
|
|
7162
|
+
});
|
|
7163
|
+
|
|
7164
|
+
access
|
|
7165
|
+
.command("deny <sessionId> <admissionId>")
|
|
7166
|
+
.description("Deny a pending agent admission request")
|
|
7167
|
+
.option("--note <text>", "Denial note (max 500 characters)")
|
|
7168
|
+
.option("--idempotency-key <key>", "Retry the same logical denial safely")
|
|
7169
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
7170
|
+
.option("--json", "Emit machine-readable output")
|
|
7171
|
+
.action(async (sessionId, admissionId, options, command) => {
|
|
7172
|
+
const sid = normalizeString(sessionId);
|
|
7173
|
+
const aid = normalizeString(admissionId);
|
|
7174
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7175
|
+
const decision = await decideSessionAdmission(sid, aid, {
|
|
7176
|
+
decision: "deny",
|
|
7177
|
+
note: options.note,
|
|
7178
|
+
idempotencyKey: options.idempotencyKey,
|
|
7179
|
+
targetPath,
|
|
7180
|
+
});
|
|
7181
|
+
accessOutput(
|
|
7182
|
+
options,
|
|
7183
|
+
command,
|
|
7184
|
+
{ command: "session access deny", sessionId: sid, idempotencyKey: decision.idempotencyKey, admission: decision.result },
|
|
7185
|
+
() => console.log(`Denied ${terminalText(decision.result.admissionId, 64) || aid}.`),
|
|
7186
|
+
);
|
|
7187
|
+
});
|
|
7188
|
+
|
|
7189
|
+
access
|
|
7190
|
+
.command("revoke <sessionId> <admissionId>")
|
|
7191
|
+
.description("Revoke an approved or active agent admission")
|
|
7192
|
+
.option("--reason <text>", "Revocation reason (max 200 characters)")
|
|
7193
|
+
.option("--idempotency-key <key>", "Retry the same logical revocation safely")
|
|
7194
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
7195
|
+
.option("--json", "Emit machine-readable output")
|
|
7196
|
+
.action(async (sessionId, admissionId, options, command) => {
|
|
7197
|
+
const sid = normalizeString(sessionId);
|
|
7198
|
+
const aid = normalizeString(admissionId);
|
|
7199
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7200
|
+
const revoked = await revokeSessionAdmission(sid, aid, {
|
|
7201
|
+
reason: options.reason,
|
|
7202
|
+
idempotencyKey: options.idempotencyKey,
|
|
7203
|
+
targetPath,
|
|
7204
|
+
});
|
|
7205
|
+
accessOutput(
|
|
7206
|
+
options,
|
|
7207
|
+
command,
|
|
7208
|
+
{ command: "session access revoke", sessionId: sid, idempotencyKey: revoked.idempotencyKey, admission: revoked.result },
|
|
7209
|
+
() => console.log(`Revoked ${terminalText(revoked.result.admissionId, 64) || aid}.`),
|
|
7210
|
+
);
|
|
7211
|
+
});
|
|
7212
|
+
|
|
6790
7213
|
// ---------------------------------------------------------------- tickets
|
|
6791
7214
|
// The room's shared backlog (API "Ticket Lease Contract (T2)"). Every subcommand is
|
|
6792
7215
|
// an ACTOR command: the runCli choke point runs it on the agent's admission.
|
|
@@ -64,6 +64,9 @@ const SENSITIVE_COMMAND_PREFIXES = [
|
|
|
64
64
|
"ai.identity.target.",
|
|
65
65
|
"ai.identity.site.",
|
|
66
66
|
"ai.identity.legal-hold.",
|
|
67
|
+
// Room access administration (request/list/status/approve/deny/revoke/mode) belongs to
|
|
68
|
+
// the room owner's own session; operators use the direct CLI or the web dashboard.
|
|
69
|
+
"session.access.",
|
|
67
70
|
];
|
|
68
71
|
const REDACTION_MARKER = "[REDACTED]";
|
|
69
72
|
|
package/src/mcp/registry.js
CHANGED
|
@@ -655,7 +655,7 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
655
655
|
name: "session_action",
|
|
656
656
|
title: "Record Senti Session Action",
|
|
657
657
|
description:
|
|
658
|
-
"Record a low-noise message action such as ack, working_on, disregard, like, dislike, or reply; view advances the monotonic read cursor instead of appending an action.",
|
|
658
|
+
"Record a low-noise message action such as ack, working_on, disregard, like, dislike, unlike, undislike, or reply; view advances the monotonic read cursor instead of appending an action.",
|
|
659
659
|
input_schema: {
|
|
660
660
|
type: "object",
|
|
661
661
|
additionalProperties: false,
|
|
@@ -665,7 +665,7 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
665
665
|
agentId: { type: "string" },
|
|
666
666
|
actionType: {
|
|
667
667
|
type: "string",
|
|
668
|
-
enum: ["ack", "working_on", "reply", "like", "dislike", "disregard", "view"],
|
|
668
|
+
enum: ["ack", "working_on", "reply", "like", "dislike", "unlike", "undislike", "disregard", "view"],
|
|
669
669
|
},
|
|
670
670
|
targetSequenceId: { type: "integer", minimum: 1 },
|
|
671
671
|
targetCursor: { type: "string" },
|
|
@@ -702,7 +702,7 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
702
702
|
name: "session_react",
|
|
703
703
|
title: "React To Senti Message",
|
|
704
704
|
description:
|
|
705
|
-
"Acknowledge or react to a target session event with ack, like, or dislike.",
|
|
705
|
+
"Acknowledge or react to a target session event with ack, like, or dislike, or retract your own like/dislike with unlike/undislike. Reactions report an outcome and an operation key; resend that key as idempotencyKey only to retry the same intent.",
|
|
706
706
|
input_schema: {
|
|
707
707
|
type: "object",
|
|
708
708
|
additionalProperties: false,
|
|
@@ -710,7 +710,7 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
710
710
|
properties: {
|
|
711
711
|
sessionId: { type: "string" },
|
|
712
712
|
agentId: { type: "string" },
|
|
713
|
-
reaction: { type: "string", enum: ["ack", "like", "dislike"] },
|
|
713
|
+
reaction: { type: "string", enum: ["ack", "like", "dislike", "unlike", "undislike"] },
|
|
714
714
|
targetSequenceId: { type: "integer", minimum: 1 },
|
|
715
715
|
targetCursor: { type: "string" },
|
|
716
716
|
targetActionId: { type: "string" },
|