sentinelayer-cli 0.41.0 → 0.43.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 +55 -0
- package/package.json +3 -2
- package/src/auth/admission-scope.js +34 -0
- package/src/auth/gate.js +6 -0
- package/src/auth/http.js +4 -0
- package/src/auth/service.js +6 -0
- package/src/board/done-gate.js +155 -0
- package/src/board/evidence-resolvers.js +138 -0
- package/src/cli.js +24 -1
- package/src/commands/board.js +116 -0
- package/src/commands/session.js +779 -47
- package/src/engram/build-state.js +183 -0
- package/src/engram/canonical.js +75 -0
- package/src/engram/document.js +190 -0
- package/src/engram/index.js +11 -2
- package/src/engram/ingest-document.js +107 -0
- package/src/engram/store.js +6 -1
- package/src/engram/tools.js +6 -0
- package/src/events/schema.js +18 -0
- package/src/legacy-cli.js +2 -2
- package/src/net/test-egress-guard.js +158 -0
- package/src/review/investor-dd-orchestrator.js +14 -2
- package/src/session/admission-access.js +256 -0
- package/src/session/admission-auth.js +319 -0
- package/src/session/admission-preimage.js +90 -0
- package/src/session/admission.js +641 -0
- package/src/session/checkpoints.js +25 -1
- package/src/session/codebase-context.js +54 -0
- package/src/session/coordination-guidance.js +1 -1
- package/src/session/daemon-spawn.js +20 -2
- package/src/session/daemon.js +7 -1
- package/src/session/event-identity.js +9 -2
- package/src/session/first-message.js +11 -3
- package/src/session/listener.js +3 -0
- package/src/session/live-source.js +47 -16
- package/src/session/message-edits.js +89 -0
- package/src/session/recap.js +26 -4
- package/src/session/remote-hydrate.js +7 -19
- package/src/session/store.js +13 -14
- package/src/session/stream.js +23 -3
- package/src/session/sync.js +128 -11
- package/src/session/tickets.js +316 -0
- package/src/session/title-sync.js +2 -1
- package/src/session/usage-report.js +41 -2
- package/src/session/wake/resolve-target.js +10 -5
package/src/commands/session.js
CHANGED
|
@@ -91,6 +91,34 @@ import {
|
|
|
91
91
|
normalizeSessionOnboarding,
|
|
92
92
|
writeSessionOnboardingBrief,
|
|
93
93
|
} from "../session/invitations.js";
|
|
94
|
+
import {
|
|
95
|
+
cancelAdmission,
|
|
96
|
+
fetchOwnAdmissionReceipt,
|
|
97
|
+
parseScope as parseAdmissionScope,
|
|
98
|
+
parseTtlSeconds as parseAdmissionTtlSeconds,
|
|
99
|
+
runAdmissionJoin,
|
|
100
|
+
} from "../session/admission.js";
|
|
101
|
+
import {
|
|
102
|
+
decideSessionAdmission,
|
|
103
|
+
getSessionAdmission,
|
|
104
|
+
listSessionAdmissions,
|
|
105
|
+
revokeSessionAdmission,
|
|
106
|
+
setSessionAdmissionMode,
|
|
107
|
+
} from "../session/admission-access.js";
|
|
108
|
+
import {
|
|
109
|
+
assertCanonicalAgentId,
|
|
110
|
+
canonicalAgentId,
|
|
111
|
+
currentAdmittedAgent,
|
|
112
|
+
withAgentAdmission,
|
|
113
|
+
} from "../session/admission-auth.js";
|
|
114
|
+
import {
|
|
115
|
+
claimTicket,
|
|
116
|
+
listTickets,
|
|
117
|
+
releaseTicketLease,
|
|
118
|
+
renewTicketLease,
|
|
119
|
+
submitTicket,
|
|
120
|
+
ticketEvents,
|
|
121
|
+
} from "../session/tickets.js";
|
|
94
122
|
import { postFirstSentiMessage } from "../session/first-message.js";
|
|
95
123
|
import { createListenerHostWake } from "../session/wake/listen-wake.js";
|
|
96
124
|
import { appendToStream, readStream, tailStream } from "../session/stream.js";
|
|
@@ -104,6 +132,8 @@ import {
|
|
|
104
132
|
import { readSessionPreview } from "../session/preview.js";
|
|
105
133
|
import {
|
|
106
134
|
createSessionMessageAction,
|
|
135
|
+
editSessionMessage,
|
|
136
|
+
fetchJsonWithFullTimeout,
|
|
107
137
|
fetchSessionUsageLedger,
|
|
108
138
|
fetchSessionPinnedMessages,
|
|
109
139
|
listSessionMessageActions,
|
|
@@ -120,6 +150,7 @@ import {
|
|
|
120
150
|
updateSessionReadCursor,
|
|
121
151
|
} from "../session/sync.js";
|
|
122
152
|
import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
|
|
153
|
+
import { messageRevision, replyRevisionEvent, sessionMessageRoutingEvent } from "../session/message-edits.js";
|
|
123
154
|
import { runSessionRecall } from "../session/recall/index.js";
|
|
124
155
|
import { mergeLiveSources } from "../session/live-source.js";
|
|
125
156
|
import { listenSessionEvents } from "../session/listener.js";
|
|
@@ -573,11 +604,6 @@ const SESSION_MESSAGE_ACTION_DESCRIPTIONS = Object.freeze([
|
|
|
573
604
|
command: "sl session action <id> disregard --target-sequence <n>",
|
|
574
605
|
description: "Mark a message as intentionally ignored or superseded.",
|
|
575
606
|
},
|
|
576
|
-
{
|
|
577
|
-
type: "view",
|
|
578
|
-
command: "sl session view <id> <sequence>",
|
|
579
|
-
description: "Manually backfill a read receipt for a target message; remote reads record views automatically.",
|
|
580
|
-
},
|
|
581
607
|
]);
|
|
582
608
|
|
|
583
609
|
function normalizeSessionMessageActionType(value) {
|
|
@@ -669,6 +695,11 @@ function buildSessionActionEvent(sessionId, action = {}) {
|
|
|
669
695
|
},
|
|
670
696
|
sessionId,
|
|
671
697
|
ts: actionCreatedAt(action),
|
|
698
|
+
messageRevision: messageRevision(action),
|
|
699
|
+
editedAt: action.editedAt,
|
|
700
|
+
editedBy: action.editedBy,
|
|
701
|
+
canEdit: action.canEdit,
|
|
702
|
+
revisionEvidence: action.revisionEvidence,
|
|
672
703
|
payload: {
|
|
673
704
|
actionId: id,
|
|
674
705
|
actionType,
|
|
@@ -676,6 +707,7 @@ function buildSessionActionEvent(sessionId, action = {}) {
|
|
|
676
707
|
targetCursor: actionTargetCursor(action) || null,
|
|
677
708
|
targetActionId: actionTargetActionId(action) || null,
|
|
678
709
|
note: normalizeString(action.note) || null,
|
|
710
|
+
metadata: action.metadata && typeof action.metadata === "object" ? action.metadata : undefined,
|
|
679
711
|
message: actionDisplayMessage(action),
|
|
680
712
|
source: "session_action",
|
|
681
713
|
},
|
|
@@ -1404,19 +1436,22 @@ async function findReusableSessionCandidate({
|
|
|
1404
1436
|
// can exercise the local materialization path without a real API.
|
|
1405
1437
|
async function fetchRemoteSessionDetail(endpoint, headers) {
|
|
1406
1438
|
let response;
|
|
1439
|
+
let payload;
|
|
1407
1440
|
try {
|
|
1408
|
-
response = await
|
|
1441
|
+
({ response, payload } = await fetchJsonWithFullTimeout(
|
|
1442
|
+
endpoint, { method: "GET", headers, redirect: "error" }, 2_000,
|
|
1443
|
+
));
|
|
1409
1444
|
} catch (err) {
|
|
1410
1445
|
return {
|
|
1411
1446
|
ok: false,
|
|
1412
1447
|
response: null,
|
|
1413
1448
|
status: 0,
|
|
1414
|
-
reason: normalizeString(err?.message) || "fetch_failed",
|
|
1449
|
+
reason: err?.name === "AbortError" ? "request_timeout" : (normalizeString(err?.message) || "fetch_failed"),
|
|
1415
1450
|
retryable: true,
|
|
1416
1451
|
};
|
|
1417
1452
|
}
|
|
1418
1453
|
if (response && response.ok) {
|
|
1419
|
-
return { ok: true,
|
|
1454
|
+
return { ok: true, payload, status: response.status, reason: "", retryable: false };
|
|
1420
1455
|
}
|
|
1421
1456
|
if (!response) {
|
|
1422
1457
|
return { ok: false, response: null, status: 0, reason: "no_response", retryable: true };
|
|
@@ -1431,8 +1466,7 @@ async function fetchRemoteSessionDetail(endpoint, headers) {
|
|
|
1431
1466
|
};
|
|
1432
1467
|
}
|
|
1433
1468
|
|
|
1434
|
-
|
|
1435
|
-
const body = await response.json().catch(() => ({}));
|
|
1469
|
+
function parseRemoteSessionDetailResponse(body) {
|
|
1436
1470
|
return body && body.session && typeof body.session === "object"
|
|
1437
1471
|
? body.session
|
|
1438
1472
|
: body && typeof body === "object"
|
|
@@ -1475,7 +1509,7 @@ async function verifyRemoteSession(sessionId, { targetPath } = {}) {
|
|
|
1475
1509
|
return {
|
|
1476
1510
|
ok: true,
|
|
1477
1511
|
source: "singleton",
|
|
1478
|
-
session:
|
|
1512
|
+
session: parseRemoteSessionDetailResponse(detail.payload),
|
|
1479
1513
|
status: detail.status,
|
|
1480
1514
|
};
|
|
1481
1515
|
}
|
|
@@ -1769,7 +1803,13 @@ export async function ensureWorkspaceSession({
|
|
|
1769
1803
|
);
|
|
1770
1804
|
let titleSync = null;
|
|
1771
1805
|
if (shouldPushTitle) {
|
|
1772
|
-
titleSync = await pushSessionTitleToApi(created.sessionId, effectiveTitle, {
|
|
1806
|
+
titleSync = await pushSessionTitleToApi(created.sessionId, effectiveTitle, {
|
|
1807
|
+
targetPath,
|
|
1808
|
+
// Startup uses the already validated credential; rotation and Retry-After
|
|
1809
|
+
// sleeps belong to explicit auth/title operations, not optional startup.
|
|
1810
|
+
autoRotate: false,
|
|
1811
|
+
maxRetries: 0,
|
|
1812
|
+
});
|
|
1773
1813
|
}
|
|
1774
1814
|
|
|
1775
1815
|
return {
|
|
@@ -1788,11 +1828,8 @@ export async function ensureWorkspaceSession({
|
|
|
1788
1828
|
}
|
|
1789
1829
|
|
|
1790
1830
|
function normalizeAgentId(value, fallbackValue = "cli-user") {
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1794
|
-
.replace(/^-+|-+$/g, "");
|
|
1795
|
-
return normalized || fallbackValue;
|
|
1831
|
+
// One canonicalisation for authorisation and execution (see admission-auth.js).
|
|
1832
|
+
return canonicalAgentId(value) || fallbackValue;
|
|
1796
1833
|
}
|
|
1797
1834
|
|
|
1798
1835
|
function canPublishListenerPresence(agentId) {
|
|
@@ -2389,6 +2426,11 @@ function formatEventLine(event = {}) {
|
|
|
2389
2426
|
const ts = normalizeString(event.ts || event.timestamp);
|
|
2390
2427
|
const type = normalizeString(event.event || event.type) || "event";
|
|
2391
2428
|
const agentId = normalizeString(event.agent?.id || event.agentId || "unknown");
|
|
2429
|
+
if (type === "session_message_edited") {
|
|
2430
|
+
const routed = sessionMessageRoutingEvent(event);
|
|
2431
|
+
const target = event.payload?.targetActionId ? `reply ${event.payload.targetActionId}` : `#${event.payload?.targetSequenceId || "?"}`;
|
|
2432
|
+
return `${ts} ${agentId} ${type} ${target} revision ${event.payload?.messageRevision || "?"}: ${normalizeString(routed.payload?.message || routed.payload?.text || "")}`;
|
|
2433
|
+
}
|
|
2392
2434
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
2393
2435
|
if (type === "session_action" || type === "session_reply" || type === "session_reaction") {
|
|
2394
2436
|
const actionType = normalizeString(payload.actionType) || type.replace(/^session_/, "");
|
|
@@ -2809,6 +2851,43 @@ async function emitAgentKilledEvent(sessionId, agentId, {
|
|
|
2809
2851
|
return event;
|
|
2810
2852
|
}
|
|
2811
2853
|
|
|
2854
|
+
function collectOption(value, previous) {
|
|
2855
|
+
return [...(previous || []), value];
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
/**
|
|
2859
|
+
* Wrap a command action so it runs as its agent: inside that agent's admission when
|
|
2860
|
+
* this machine holds one (every session API call then uses the admission credential
|
|
2861
|
+
* and never the human token), on the legacy path when it holds none, and refused when
|
|
2862
|
+
* what it holds is expired or unusable. `resolveIds` names the (session, agent) pair
|
|
2863
|
+
* exactly the way the action itself resolves it.
|
|
2864
|
+
*/
|
|
2865
|
+
function asAdmittedAgent(resolveIds, action) {
|
|
2866
|
+
return async (...args) => {
|
|
2867
|
+
const { sessionId, agentId } = await resolveIds(...args);
|
|
2868
|
+
return withAgentAdmission(sessionId, agentId, () => action(...args));
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
async function sessionSayAgent(sessionId, _messageParts, options) {
|
|
2873
|
+
const sid = normalizeString(sessionId);
|
|
2874
|
+
const identity = await resolveSessionSayIdentity({
|
|
2875
|
+
sessionId: sid,
|
|
2876
|
+
agentId: options.agent,
|
|
2877
|
+
targetPath: path.resolve(process.cwd(), String(options.path || ".")),
|
|
2878
|
+
});
|
|
2879
|
+
return { sessionId: sid, agentId: identity.agentId };
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
async function sessionReadAgent(sessionId, options) {
|
|
2883
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
2884
|
+
return { sessionId: normalizeString(sessionId), agentId: await defaultAgentId(options.agent, targetPath) };
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
async function sessionListenAgent(options) {
|
|
2888
|
+
return { sessionId: resolveSessionIdOption(options), agentId: normalizeAgentId(options.agent, "cli-user") };
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2812
2891
|
export function registerSessionCommand(program) {
|
|
2813
2892
|
const session = program
|
|
2814
2893
|
.command("session")
|
|
@@ -2923,8 +3002,9 @@ export function registerSessionCommand(program) {
|
|
|
2923
3002
|
};
|
|
2924
3003
|
|
|
2925
3004
|
// Best-effort admin visibility sync. Session creation remains local-first.
|
|
3005
|
+
let metadataSync = Promise.resolve(null);
|
|
2926
3006
|
if (remoteSync.attempted) {
|
|
2927
|
-
|
|
3007
|
+
metadataSync = syncSessionMetadataToApi(created.sessionId, {
|
|
2928
3008
|
targetPath,
|
|
2929
3009
|
sessionId: created.sessionId,
|
|
2930
3010
|
status: created.status,
|
|
@@ -2934,7 +3014,7 @@ export function registerSessionCommand(program) {
|
|
|
2934
3014
|
ttlSeconds,
|
|
2935
3015
|
template: created.template,
|
|
2936
3016
|
codebaseContext: created.codebaseContext,
|
|
2937
|
-
}).catch(() =>
|
|
3017
|
+
}, { targetPath, timeoutMs: 2_000 }).catch(() => null);
|
|
2938
3018
|
}
|
|
2939
3019
|
|
|
2940
3020
|
// Make the session managed by default: spawn the Senti daemon as a
|
|
@@ -2971,6 +3051,14 @@ export function registerSessionCommand(program) {
|
|
|
2971
3051
|
}).catch((error) => ({ posted: false, reason: normalizeString(error?.message) || "error" }));
|
|
2972
3052
|
}
|
|
2973
3053
|
payload.firstMessage = firstMessage;
|
|
3054
|
+
// Own the request lifecycle; do not leave a referenced fetch running
|
|
3055
|
+
// after emitting the single JSON document (even with --no-daemon).
|
|
3056
|
+
const metadataResult = await metadataSync;
|
|
3057
|
+
if (remoteSync.attempted) {
|
|
3058
|
+
remoteSync.status = metadataResult?.synced ? "synced" : "degraded";
|
|
3059
|
+
remoteSync.metadataSynced = Boolean(metadataResult?.synced);
|
|
3060
|
+
remoteSync.reason = metadataResult?.synced ? "metadata_acknowledged" : (metadataResult?.reason || "metadata_sync_failed");
|
|
3061
|
+
}
|
|
2974
3062
|
|
|
2975
3063
|
if (shouldEmitJson(options, command)) {
|
|
2976
3064
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -3319,6 +3407,21 @@ export function registerSessionCommand(program) {
|
|
|
3319
3407
|
.option("--role <role>", "Agent role: coder, reviewer, tester, observer", "coder")
|
|
3320
3408
|
.option("--model <model>", "Agent model hint", "cli")
|
|
3321
3409
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3410
|
+
.option(
|
|
3411
|
+
"--goal <text>",
|
|
3412
|
+
"Request goal-and-scope admission: say what this agent is coming to do. A room owner approves it on the web; the agent then receives its own scoped, expiring identity.",
|
|
3413
|
+
)
|
|
3414
|
+
.option("--deliverable <text>", "An output this agent will produce (repeatable)", collectOption, [])
|
|
3415
|
+
.option("--stop-when <text>", "A condition that ends this agent's work (repeatable)", collectOption, [])
|
|
3416
|
+
.option(
|
|
3417
|
+
"--scope <actions>",
|
|
3418
|
+
"Comma-separated actions to request: session.read, session.post, session.react, tickets.read, tickets.work",
|
|
3419
|
+
)
|
|
3420
|
+
.option("--ttl <duration>", "How long the grant should last, e.g. 90m or 2h (5m..24h)", "2h")
|
|
3421
|
+
.option("--provider <name>", "Model provider hint for the admission request")
|
|
3422
|
+
.option("--no-wait", "Request admission and return immediately with the approval URL")
|
|
3423
|
+
.option("--wait-seconds <n>", "How long to wait for approval before returning pending", "900")
|
|
3424
|
+
.option("--cancel-admission", "Cancel this workspace's pending admission request for the session")
|
|
3322
3425
|
.option("--json", "Emit machine-readable output")
|
|
3323
3426
|
.action(async (sessionId, options, command) => {
|
|
3324
3427
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -3329,6 +3432,34 @@ export function registerSessionCommand(program) {
|
|
|
3329
3432
|
const explicitAgent = normalizeString(options.agent);
|
|
3330
3433
|
const legacyName = normalizeString(options.name);
|
|
3331
3434
|
const inviteToken = normalizeString(options.inviteToken);
|
|
3435
|
+
const emitJson = shouldEmitJson(options, command);
|
|
3436
|
+
// ONE requested identity for the whole join, validated before any side effect.
|
|
3437
|
+
// --name is the identity only when --agent is absent (its legacy alias); beside an
|
|
3438
|
+
// explicit --agent it is a free-text display label. Either way the identity is
|
|
3439
|
+
// used in its canonical spelling from here on, so the admission, its receipt,
|
|
3440
|
+
// the local registration and every later implicit command name the same agent.
|
|
3441
|
+
if (!explicitAgent) assertCanonicalAgentId(legacyName, "--name");
|
|
3442
|
+
const requestedAgent = canonicalAgentId(explicitAgent || legacyName);
|
|
3443
|
+
if (options.cancelAdmission) {
|
|
3444
|
+
const cancelled = await cancelAdmission(normalizedSessionId, {
|
|
3445
|
+
agentId: requestedAgent,
|
|
3446
|
+
targetPath,
|
|
3447
|
+
});
|
|
3448
|
+
const payload = {
|
|
3449
|
+
command: "session join",
|
|
3450
|
+
joined: false,
|
|
3451
|
+
sessionId: normalizedSessionId,
|
|
3452
|
+
admission: {
|
|
3453
|
+
status: cancelled?.status || "cancelled",
|
|
3454
|
+
admissionId: cancelled?.admissionId || null,
|
|
3455
|
+
},
|
|
3456
|
+
};
|
|
3457
|
+
if (emitJson) console.log(JSON.stringify(payload, null, 2));
|
|
3458
|
+
else console.log(`Admission request ${payload.admission.admissionId || ""} cancelled.`);
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
// Membership first. Only a member may ask for an agent to be admitted, so an
|
|
3462
|
+
// invitation is accepted BEFORE the admission request, never after.
|
|
3332
3463
|
let invitationAccept = null;
|
|
3333
3464
|
let invitationAcceptResult = null;
|
|
3334
3465
|
if (inviteToken) {
|
|
@@ -3336,7 +3467,7 @@ export function registerSessionCommand(program) {
|
|
|
3336
3467
|
targetPath,
|
|
3337
3468
|
invitationToken: inviteToken,
|
|
3338
3469
|
seatKey: options.seatKey,
|
|
3339
|
-
agentId:
|
|
3470
|
+
agentId: requestedAgent,
|
|
3340
3471
|
idempotencyKey: options.idempotencyKey,
|
|
3341
3472
|
});
|
|
3342
3473
|
invitationAcceptResult =
|
|
@@ -3344,6 +3475,65 @@ export function registerSessionCommand(program) {
|
|
|
3344
3475
|
? invitationAccept.result
|
|
3345
3476
|
: null;
|
|
3346
3477
|
}
|
|
3478
|
+
let admission = null;
|
|
3479
|
+
if (normalizeString(options.goal)) {
|
|
3480
|
+
const admissionAgent = requestedAgent;
|
|
3481
|
+
if (!admissionAgent) {
|
|
3482
|
+
throw new Error("--goal requires --agent <id>: admission is requested for one named agent.");
|
|
3483
|
+
}
|
|
3484
|
+
const waitSeconds = Number.parseInt(String(options.waitSeconds ?? "900"), 10);
|
|
3485
|
+
admission = await runAdmissionJoin(normalizedSessionId, {
|
|
3486
|
+
agentId: admissionAgent,
|
|
3487
|
+
displayName: legacyName && explicitAgent ? legacyName : "",
|
|
3488
|
+
model: options.model,
|
|
3489
|
+
provider: options.provider,
|
|
3490
|
+
goal: options.goal,
|
|
3491
|
+
deliverables: options.deliverable || [],
|
|
3492
|
+
stopConditions: options.stopWhen || [],
|
|
3493
|
+
actions: parseAdmissionScope(options.scope),
|
|
3494
|
+
ttlSeconds: parseAdmissionTtlSeconds(options.ttl),
|
|
3495
|
+
wait: options.wait !== false,
|
|
3496
|
+
waitTimeoutMs: (Number.isFinite(waitSeconds) && waitSeconds >= 0 ? waitSeconds : 900) * 1000,
|
|
3497
|
+
targetPath,
|
|
3498
|
+
onPending: ({ approveUrl, phase }) => {
|
|
3499
|
+
// stderr, so --json stdout stays a single parseable document.
|
|
3500
|
+
const line = phase === "identity"
|
|
3501
|
+
? "Approval received; waiting for the AIdenID email and signed purpose receipt."
|
|
3502
|
+
: `Waiting for a room owner to approve this agent: ${terminalText(approveUrl, 1024)}`;
|
|
3503
|
+
if (emitJson) process.stderr.write(`${line}\n`);
|
|
3504
|
+
// Keep the line containing a remote URL free of all terminal control
|
|
3505
|
+
// bytes. `terminalText()` strips controls from the value itself; adding
|
|
3506
|
+
// local ANSI colour around it would make that guarantee untestable and
|
|
3507
|
+
// lets OSC-looking input hide inside a coloured terminal span.
|
|
3508
|
+
else if (phase === "identity") console.log(pc.yellow(line));
|
|
3509
|
+
else console.log(line);
|
|
3510
|
+
},
|
|
3511
|
+
});
|
|
3512
|
+
if (admission.status !== "active") {
|
|
3513
|
+
// Only an ACTIVE admission joins. Pending, denied, cancelled or expired never
|
|
3514
|
+
// registers the agent or materializes the room for it.
|
|
3515
|
+
process.exitCode = ["pending", "approved"].includes(admission.status) ? 3 : 4;
|
|
3516
|
+
const payload = {
|
|
3517
|
+
command: "session join",
|
|
3518
|
+
joined: false,
|
|
3519
|
+
sessionId: normalizedSessionId,
|
|
3520
|
+
agentId: admissionAgent,
|
|
3521
|
+
invitationAccepted: Boolean(invitationAcceptResult),
|
|
3522
|
+
admission,
|
|
3523
|
+
};
|
|
3524
|
+
if (emitJson) {
|
|
3525
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3526
|
+
} else if (admission.status === "pending") {
|
|
3527
|
+
const approval = terminalText(admission.approveUrl, 1024) || "(see the session page)";
|
|
3528
|
+
console.log(`Admission pending. A room owner approves it here: ${approval}`);
|
|
3529
|
+
} else if (admission.status === "approved") {
|
|
3530
|
+
console.log(pc.yellow("Admission approved; AIdenID identity evidence is still provisioning. The agent was not joined."));
|
|
3531
|
+
} else {
|
|
3532
|
+
console.log(pc.red(`Admission ${terminalText(admission.status, 16) || "unknown"}. The agent was not joined.`));
|
|
3533
|
+
}
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3347
3537
|
|
|
3348
3538
|
// PR #483 contract: verify the session exists and the caller has access
|
|
3349
3539
|
// BEFORE materializing local cache state. Without this we'd silently
|
|
@@ -3355,7 +3545,48 @@ export function registerSessionCommand(program) {
|
|
|
3355
3545
|
// before the invite is accepted the user may legitimately receive 403,
|
|
3356
3546
|
// so --invite-token performs the guarded accept mutation first and this
|
|
3357
3547
|
// verification proves membership immediately afterward.
|
|
3358
|
-
|
|
3548
|
+
// An admitted agent proves its grant is live with ITS OWN credential, via the
|
|
3549
|
+
// grant's receipt, and never with the human token. A cached "active" is not
|
|
3550
|
+
// reported until this succeeds (a revoked grant is not joined), and nothing
|
|
3551
|
+
// about the room is fetched on the human's authority.
|
|
3552
|
+
const admittedAgentId = admission ? requestedAgent : "";
|
|
3553
|
+
let admissionReceipt = null;
|
|
3554
|
+
if (admission) {
|
|
3555
|
+
try {
|
|
3556
|
+
admissionReceipt = await withAgentAdmission(normalizedSessionId, admittedAgentId, () =>
|
|
3557
|
+
fetchOwnAdmissionReceipt(normalizedSessionId, { targetPath }),
|
|
3558
|
+
);
|
|
3559
|
+
// The grant must be for EXACTLY the identity this join registers, byte for
|
|
3560
|
+
// byte: a receipt for any other spelling is a different agent's authority.
|
|
3561
|
+
const receiptAgent = String(admissionReceipt?.agentId ?? "");
|
|
3562
|
+
if (receiptAgent !== admittedAgentId) {
|
|
3563
|
+
throw new Error(
|
|
3564
|
+
`the admission is for agent "${receiptAgent || "(none)"}", not "${admittedAgentId}"`,
|
|
3565
|
+
);
|
|
3566
|
+
}
|
|
3567
|
+
} catch (error) {
|
|
3568
|
+
process.exitCode = 4;
|
|
3569
|
+
const payload = {
|
|
3570
|
+
command: "session join",
|
|
3571
|
+
joined: false,
|
|
3572
|
+
sessionId: normalizedSessionId,
|
|
3573
|
+
agentId: admittedAgentId,
|
|
3574
|
+
admission: {
|
|
3575
|
+
...admission,
|
|
3576
|
+
status: "not_accepted",
|
|
3577
|
+
verified: false,
|
|
3578
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
3579
|
+
},
|
|
3580
|
+
};
|
|
3581
|
+
if (emitJson) console.log(JSON.stringify(payload, null, 2));
|
|
3582
|
+
else console.log(pc.red(`The API no longer accepts this agent's admission; it was not joined.`));
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
admission = { ...admission, verified: true, receipt: admissionReceipt };
|
|
3586
|
+
}
|
|
3587
|
+
const verification = admission
|
|
3588
|
+
? { ok: true, source: "admission_receipt", session: { sessionId: normalizedSessionId } }
|
|
3589
|
+
: await verifyRemoteSession(normalizedSessionId, { targetPath });
|
|
3359
3590
|
if (!verification.ok) {
|
|
3360
3591
|
if (verification.status === 404 || verification.reason === "not_found") {
|
|
3361
3592
|
throw new Error(
|
|
@@ -3382,17 +3613,31 @@ export function registerSessionCommand(program) {
|
|
|
3382
3613
|
skipRemoteProbe: true,
|
|
3383
3614
|
remoteSession,
|
|
3384
3615
|
});
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3616
|
+
// History only for a grant that may read it, and then on the admission.
|
|
3617
|
+
const joinHydration = !admission
|
|
3618
|
+
? await hydrateJoinBriefingContext(normalizedSessionId, { targetPath })
|
|
3619
|
+
: (admissionReceipt?.actions || []).includes("session.read")
|
|
3620
|
+
? await withAgentAdmission(normalizedSessionId, admittedAgentId, () =>
|
|
3621
|
+
hydrateJoinBriefingContext(normalizedSessionId, { targetPath }),
|
|
3622
|
+
)
|
|
3623
|
+
: { skipped: true, reason: "the grant does not include session.read" };
|
|
3388
3624
|
|
|
3389
3625
|
const acceptedOnboarding = normalizeSessionOnboarding(
|
|
3390
3626
|
invitationAcceptResult?.onboarding,
|
|
3391
3627
|
invitationAcceptResult?.claimedSeat,
|
|
3392
3628
|
);
|
|
3393
3629
|
const acceptedAgentId = normalizeString(acceptedOnboarding?.agentId);
|
|
3394
|
-
|
|
3630
|
+
// An admitted join registers the admitted identity and nothing else. Otherwise an
|
|
3631
|
+
// invitation's onboarding agent id is the identity only when none was given, and
|
|
3632
|
+
// it is held to the same canonical rule before anything is registered locally.
|
|
3633
|
+
if (!admission && !explicitAgent && acceptedAgentId) {
|
|
3634
|
+
assertCanonicalAgentId(acceptedAgentId, "the invitation's agent id");
|
|
3635
|
+
}
|
|
3636
|
+
const agentSeed = admission ? admittedAgentId : explicitAgent || acceptedAgentId || legacyName;
|
|
3395
3637
|
const resolvedAgentId = await defaultAgentId(agentSeed, targetPath);
|
|
3638
|
+
if (admission && resolvedAgentId !== admittedAgentId) {
|
|
3639
|
+
throw new Error(`join would register "${resolvedAgentId}", not the admitted agent "${admittedAgentId}".`);
|
|
3640
|
+
}
|
|
3396
3641
|
const roleWasExplicit = optionWasSetByCli(command, "role");
|
|
3397
3642
|
const role =
|
|
3398
3643
|
normalizeString(roleWasExplicit ? options.role : acceptedOnboarding?.role || options.role) ||
|
|
@@ -3485,6 +3730,7 @@ export function registerSessionCommand(program) {
|
|
|
3485
3730
|
jsonPath: onboardingBrief.jsonPath,
|
|
3486
3731
|
}
|
|
3487
3732
|
: null,
|
|
3733
|
+
admission,
|
|
3488
3734
|
};
|
|
3489
3735
|
if (shouldEmitJson(options, command)) {
|
|
3490
3736
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -3505,6 +3751,23 @@ export function registerSessionCommand(program) {
|
|
|
3505
3751
|
console.log(pc.green(`Invitation accepted${seatLabel}`));
|
|
3506
3752
|
}
|
|
3507
3753
|
console.log(pc.gray(`agent=${joined.agentId} role=${joined.role} model=${joined.model}`));
|
|
3754
|
+
if (admission) {
|
|
3755
|
+
if (admission.status === "active") {
|
|
3756
|
+
const expires = new Date(admission.grant.expiresAt * 1000).toISOString();
|
|
3757
|
+
console.log(
|
|
3758
|
+
pc.green(
|
|
3759
|
+
`Admitted${admission.reused ? " (existing grant)" : ""}: ${admission.grant.actions.join(", ")} until ${expires}`,
|
|
3760
|
+
),
|
|
3761
|
+
);
|
|
3762
|
+
if (admission.identity?.subject) console.log(pc.gray(`identity=${admission.identity.subject}`));
|
|
3763
|
+
console.log(pc.gray(`credential stored at ${admission.credential.storage} (never printed)`));
|
|
3764
|
+
} else if (admission.status === "pending") {
|
|
3765
|
+
console.log(pc.yellow(`Admission pending approval: ${admission.approveUrl}`));
|
|
3766
|
+
console.log(pc.gray("Re-run the same command to resume waiting."));
|
|
3767
|
+
} else {
|
|
3768
|
+
console.log(pc.red(`Admission ${admission.status}.`));
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3508
3771
|
if (payload.onboardingGuide?.markdownPath) {
|
|
3509
3772
|
console.log(pc.gray(`onboarding=${payload.onboardingGuide.markdownPath}`));
|
|
3510
3773
|
}
|
|
@@ -3544,7 +3807,7 @@ export function registerSessionCommand(program) {
|
|
|
3544
3807
|
.option("--local-only", "Append only to the local session cache without remote send confirmation")
|
|
3545
3808
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3546
3809
|
.option("--json", "Emit machine-readable output")
|
|
3547
|
-
.action(async (sessionId, messageParts, options, command) => {
|
|
3810
|
+
.action(asAdmittedAgent(sessionSayAgent, async (sessionId, messageParts, options, command) => {
|
|
3548
3811
|
const normalizedSessionId = normalizeString(sessionId);
|
|
3549
3812
|
if (!normalizedSessionId) {
|
|
3550
3813
|
throw new Error("session id is required.");
|
|
@@ -3672,7 +3935,7 @@ export function registerSessionCommand(program) {
|
|
|
3672
3935
|
console.error(pc.yellow(`Identity warning: ${identity.identityWarning}`));
|
|
3673
3936
|
}
|
|
3674
3937
|
console.log(formatEventLine(persisted));
|
|
3675
|
-
});
|
|
3938
|
+
}));
|
|
3676
3939
|
|
|
3677
3940
|
session
|
|
3678
3941
|
.command("post-agent <sessionId> <message>")
|
|
@@ -3962,7 +4225,20 @@ export function registerSessionCommand(program) {
|
|
|
3962
4225
|
console.log(formatEventLine(persisted));
|
|
3963
4226
|
});
|
|
3964
4227
|
|
|
3965
|
-
|
|
4228
|
+
// Every message-action command (react, reply, ack, ...) runs as its authoring
|
|
4229
|
+
// agent: inside that agent's admission when this machine holds one.
|
|
4230
|
+
async function runMessageActionCommand(args = {}) {
|
|
4231
|
+
const sid = normalizeString(args.sessionId);
|
|
4232
|
+
const identity = await resolveMessageActionIdentity({
|
|
4233
|
+
sessionId: sid,
|
|
4234
|
+
optionAgent: args.options?.agent,
|
|
4235
|
+
targetPath: path.resolve(process.cwd(), String(args.options?.path || ".")),
|
|
4236
|
+
env: process.env,
|
|
4237
|
+
});
|
|
4238
|
+
return withAgentAdmission(sid, identity.agentId, () => runMessageActionCommandUnscoped(args));
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
async function runMessageActionCommandUnscoped({
|
|
3966
4242
|
sessionId,
|
|
3967
4243
|
actionType,
|
|
3968
4244
|
options,
|
|
@@ -4193,20 +4469,50 @@ export function registerSessionCommand(program) {
|
|
|
4193
4469
|
});
|
|
4194
4470
|
|
|
4195
4471
|
session
|
|
4196
|
-
.command("
|
|
4197
|
-
.description("
|
|
4198
|
-
.option("--agent <id>", "
|
|
4199
|
-
.option("--
|
|
4472
|
+
.command("edit <sessionId> <target> <message...>")
|
|
4473
|
+
.description("Edit your message by sequence or threaded reply UUID; preserves its audit history")
|
|
4474
|
+
.option("--agent <id>", "Authoring agent (defaults to the joined session agent)")
|
|
4475
|
+
.option("--expected-revision <n>", "Guard against concurrent edits; otherwise fetch the current revision once")
|
|
4476
|
+
.option("--idempotency-key <key>", "Reuse an explicit key when retrying the same edit")
|
|
4200
4477
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4201
4478
|
.option("--json", "Emit machine-readable output")
|
|
4202
|
-
.action(async (sessionId,
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4479
|
+
.action(async (sessionId, target, messageParts, options, command) => {
|
|
4480
|
+
const sid = normalizeString(sessionId);
|
|
4481
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4482
|
+
const text = Array.isArray(messageParts) ? messageParts.join(" ") : String(messageParts || "");
|
|
4483
|
+
const sequence = /^\d+$/.test(target) ? Number(target) : null;
|
|
4484
|
+
if (sequence !== null && (!Number.isSafeInteger(sequence) || sequence <= 0)) throw new Error("target sequence must be a positive safe integer.");
|
|
4485
|
+
if (!sequence && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(target)) {
|
|
4486
|
+
throw new Error("Edit target must be a positive message sequence or a threaded reply UUID.");
|
|
4487
|
+
}
|
|
4488
|
+
const identity = await resolveMessageActionIdentity({ sessionId: sid, optionAgent: options.agent, targetPath, env: process.env });
|
|
4489
|
+
if (shouldBlockImplicitCliUserSessionSay(identity)) throw new Error(identity.identityWarning || "session edit requires an authoring agent identity; pass --agent <id>.");
|
|
4490
|
+
const expectedRevision = options.expectedRevision === undefined ? null : Number(options.expectedRevision);
|
|
4491
|
+
if (expectedRevision !== null && (!Number.isSafeInteger(expectedRevision) || expectedRevision <= 0)) throw new Error("expected-revision must be a positive safe integer.");
|
|
4492
|
+
return withAgentAdmission(sid, identity.agentId, async () => {
|
|
4493
|
+
const result = await editSessionMessage(sid, {
|
|
4494
|
+
targetPath,
|
|
4495
|
+
targetSequenceId: sequence,
|
|
4496
|
+
targetActionId: sequence ? "" : target,
|
|
4497
|
+
text,
|
|
4498
|
+
agentId: identity.agentId,
|
|
4499
|
+
expectedRevision,
|
|
4500
|
+
idempotencyKey: options.idempotencyKey,
|
|
4501
|
+
timeoutMs: 15_000,
|
|
4502
|
+
});
|
|
4503
|
+
if (!result.ok) throw new Error(`Session edit failed (${result.reason || "unknown"}). No local message was changed.`);
|
|
4504
|
+
let localAppend = { appended: 0, upgraded: 0, failed: 0 };
|
|
4505
|
+
try {
|
|
4506
|
+
await ensureLocalSessionForRemoteCommand(sid, { targetPath });
|
|
4507
|
+
const current = result.reply ? replyRevisionEvent(sid, result.reply) : result.event;
|
|
4508
|
+
localAppend = await appendMissingRemoteEvents(sid, [current, result.notification].filter(Boolean), { targetPath });
|
|
4509
|
+
} catch {
|
|
4510
|
+
localAppend = { appended: 0, upgraded: 0, failed: 1 };
|
|
4511
|
+
}
|
|
4512
|
+
const payload = { command: "session edit", sessionId: sid, target, ...result, localAppend, localAppendComplete: localAppend.failed === 0 };
|
|
4513
|
+
if (shouldEmitJson(options, command)) console.log(JSON.stringify(payload, null, 2));
|
|
4514
|
+
else console.log(`${result.changed ? "Edited" : "Confirmed"} ${sequence ? `#${sequence}` : `reply ${target}`} at revision ${messageRevision(result.reply || result.event)}.${localAppend.failed ? " Remote edit succeeded; local cache needs sync." : ""}`);
|
|
4515
|
+
return payload;
|
|
4210
4516
|
});
|
|
4211
4517
|
});
|
|
4212
4518
|
|
|
@@ -4951,7 +5257,7 @@ export function registerSessionCommand(program) {
|
|
|
4951
5257
|
`Total log files to retain including the active file (default ${DEFAULT_ROTATING_LOG_MAX_FILES})`,
|
|
4952
5258
|
String(DEFAULT_ROTATING_LOG_MAX_FILES),
|
|
4953
5259
|
)
|
|
4954
|
-
.action(async (options) => {
|
|
5260
|
+
.action(asAdmittedAgent(sessionListenAgent, async (options) => {
|
|
4955
5261
|
const normalizedSessionId = resolveSessionIdOption(options);
|
|
4956
5262
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4957
5263
|
const agentId = normalizeAgentId(options.agent, "cli-user");
|
|
@@ -5043,9 +5349,15 @@ export function registerSessionCommand(program) {
|
|
|
5043
5349
|
options.maxPolls === undefined
|
|
5044
5350
|
? null
|
|
5045
5351
|
: parsePositiveInteger(options.maxPolls, "max-polls", 1);
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5352
|
+
if (currentAdmittedAgent() && requestedTransport === "stream") {
|
|
5353
|
+
throw new Error(
|
|
5354
|
+
"An admitted agent listens by polling: the event stream is not part of an admission grant, so a revoke never has to chase an open stream. Use --transport poll.",
|
|
5355
|
+
);
|
|
5356
|
+
}
|
|
5357
|
+
const listenTransport =
|
|
5358
|
+
requestedTransport === "auto" && (maxPolls !== null || currentAdmittedAgent())
|
|
5359
|
+
? "poll"
|
|
5360
|
+
: requestedTransport;
|
|
5049
5361
|
const since = options.since === undefined ? undefined : String(options.since);
|
|
5050
5362
|
if (options.fromNow && options.since !== undefined) {
|
|
5051
5363
|
throw new Error("Use either --from-now or --since, not both.");
|
|
@@ -5367,7 +5679,7 @@ export function registerSessionCommand(program) {
|
|
|
5367
5679
|
}).catch(() => {});
|
|
5368
5680
|
}
|
|
5369
5681
|
}
|
|
5370
|
-
});
|
|
5682
|
+
}));
|
|
5371
5683
|
|
|
5372
5684
|
session
|
|
5373
5685
|
.command("daemon [sessionId]")
|
|
@@ -5939,7 +6251,7 @@ export function registerSessionCommand(program) {
|
|
|
5939
6251
|
.option("--include-control-events", "Include listener lifecycle/control-plane events in transcript output")
|
|
5940
6252
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
5941
6253
|
.option("--json", "Emit machine-readable output")
|
|
5942
|
-
.action(async (sessionId, options, command) => {
|
|
6254
|
+
.action(asAdmittedAgent(sessionReadAgent, async (sessionId, options, command) => {
|
|
5943
6255
|
const normalizedSessionId = normalizeString(sessionId);
|
|
5944
6256
|
if (!normalizedSessionId) {
|
|
5945
6257
|
throw new Error("session id is required.");
|
|
@@ -6283,7 +6595,7 @@ export function registerSessionCommand(program) {
|
|
|
6283
6595
|
console.log(formatEventLine(event));
|
|
6284
6596
|
}
|
|
6285
6597
|
}
|
|
6286
|
-
});
|
|
6598
|
+
}));
|
|
6287
6599
|
|
|
6288
6600
|
session
|
|
6289
6601
|
.command("search <sessionId> <query>")
|
|
@@ -6492,6 +6804,426 @@ export function registerSessionCommand(program) {
|
|
|
6492
6804
|
}
|
|
6493
6805
|
});
|
|
6494
6806
|
|
|
6807
|
+
// ------------------------------------------------------- agent access
|
|
6808
|
+
// Human control plane for the Agent Admission Contract. The request command
|
|
6809
|
+
// delegates to the established agent-side flow; list/decision/revoke use the
|
|
6810
|
+
// matching owner-only REST routes. Goal text is an agent claim and is always
|
|
6811
|
+
// rendered as bounded, untrusted data.
|
|
6812
|
+
const access = session
|
|
6813
|
+
.command("access")
|
|
6814
|
+
.description("Request and govern AIdenID-backed, scoped agent access to a session");
|
|
6815
|
+
|
|
6816
|
+
function accessOutput(options, command, payload, render) {
|
|
6817
|
+
if (shouldEmitJson(options, command)) {
|
|
6818
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
6819
|
+
return;
|
|
6820
|
+
}
|
|
6821
|
+
render();
|
|
6822
|
+
}
|
|
6823
|
+
|
|
6824
|
+
function terminalText(value, maxLength = 240) {
|
|
6825
|
+
let result = "";
|
|
6826
|
+
for (const ch of normalizeString(value).normalize("NFC")) {
|
|
6827
|
+
// Refuse terminal control/invisible formatting from remote values,
|
|
6828
|
+
// including ANSI/OSC initiators and bidi overrides/isolates.
|
|
6829
|
+
result += /[\p{Cc}\p{Cf}\p{Cs}\p{Co}\p{Cn}\p{Zl}\p{Zp}]/u.test(ch) ? " " : ch;
|
|
6830
|
+
if (result.length >= maxLength) break;
|
|
6831
|
+
}
|
|
6832
|
+
return result.replace(/\s+/gu, " ").trim();
|
|
6833
|
+
}
|
|
6834
|
+
|
|
6835
|
+
function renderAdmissionLine(admission) {
|
|
6836
|
+
const requested = Array.isArray(admission?.requestedScope?.actions)
|
|
6837
|
+
? terminalText(admission.requestedScope.actions.join(","), 180)
|
|
6838
|
+
: "scope unavailable";
|
|
6839
|
+
const ttl = Number(admission?.requestedScope?.ttlSeconds);
|
|
6840
|
+
const duration = Number.isFinite(ttl) ? ` for ${ttl}s` : "";
|
|
6841
|
+
const identity = terminalText(admission?.displayName, 120) || terminalText(admission?.agentId, 64) || "unknown-agent";
|
|
6842
|
+
console.log(`${terminalText(admission?.admissionId, 64) || "unknown"} ${terminalText(admission?.status, 16) || "unknown"} ${identity}`);
|
|
6843
|
+
console.log(pc.gray(` requested: ${requested}${duration}`));
|
|
6844
|
+
const summary = terminalText(admission?.goal?.summary);
|
|
6845
|
+
if (summary) console.log(pc.gray(` claimed goal (untrusted): ${JSON.stringify(summary)}`));
|
|
6846
|
+
if (admission?.email) {
|
|
6847
|
+
// Human terminal output is routinely copied into logs and support
|
|
6848
|
+
// transcripts. Keep the address in the explicit --json response only.
|
|
6849
|
+
const inboxStatus = terminalText(admission.email.status, 32) || "unknown";
|
|
6850
|
+
console.log(pc.gray(` AIdenID inbox: ${inboxStatus}`));
|
|
6851
|
+
}
|
|
6852
|
+
}
|
|
6853
|
+
|
|
6854
|
+
access
|
|
6855
|
+
.command("mode <sessionId> <mode>")
|
|
6856
|
+
.description("Set whether this room requires admission for agent-attributed access (required or legacy)")
|
|
6857
|
+
.option("--idempotency-key <key>", "Retry the same logical mode change safely")
|
|
6858
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
6859
|
+
.option("--json", "Emit machine-readable output")
|
|
6860
|
+
.action(async (sessionId, mode, options, command) => {
|
|
6861
|
+
const sid = normalizeString(sessionId);
|
|
6862
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6863
|
+
const changed = await setSessionAdmissionMode(sid, mode, {
|
|
6864
|
+
idempotencyKey: options.idempotencyKey,
|
|
6865
|
+
targetPath,
|
|
6866
|
+
});
|
|
6867
|
+
accessOutput(
|
|
6868
|
+
options,
|
|
6869
|
+
command,
|
|
6870
|
+
{
|
|
6871
|
+
command: "session access mode",
|
|
6872
|
+
idempotencyKey: changed.idempotencyKey,
|
|
6873
|
+
...changed.result,
|
|
6874
|
+
},
|
|
6875
|
+
() => console.log(
|
|
6876
|
+
`Agent admission mode for ${terminalText(sid, 64)}: ${terminalText(changed.result.agentAdmissionMode, 16) || "unknown"}.`,
|
|
6877
|
+
),
|
|
6878
|
+
);
|
|
6879
|
+
});
|
|
6880
|
+
|
|
6881
|
+
access
|
|
6882
|
+
.command("request <sessionId>")
|
|
6883
|
+
.description("Request agent access; a human must approve before the agent can claim or join")
|
|
6884
|
+
.requiredOption("--agent <id>", "Canonical agent id requesting access")
|
|
6885
|
+
.requiredOption("--goal <text>", "What the agent is coming to do")
|
|
6886
|
+
.option("--display-name <name>", "Human-readable agent name")
|
|
6887
|
+
.option("--deliverable <text>", "An output this agent will produce (repeatable)", collectOption, [])
|
|
6888
|
+
.option("--stop-when <text>", "A condition that ends this agent's work (repeatable)", collectOption, [])
|
|
6889
|
+
.option(
|
|
6890
|
+
"--scope <actions>",
|
|
6891
|
+
"Comma-separated actions: session.read, session.post, session.react, tickets.read, tickets.work",
|
|
6892
|
+
)
|
|
6893
|
+
.option("--ttl <duration>", "Requested grant lifetime, e.g. 90m or 24h (5m..24h)", "2h")
|
|
6894
|
+
.option("--model <model>", "Agent model hint", "cli")
|
|
6895
|
+
.option("--provider <name>", "Model provider hint")
|
|
6896
|
+
.option("--wait", "Wait for approval and claim the grant on this machine")
|
|
6897
|
+
.option("--wait-seconds <n>", "Maximum approval wait when --wait is set", "900")
|
|
6898
|
+
.option("--path <path>", "Workspace path for local admission state", ".")
|
|
6899
|
+
.option("--json", "Emit machine-readable output")
|
|
6900
|
+
.action(async (sessionId, options, command) => {
|
|
6901
|
+
const sid = normalizeString(sessionId);
|
|
6902
|
+
const agentId = canonicalAgentId(options.agent);
|
|
6903
|
+
assertCanonicalAgentId(options.agent, "--agent");
|
|
6904
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6905
|
+
const waitSeconds = Number.parseInt(String(options.waitSeconds ?? "900"), 10);
|
|
6906
|
+
if (!Number.isFinite(waitSeconds) || waitSeconds < 0) {
|
|
6907
|
+
throw new Error("--wait-seconds must be a non-negative integer.");
|
|
6908
|
+
}
|
|
6909
|
+
const admission = await runAdmissionJoin(sid, {
|
|
6910
|
+
agentId,
|
|
6911
|
+
displayName: options.displayName,
|
|
6912
|
+
model: options.model,
|
|
6913
|
+
provider: options.provider,
|
|
6914
|
+
goal: options.goal,
|
|
6915
|
+
deliverables: options.deliverable || [],
|
|
6916
|
+
stopConditions: options.stopWhen || [],
|
|
6917
|
+
actions: parseAdmissionScope(options.scope),
|
|
6918
|
+
ttlSeconds: parseAdmissionTtlSeconds(options.ttl),
|
|
6919
|
+
wait: Boolean(options.wait),
|
|
6920
|
+
waitTimeoutMs: waitSeconds * 1000,
|
|
6921
|
+
targetPath,
|
|
6922
|
+
onPending: ({ approveUrl, phase }) => {
|
|
6923
|
+
const line = phase === "identity"
|
|
6924
|
+
? "Approval received; waiting for the AIdenID email and signed purpose receipt."
|
|
6925
|
+
: `Waiting for a room owner to approve this agent: ${terminalText(approveUrl, 1024)}`;
|
|
6926
|
+
if (shouldEmitJson(options, command)) process.stderr.write(`${line}\n`);
|
|
6927
|
+
// A line carrying a remote URL must remain free of every terminal
|
|
6928
|
+
// control byte, including trusted colour wrappers. This mirrors the
|
|
6929
|
+
// guarded `session join` path and keeps raw support logs auditable.
|
|
6930
|
+
else if (phase === "identity") console.log(pc.yellow(line));
|
|
6931
|
+
else console.log(line);
|
|
6932
|
+
},
|
|
6933
|
+
});
|
|
6934
|
+
const payload = {
|
|
6935
|
+
command: "session access request",
|
|
6936
|
+
sessionId: sid,
|
|
6937
|
+
agentId,
|
|
6938
|
+
admission,
|
|
6939
|
+
readyToJoin: admission.status === "active",
|
|
6940
|
+
};
|
|
6941
|
+
accessOutput(options, command, payload, () => {
|
|
6942
|
+
if (admission.status === "pending") {
|
|
6943
|
+
console.log(`Access requested for ${agentId}; waiting for a room owner.`);
|
|
6944
|
+
if (admission.approveUrl) console.log(`Approval: ${terminalText(admission.approveUrl, 1024)}`);
|
|
6945
|
+
} else if (admission.status === "active") {
|
|
6946
|
+
console.log(`Access active for ${agentId}. The scoped credential is stored locally and was not printed.`);
|
|
6947
|
+
console.log(pc.gray("Run `sl session join` again with the same goal, scope, and TTL to attach this agent."));
|
|
6948
|
+
} else if (admission.status === "approved") {
|
|
6949
|
+
console.log("Access approved; waiting for the AIdenID inbox and signed purpose receipt before claim.");
|
|
6950
|
+
} else {
|
|
6951
|
+
console.log(`Access ${terminalText(admission.status, 16) || "unknown"} for ${agentId}.`);
|
|
6952
|
+
}
|
|
6953
|
+
});
|
|
6954
|
+
});
|
|
6955
|
+
|
|
6956
|
+
access
|
|
6957
|
+
.command("list <sessionId>")
|
|
6958
|
+
.description("List admission requests visible to a room owner or admin")
|
|
6959
|
+
.option("--status <status>", "Filter by admission status")
|
|
6960
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
6961
|
+
.option("--json", "Emit machine-readable output")
|
|
6962
|
+
.action(async (sessionId, options, command) => {
|
|
6963
|
+
const sid = normalizeString(sessionId);
|
|
6964
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6965
|
+
const result = await listSessionAdmissions(sid, {
|
|
6966
|
+
status: options.status,
|
|
6967
|
+
targetPath,
|
|
6968
|
+
});
|
|
6969
|
+
const payload = { command: "session access list", ...result };
|
|
6970
|
+
accessOutput(options, command, payload, () => {
|
|
6971
|
+
console.log(pc.gray(`Admission mode: ${terminalText(result.agentAdmissionMode, 16) || "unknown"}`));
|
|
6972
|
+
if (!Array.isArray(result.admissions) || result.admissions.length === 0) {
|
|
6973
|
+
console.log("No admission requests.");
|
|
6974
|
+
return;
|
|
6975
|
+
}
|
|
6976
|
+
for (const admission of result.admissions) renderAdmissionLine(admission);
|
|
6977
|
+
});
|
|
6978
|
+
});
|
|
6979
|
+
|
|
6980
|
+
access
|
|
6981
|
+
.command("status <sessionId> <admissionId>")
|
|
6982
|
+
.description("Show the requesting delegator's current admission status")
|
|
6983
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
6984
|
+
.option("--json", "Emit machine-readable output")
|
|
6985
|
+
.action(async (sessionId, admissionId, options, command) => {
|
|
6986
|
+
const sid = normalizeString(sessionId);
|
|
6987
|
+
const aid = normalizeString(admissionId);
|
|
6988
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6989
|
+
const result = await getSessionAdmission(sid, aid, { targetPath });
|
|
6990
|
+
accessOutput(
|
|
6991
|
+
options,
|
|
6992
|
+
command,
|
|
6993
|
+
{ command: "session access status", sessionId: sid, ...result },
|
|
6994
|
+
() => {
|
|
6995
|
+
console.log(`${terminalText(result.admissionId, 64) || aid} ${terminalText(result.status, 16) || "unknown"} ${terminalText(result.agentId, 64) || "unknown-agent"}`);
|
|
6996
|
+
if (result.approveUrl) console.log(`Approval: ${terminalText(result.approveUrl, 1024)}`);
|
|
6997
|
+
},
|
|
6998
|
+
);
|
|
6999
|
+
});
|
|
7000
|
+
|
|
7001
|
+
access
|
|
7002
|
+
.command("approve <sessionId> <admissionId>")
|
|
7003
|
+
.description("Approve a pending request, optionally narrowing its scope or TTL")
|
|
7004
|
+
.option("--scope <actions>", "Comma-separated granted actions; must be a subset of the request")
|
|
7005
|
+
.option("--ttl <duration>", "Granted lifetime; may only shorten the request (5m..24h)")
|
|
7006
|
+
.option("--note <text>", "Approval note (max 500 characters)")
|
|
7007
|
+
.option("--idempotency-key <key>", "Retry the same logical approval safely")
|
|
7008
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
7009
|
+
.option("--json", "Emit machine-readable output")
|
|
7010
|
+
.action(async (sessionId, admissionId, options, command) => {
|
|
7011
|
+
const sid = normalizeString(sessionId);
|
|
7012
|
+
const aid = normalizeString(admissionId);
|
|
7013
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7014
|
+
const decision = await decideSessionAdmission(sid, aid, {
|
|
7015
|
+
decision: "approve",
|
|
7016
|
+
grantedActions: options.scope === undefined ? undefined : parseAdmissionScope(options.scope),
|
|
7017
|
+
ttlSeconds: options.ttl === undefined ? undefined : parseAdmissionTtlSeconds(options.ttl),
|
|
7018
|
+
note: options.note,
|
|
7019
|
+
idempotencyKey: options.idempotencyKey,
|
|
7020
|
+
targetPath,
|
|
7021
|
+
});
|
|
7022
|
+
const result = decision.result;
|
|
7023
|
+
accessOutput(
|
|
7024
|
+
options,
|
|
7025
|
+
command,
|
|
7026
|
+
{ command: "session access approve", sessionId: sid, idempotencyKey: decision.idempotencyKey, admission: result },
|
|
7027
|
+
() => {
|
|
7028
|
+
console.log(`Approved ${terminalText(result.admissionId, 64) || aid} for ${terminalText(result.agentId, 64) || "the agent"}.`);
|
|
7029
|
+
if (result.email) {
|
|
7030
|
+
const inboxStatus = terminalText(result.email.status, 32) || "queued";
|
|
7031
|
+
console.log(pc.gray(`AIdenID inbox: ${inboxStatus}. Use --json to inspect its address.`));
|
|
7032
|
+
}
|
|
7033
|
+
},
|
|
7034
|
+
);
|
|
7035
|
+
});
|
|
7036
|
+
|
|
7037
|
+
access
|
|
7038
|
+
.command("deny <sessionId> <admissionId>")
|
|
7039
|
+
.description("Deny a pending agent admission request")
|
|
7040
|
+
.option("--note <text>", "Denial note (max 500 characters)")
|
|
7041
|
+
.option("--idempotency-key <key>", "Retry the same logical denial safely")
|
|
7042
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
7043
|
+
.option("--json", "Emit machine-readable output")
|
|
7044
|
+
.action(async (sessionId, admissionId, options, command) => {
|
|
7045
|
+
const sid = normalizeString(sessionId);
|
|
7046
|
+
const aid = normalizeString(admissionId);
|
|
7047
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7048
|
+
const decision = await decideSessionAdmission(sid, aid, {
|
|
7049
|
+
decision: "deny",
|
|
7050
|
+
note: options.note,
|
|
7051
|
+
idempotencyKey: options.idempotencyKey,
|
|
7052
|
+
targetPath,
|
|
7053
|
+
});
|
|
7054
|
+
accessOutput(
|
|
7055
|
+
options,
|
|
7056
|
+
command,
|
|
7057
|
+
{ command: "session access deny", sessionId: sid, idempotencyKey: decision.idempotencyKey, admission: decision.result },
|
|
7058
|
+
() => console.log(`Denied ${terminalText(decision.result.admissionId, 64) || aid}.`),
|
|
7059
|
+
);
|
|
7060
|
+
});
|
|
7061
|
+
|
|
7062
|
+
access
|
|
7063
|
+
.command("revoke <sessionId> <admissionId>")
|
|
7064
|
+
.description("Revoke an approved or active agent admission")
|
|
7065
|
+
.option("--reason <text>", "Revocation reason (max 200 characters)")
|
|
7066
|
+
.option("--idempotency-key <key>", "Retry the same logical revocation safely")
|
|
7067
|
+
.option("--path <path>", "Workspace path for auth context", ".")
|
|
7068
|
+
.option("--json", "Emit machine-readable output")
|
|
7069
|
+
.action(async (sessionId, admissionId, options, command) => {
|
|
7070
|
+
const sid = normalizeString(sessionId);
|
|
7071
|
+
const aid = normalizeString(admissionId);
|
|
7072
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7073
|
+
const revoked = await revokeSessionAdmission(sid, aid, {
|
|
7074
|
+
reason: options.reason,
|
|
7075
|
+
idempotencyKey: options.idempotencyKey,
|
|
7076
|
+
targetPath,
|
|
7077
|
+
});
|
|
7078
|
+
accessOutput(
|
|
7079
|
+
options,
|
|
7080
|
+
command,
|
|
7081
|
+
{ command: "session access revoke", sessionId: sid, idempotencyKey: revoked.idempotencyKey, admission: revoked.result },
|
|
7082
|
+
() => console.log(`Revoked ${terminalText(revoked.result.admissionId, 64) || aid}.`),
|
|
7083
|
+
);
|
|
7084
|
+
});
|
|
7085
|
+
|
|
7086
|
+
// ---------------------------------------------------------------- tickets
|
|
7087
|
+
// The room's shared backlog (API "Ticket Lease Contract (T2)"). Every subcommand is
|
|
7088
|
+
// an ACTOR command: the runCli choke point runs it on the agent's admission.
|
|
7089
|
+
const ticket = session
|
|
7090
|
+
.command("ticket")
|
|
7091
|
+
.description("The room's shared backlog: list, claim, renew, release and submit tickets");
|
|
7092
|
+
|
|
7093
|
+
// Ticket work is attributed by the CREDENTIAL, never by a label: the lease routes take the
|
|
7094
|
+
// holder from authentication alone. So an agent identity is allowed only with that agent's
|
|
7095
|
+
// live admission in scope; without it, the request would go out on the human token and
|
|
7096
|
+
// claim as the PERSON while this command reported the agent. Refused before any request.
|
|
7097
|
+
async function ticketIdentity(sessionId, options, targetPath) {
|
|
7098
|
+
const identity = await resolveSessionSayIdentity({ sessionId, agentId: options.agent, targetPath });
|
|
7099
|
+
const agent = normalizeString(identity.agentId);
|
|
7100
|
+
if (!agent || agent === "cli-user") return "human";
|
|
7101
|
+
const admitted = currentAdmittedAgent();
|
|
7102
|
+
if (!admitted || admitted.sessionId !== sessionId || admitted.agentId !== agent) {
|
|
7103
|
+
throw new Error(
|
|
7104
|
+
`Agent "${agent}" has no live admission in this room. Ticket work is attributed by the credential, so ` +
|
|
7105
|
+
`without one this would act as you, not as "${agent}". Admit it first: ` +
|
|
7106
|
+
`sl session join ${sessionId} --agent ${agent} --goal "<what it is here to do>"`
|
|
7107
|
+
);
|
|
7108
|
+
}
|
|
7109
|
+
return agent;
|
|
7110
|
+
}
|
|
7111
|
+
|
|
7112
|
+
function ticketOutput(options, command, payload, line) {
|
|
7113
|
+
if (shouldEmitJson(options, command)) {
|
|
7114
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
7115
|
+
return;
|
|
7116
|
+
}
|
|
7117
|
+
console.log(line);
|
|
7118
|
+
}
|
|
7119
|
+
|
|
7120
|
+
ticket
|
|
7121
|
+
.command("list <sessionId>")
|
|
7122
|
+
.description("List the room's tickets (--available: only those claimable now)")
|
|
7123
|
+
.option("--available", "Only tickets that can be claimed now (never blocked work)")
|
|
7124
|
+
.option("--agent <id>", "Agent id reading the backlog (defaults to the joined session agent)")
|
|
7125
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
7126
|
+
.option("--json", "Emit machine-readable output")
|
|
7127
|
+
.action(async (sessionId, options, command) => {
|
|
7128
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7129
|
+
const result = await listTickets(normalizeString(sessionId), { targetPath, available: Boolean(options.available) });
|
|
7130
|
+
const lines = result.items.map((t) => {
|
|
7131
|
+
const holder = t.lease ? ` · ${t.lease.live ? "leased by" : "lease EXPIRED, was"} ${t.lease.holder}` : "";
|
|
7132
|
+
const done = t.completion && !t.completion.verified ? " (manual, unverified)" : "";
|
|
7133
|
+
return `${t.id} ${t.status}${done} v${t.version} ${t.title}${holder}`;
|
|
7134
|
+
});
|
|
7135
|
+
ticketOutput(options, command, { command: "session ticket list", ...result }, lines.join("\n") || "No tickets.");
|
|
7136
|
+
});
|
|
7137
|
+
|
|
7138
|
+
ticket
|
|
7139
|
+
.command("claim <sessionId> <ticketId>")
|
|
7140
|
+
.description("Claim a ticket: take its fenced lease")
|
|
7141
|
+
.option("--expected-version <n>", "Refuse if the ticket has changed since this version")
|
|
7142
|
+
.option("--agent <id>", "Agent id claiming the ticket (defaults to the joined session agent)")
|
|
7143
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
7144
|
+
.option("--json", "Emit machine-readable output")
|
|
7145
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
7146
|
+
const sid = normalizeString(sessionId);
|
|
7147
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7148
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
7149
|
+
const expectedVersion =
|
|
7150
|
+
options.expectedVersion === undefined ? null : parsePositiveInteger(options.expectedVersion, "expected-version", 1);
|
|
7151
|
+
const result = await claimTicket(sid, normalizeString(ticketId), { identity, expectedVersion, targetPath });
|
|
7152
|
+
ticketOutput(options, command, { command: "session ticket claim", identity, ...result },
|
|
7153
|
+
`Claimed ${ticketId} (fence ${result.lease.fence}, until ${result.lease.expiresAt}).`);
|
|
7154
|
+
});
|
|
7155
|
+
|
|
7156
|
+
ticket
|
|
7157
|
+
.command("renew <sessionId> <ticketId>")
|
|
7158
|
+
.description("Renew the lease this identity holds on a ticket")
|
|
7159
|
+
.option("--agent <id>", "Agent id holding the lease (defaults to the joined session agent)")
|
|
7160
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
7161
|
+
.option("--json", "Emit machine-readable output")
|
|
7162
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
7163
|
+
const sid = normalizeString(sessionId);
|
|
7164
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7165
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
7166
|
+
const result = await renewTicketLease(sid, normalizeString(ticketId), { identity, targetPath });
|
|
7167
|
+
ticketOutput(options, command, { command: "session ticket renew", identity, ...result },
|
|
7168
|
+
`Renewed ${ticketId} until ${result.lease.expiresAt}.`);
|
|
7169
|
+
});
|
|
7170
|
+
|
|
7171
|
+
ticket
|
|
7172
|
+
.command("release <sessionId> <ticketId>")
|
|
7173
|
+
.description("Give a ticket back: it returns to open for the next claimant")
|
|
7174
|
+
.option("--reason <text>", "Why it is being released")
|
|
7175
|
+
.option("--agent <id>", "Agent id holding the lease (defaults to the joined session agent)")
|
|
7176
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
7177
|
+
.option("--json", "Emit machine-readable output")
|
|
7178
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
7179
|
+
const sid = normalizeString(sessionId);
|
|
7180
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7181
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
7182
|
+
const result = await releaseTicketLease(sid, normalizeString(ticketId), {
|
|
7183
|
+
identity, targetPath, reason: normalizeString(options.reason) || null,
|
|
7184
|
+
});
|
|
7185
|
+
ticketOutput(options, command, { command: "session ticket release", identity, ...result }, `Released ${ticketId}.`);
|
|
7186
|
+
});
|
|
7187
|
+
|
|
7188
|
+
ticket
|
|
7189
|
+
.command("submit <sessionId> <ticketId>")
|
|
7190
|
+
.description("Submit the work for review (in_review). Completion needs verification; this is not done.")
|
|
7191
|
+
.option("--sha <sha>", "The implementation commit")
|
|
7192
|
+
.option("--evidence-file <path>", "JSON array of re-checkable evidence claims")
|
|
7193
|
+
.option("--agent <id>", "Agent id holding the lease (defaults to the joined session agent)")
|
|
7194
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
7195
|
+
.option("--json", "Emit machine-readable output")
|
|
7196
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
7197
|
+
const sid = normalizeString(sessionId);
|
|
7198
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7199
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
7200
|
+
let evidence = [];
|
|
7201
|
+
if (options.evidenceFile) {
|
|
7202
|
+
evidence = JSON.parse(await fsp.readFile(path.resolve(targetPath, String(options.evidenceFile)), "utf8"));
|
|
7203
|
+
if (!Array.isArray(evidence)) throw new Error("--evidence-file must contain a JSON array.");
|
|
7204
|
+
}
|
|
7205
|
+
const result = await submitTicket(sid, normalizeString(ticketId), {
|
|
7206
|
+
identity, targetPath, implementationSha: normalizeString(options.sha) || null, evidence,
|
|
7207
|
+
});
|
|
7208
|
+
ticketOutput(options, command, { command: "session ticket submit", identity, ...result },
|
|
7209
|
+
`Submitted ${ticketId} for review.`);
|
|
7210
|
+
});
|
|
7211
|
+
|
|
7212
|
+
ticket
|
|
7213
|
+
.command("events <sessionId>")
|
|
7214
|
+
.description("The room's ticket events after a cursor, in commit order")
|
|
7215
|
+
.option("--after <cursor>", "Only events after this cursor", "0")
|
|
7216
|
+
.option("--agent <id>", "Agent id reading the feed (defaults to the joined session agent)")
|
|
7217
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
7218
|
+
.option("--json", "Emit machine-readable output")
|
|
7219
|
+
.action(async (sessionId, options, command) => {
|
|
7220
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
7221
|
+
const after = Number.parseInt(String(options.after ?? "0"), 10) || 0;
|
|
7222
|
+
const result = await ticketEvents(normalizeString(sessionId), { after, targetPath });
|
|
7223
|
+
const lines = (result.events || []).map((e) => `#${e.cursor} ${e.kind} ${e.ticketId} v${e.ticketVersion} by ${e.actorId}`);
|
|
7224
|
+
ticketOutput(options, command, { command: "session ticket events", ...result }, lines.join("\n") || "No new ticket events.");
|
|
7225
|
+
});
|
|
7226
|
+
|
|
6495
7227
|
const checkpoint = session
|
|
6496
7228
|
.command("checkpoint")
|
|
6497
7229
|
.description("List, show, create, and generate durable session checkpoints");
|