sentinelayer-cli 0.30.14 → 0.31.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.30.14",
3
+ "version": "0.31.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -8,6 +8,7 @@ import fs from "node:fs/promises";
8
8
 
9
9
  import {
10
10
  buildAidenIdProvisioningAdapterTemplate,
11
+ buildHostedSentiSessionConnectorContract,
11
12
  buildAidenIdRegistryTemplate,
12
13
  buildMcpToolRegistrySchema,
13
14
  buildMcpServerConfigTemplate,
@@ -15,10 +16,12 @@ import {
15
16
  buildVsCodeMcpBridgeTemplate,
16
17
  readJsonFile,
17
18
  resolveDefaultAidenIdAdapterContractPath,
19
+ resolveDefaultHostedSentiSessionConnectorContractPath,
18
20
  resolveDefaultMcpOutputPath,
19
21
  resolveDefaultMcpServerConfigPath,
20
22
  resolveDefaultVsCodeBridgePath,
21
23
  validateAidenIdAdapterContract,
24
+ validateHostedSentiSessionConnectorContract,
22
25
  stringifyJson,
23
26
  validateMcpServerConfig,
24
27
  validateMcpToolRegistry,
@@ -255,6 +258,46 @@ export function registerMcpCommand(program) {
255
258
  console.log(pc.gray("Next: run 'sl mcp server init --id sentinelayer-session --registry-file <path>'."));
256
259
  });
257
260
 
261
+ registry
262
+ .command("init-hosted-session-connector")
263
+ .description("Write the hosted Senti session MCP connector contract")
264
+ .option(
265
+ "--registry-file <path>",
266
+ "Local session registry file referenced by hosted connector contract",
267
+ ".sentinelayer/mcp/tool-registry.session-tools.json"
268
+ )
269
+ .option("--path <path>", "Destination file path override")
270
+ .option("--output-dir <path>", "Optional artifact output root override")
271
+ .option("--force", "Overwrite destination file if it already exists")
272
+ .option("--json", "Emit machine-readable output")
273
+ .action(async (options, command) => {
274
+ const defaultPath = await resolveDefaultHostedSentiSessionConnectorContractPath({
275
+ cwd: process.cwd(),
276
+ outputDir: options.outputDir,
277
+ env: process.env,
278
+ });
279
+ const outputPath = normalizeOutputPath(options.path, defaultPath);
280
+ const template = buildHostedSentiSessionConnectorContract({
281
+ localRegistryFile: options.registryFile,
282
+ });
283
+ validateHostedSentiSessionConnectorContract(template);
284
+ const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force) });
285
+ const payload = {
286
+ command: "mcp registry init-hosted-session-connector",
287
+ outputPath: writtenPath,
288
+ localRegistryFile: template.local_registry_file,
289
+ toolCount: template.tools.length,
290
+ tools: template.tools.map((tool) => tool.tool_name),
291
+ };
292
+
293
+ if (shouldEmitJson(options, command)) {
294
+ console.log(JSON.stringify(payload, null, 2));
295
+ return;
296
+ }
297
+ console.log(pc.green(`Wrote hosted Senti session connector contract: ${writtenPath}`));
298
+ console.log(pc.gray("This is a hosted connector contract, not a local stdio runtime claim."));
299
+ });
300
+
258
301
  registry
259
302
  .command("init-cli")
260
303
  .description("Write a generated MCP registry for every SentinelLayer CLI leaf command")
@@ -335,6 +378,66 @@ export function registerMcpCommand(program) {
335
378
  }
336
379
  });
337
380
 
381
+ registry
382
+ .command("validate-hosted-session-connector")
383
+ .description("Validate a hosted Senti session MCP connector contract")
384
+ .requiredOption("--file <path>", "Hosted connector contract JSON file to validate")
385
+ .option("--registry-file <path>", "Optional local session registry JSON file for cross-check")
386
+ .option("--json", "Emit machine-readable output")
387
+ .action(async (options, command) => {
388
+ const inputPath = path.resolve(process.cwd(), String(options.file || "").trim());
389
+ const loaded = await readJsonFile(inputPath);
390
+ const registryFile = String(options.registryFile || "").trim();
391
+ const loadedRegistry = registryFile
392
+ ? await readJsonFile(path.resolve(process.cwd(), registryFile))
393
+ : null;
394
+
395
+ let parsed;
396
+ try {
397
+ parsed = validateHostedSentiSessionConnectorContract(loaded.data, {
398
+ registryPayload: loadedRegistry ? loadedRegistry.data : undefined,
399
+ });
400
+ } catch (error) {
401
+ const payload = {
402
+ command: "mcp registry validate-hosted-session-connector",
403
+ valid: false,
404
+ filePath: loaded.path,
405
+ registryFilePath: loadedRegistry ? loadedRegistry.path : null,
406
+ error: zodIssueSummary(error),
407
+ };
408
+ if (shouldEmitJson(options, command)) {
409
+ console.log(JSON.stringify(payload, null, 2));
410
+ } else {
411
+ console.log(pc.red(`Hosted Senti session connector contract invalid: ${payload.error}`));
412
+ console.log(pc.gray(`File: ${loaded.path}`));
413
+ if (payload.registryFilePath) {
414
+ console.log(pc.gray(`Registry: ${payload.registryFilePath}`));
415
+ }
416
+ }
417
+ process.exitCode = 2;
418
+ return;
419
+ }
420
+
421
+ const payload = {
422
+ command: "mcp registry validate-hosted-session-connector",
423
+ valid: true,
424
+ filePath: loaded.path,
425
+ registryFilePath: loadedRegistry ? loadedRegistry.path : null,
426
+ connector: parsed.connector,
427
+ toolCount: parsed.tools.length,
428
+ releaseGateCount: parsed.release_gates.length,
429
+ };
430
+ if (shouldEmitJson(options, command)) {
431
+ console.log(JSON.stringify(payload, null, 2));
432
+ return;
433
+ }
434
+ console.log(pc.green(`Hosted Senti session connector contract valid (${parsed.tools.length} tools)`));
435
+ console.log(pc.gray(`File: ${loaded.path}`));
436
+ if (payload.registryFilePath) {
437
+ console.log(pc.gray(`Registry: ${payload.registryFilePath}`));
438
+ }
439
+ });
440
+
338
441
  registry
339
442
  .command("validate-aidenid-adapter")
340
443
  .description(
@@ -12,6 +12,8 @@ export const MCP_TOOL_REGISTRY_SCHEMA_VERSION = "1.0.0";
12
12
  export const MCP_SERVER_CONFIG_SCHEMA_VERSION = "1.0.0";
13
13
  /** AIdenID adapter-contract schema version used by Sentinelayer CLI generated artifacts. */
14
14
  export const AIDENID_ADAPTER_CONTRACT_SCHEMA_VERSION = "1.0.0";
15
+ /** Hosted Senti session connector-contract schema version. */
16
+ export const HOSTED_SENTI_SESSION_CONNECTOR_CONTRACT_SCHEMA_VERSION = "1.0.0";
15
17
 
16
18
  const serverIdRegex = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/;
17
19
  const inputPlaceholderRegex = /^\{\{input\.[a-zA-Z0-9_.-]+\}\}$/;
@@ -142,6 +144,113 @@ const aidenIdAdapterContractSchema = z
142
144
  })
143
145
  .strict();
144
146
 
147
+ const hostedSessionConnectorToolSchema = z
148
+ .object({
149
+ tool_name: z
150
+ .string()
151
+ .min(1)
152
+ .regex(/^[a-zA-Z0-9_.:-]+$/, "tool_name must contain only [a-zA-Z0-9_.:-]"),
153
+ local_tool_name: z
154
+ .string()
155
+ .min(1)
156
+ .regex(/^[a-zA-Z0-9_.:-]+$/, "local_tool_name must contain only [a-zA-Z0-9_.:-]"),
157
+ operation: z.enum([
158
+ "join_and_hydrate",
159
+ "read_history",
160
+ "send_message",
161
+ "session_action",
162
+ "session_lock",
163
+ "session_unlock",
164
+ "list_locks",
165
+ "attention_request",
166
+ "subscribe_wake",
167
+ "presence_renew",
168
+ "runner_teardown",
169
+ ]),
170
+ input_policy: z
171
+ .object({
172
+ identity_source: z.literal("server_session_seat"),
173
+ rejects_identity_from_tool_args: z.literal(true),
174
+ allowed_user_args: z.array(z.string().min(1)).default([]),
175
+ forbidden_capability_args: z
176
+ .array(z.enum(["sessionId", "session_id", "agentId", "agent_id", "userId", "user_id", "orgId", "org_id"]))
177
+ .default(["sessionId", "session_id", "agentId", "agent_id", "userId", "user_id", "orgId", "org_id"]),
178
+ })
179
+ .strict(),
180
+ authorization: z
181
+ .object({
182
+ requires_validated_claims: z.literal(true),
183
+ requires_session_seat: z.literal(true),
184
+ required_scopes: z.array(z.string().min(1)).min(1),
185
+ })
186
+ .strict(),
187
+ wake_payload: z
188
+ .object({
189
+ policy: z.enum(["none", "cursor_reason_counts_only", "bounded_redacted_window"]),
190
+ may_include_message_content: z.boolean().default(false),
191
+ max_events: z.number().int().positive().max(200).optional(),
192
+ })
193
+ .strict()
194
+ .superRefine((payload, ctx) => {
195
+ if (payload.policy === "cursor_reason_counts_only" && payload.may_include_message_content) {
196
+ ctx.addIssue({
197
+ code: z.ZodIssueCode.custom,
198
+ path: ["may_include_message_content"],
199
+ message: "cursor/reason wake payloads must not include message content.",
200
+ });
201
+ }
202
+ }),
203
+ audit: z
204
+ .object({
205
+ durable_receipt_required: z.literal(true),
206
+ idempotency_key_fields: z.array(z.string().min(1)).min(1),
207
+ })
208
+ .strict(),
209
+ runner_lifecycle: z
210
+ .object({
211
+ requires_scoped_runner_token: z.literal(true),
212
+ revoke_token_on_idle_teardown: z.literal(true),
213
+ })
214
+ .strict()
215
+ .optional(),
216
+ })
217
+ .strict();
218
+
219
+ const hostedSessionConnectorContractSchema = z
220
+ .object({
221
+ version: z
222
+ .literal(HOSTED_SENTI_SESSION_CONNECTOR_CONTRACT_SCHEMA_VERSION)
223
+ .default(HOSTED_SENTI_SESSION_CONNECTOR_CONTRACT_SCHEMA_VERSION),
224
+ provider: z.literal("sentinelayer"),
225
+ connector: z.literal("hosted-senti-session"),
226
+ generated_at: z.string().min(1),
227
+ local_registry_file: z.string().min(1),
228
+ runtime_status: z.literal("contract_only"),
229
+ boundary: z
230
+ .object({
231
+ transport: z.literal("https_mcp_streamable_http"),
232
+ identity_source: z.literal("validated_oauth_claims_and_server_session_seat"),
233
+ local_stdio_registry_is_not_hosted_capability: z.literal(true),
234
+ long_lived_cli_token_passthrough_allowed: z.literal(false),
235
+ })
236
+ .strict(),
237
+ tools: z.array(hostedSessionConnectorToolSchema).min(1),
238
+ release_gates: z.array(z.string().min(1)).min(1),
239
+ })
240
+ .strict();
241
+
242
+ const REQUIRED_HOSTED_SESSION_CONNECTOR_RELEASE_GATES = [
243
+ "oauth_resource_binding",
244
+ "session_seat_ownership",
245
+ "deny_identity_from_tool_args",
246
+ "approval_policy_enforcement",
247
+ "scoped_runner_token_revocation",
248
+ "wake_payload_minimization",
249
+ "durable_audit_receipts",
250
+ "output_redaction",
251
+ "omar_gate_mcp_security_pack",
252
+ ];
253
+
145
254
  const mcpServerTransportSchema = z.discriminatedUnion("mode", [
146
255
  z
147
256
  .object({
@@ -812,6 +921,152 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
812
921
  };
813
922
  }
814
923
 
924
+ function hostedSessionTool({
925
+ toolName,
926
+ operation,
927
+ allowedUserArgs,
928
+ requiredScopes,
929
+ wakePayload = { policy: "none", may_include_message_content: false },
930
+ runnerLifecycle = null,
931
+ }) {
932
+ const tool = {
933
+ tool_name: toolName,
934
+ local_tool_name: toolName,
935
+ operation,
936
+ input_policy: {
937
+ identity_source: "server_session_seat",
938
+ rejects_identity_from_tool_args: true,
939
+ allowed_user_args: allowedUserArgs,
940
+ forbidden_capability_args: ["sessionId", "session_id", "agentId", "agent_id", "userId", "user_id", "orgId", "org_id"],
941
+ },
942
+ authorization: {
943
+ requires_validated_claims: true,
944
+ requires_session_seat: true,
945
+ required_scopes: requiredScopes,
946
+ },
947
+ wake_payload: wakePayload,
948
+ audit: {
949
+ durable_receipt_required: true,
950
+ idempotency_key_fields: ["subject", "session_seat", "tool_name", "normalized_inputs"],
951
+ },
952
+ };
953
+ if (runnerLifecycle) {
954
+ tool.runner_lifecycle = runnerLifecycle;
955
+ }
956
+ return tool;
957
+ }
958
+
959
+ /**
960
+ * Create the hosted Senti session connector contract.
961
+ *
962
+ * This is intentionally separate from the local stdio session registry. Local
963
+ * tools need sessionId/agentId arguments; hosted tools must derive identity from
964
+ * validated OAuth claims plus a server-side session seat and must not treat those
965
+ * arguments as capabilities.
966
+ *
967
+ * @param {{ generatedAt?: string, localRegistryFile?: string }} [options]
968
+ * @returns {Record<string, any>}
969
+ */
970
+ export function buildHostedSentiSessionConnectorContract({
971
+ generatedAt = new Date().toISOString(),
972
+ localRegistryFile = ".sentinelayer/mcp/tool-registry.session-tools.json",
973
+ } = {}) {
974
+ return {
975
+ version: HOSTED_SENTI_SESSION_CONNECTOR_CONTRACT_SCHEMA_VERSION,
976
+ provider: "sentinelayer",
977
+ connector: "hosted-senti-session",
978
+ generated_at: generatedAt,
979
+ local_registry_file: String(localRegistryFile || "").trim() || ".sentinelayer/mcp/tool-registry.session-tools.json",
980
+ runtime_status: "contract_only",
981
+ boundary: {
982
+ transport: "https_mcp_streamable_http",
983
+ identity_source: "validated_oauth_claims_and_server_session_seat",
984
+ local_stdio_registry_is_not_hosted_capability: true,
985
+ long_lived_cli_token_passthrough_allowed: false,
986
+ },
987
+ tools: [
988
+ hostedSessionTool({
989
+ toolName: "poll_inbox",
990
+ operation: "read_history",
991
+ allowedUserArgs: ["cursor", "limit", "actionLimit", "includeActions"],
992
+ requiredScopes: ["session:read"],
993
+ wakePayload: { policy: "bounded_redacted_window", may_include_message_content: true, max_events: 50 },
994
+ }),
995
+ hostedSessionTool({
996
+ toolName: "send_message",
997
+ operation: "send_message",
998
+ allowedUserArgs: ["message", "to", "idempotencyKey"],
999
+ requiredScopes: ["session:write"],
1000
+ }),
1001
+ hostedSessionTool({
1002
+ toolName: "session_action",
1003
+ operation: "session_action",
1004
+ allowedUserArgs: ["actionType", "targetSequenceId", "targetCursor", "targetActionId", "note", "idempotencyKey"],
1005
+ requiredScopes: ["session:write", "session:action"],
1006
+ }),
1007
+ hostedSessionTool({
1008
+ toolName: "session_react",
1009
+ operation: "session_action",
1010
+ allowedUserArgs: ["reaction", "targetSequenceId", "targetCursor", "targetActionId", "idempotencyKey"],
1011
+ requiredScopes: ["session:write", "session:action"],
1012
+ }),
1013
+ hostedSessionTool({
1014
+ toolName: "session_reply",
1015
+ operation: "session_action",
1016
+ allowedUserArgs: ["targetSequenceId", "targetCursor", "targetActionId", "message", "idempotencyKey"],
1017
+ requiredScopes: ["session:write", "session:action"],
1018
+ }),
1019
+ hostedSessionTool({
1020
+ toolName: "session_lock",
1021
+ operation: "session_lock",
1022
+ allowedUserArgs: ["files", "intent", "ttlSeconds"],
1023
+ requiredScopes: ["session:lock"],
1024
+ }),
1025
+ hostedSessionTool({
1026
+ toolName: "session_unlock",
1027
+ operation: "session_unlock",
1028
+ allowedUserArgs: ["files", "reason"],
1029
+ requiredScopes: ["session:lock"],
1030
+ }),
1031
+ hostedSessionTool({
1032
+ toolName: "session_locks",
1033
+ operation: "list_locks",
1034
+ allowedUserArgs: [],
1035
+ requiredScopes: ["session:lock"],
1036
+ }),
1037
+ hostedSessionTool({
1038
+ toolName: "attention_request",
1039
+ operation: "attention_request",
1040
+ allowedUserArgs: ["message", "to", "priority", "severity", "idempotencyKey"],
1041
+ requiredScopes: ["session:write"],
1042
+ wakePayload: { policy: "cursor_reason_counts_only", may_include_message_content: false },
1043
+ }),
1044
+ hostedSessionTool({
1045
+ toolName: "subscribe_wake",
1046
+ operation: "subscribe_wake",
1047
+ allowedUserArgs: ["cadencePreset", "wakeTriggers", "sinceCursor"],
1048
+ requiredScopes: ["session:read", "session:wake"],
1049
+ wakePayload: { policy: "cursor_reason_counts_only", may_include_message_content: false },
1050
+ runnerLifecycle: {
1051
+ requires_scoped_runner_token: true,
1052
+ revoke_token_on_idle_teardown: true,
1053
+ },
1054
+ }),
1055
+ ],
1056
+ release_gates: [
1057
+ "oauth_resource_binding",
1058
+ "session_seat_ownership",
1059
+ "deny_identity_from_tool_args",
1060
+ "approval_policy_enforcement",
1061
+ "scoped_runner_token_revocation",
1062
+ "wake_payload_minimization",
1063
+ "durable_audit_receipts",
1064
+ "output_redaction",
1065
+ "omar_gate_mcp_security_pack",
1066
+ ],
1067
+ };
1068
+ }
1069
+
815
1070
  /**
816
1071
  * Create an AIdenID adapter contract template that binds MCP tools to provisioning endpoints.
817
1072
  *
@@ -975,6 +1230,68 @@ export function validateAidenIdAdapterContract(payload, { registryPayload } = {}
975
1230
  return parsed;
976
1231
  }
977
1232
 
1233
+ /**
1234
+ * Validate and normalize hosted Senti session connector contract payload.
1235
+ * Optionally cross-validates hosted local_tool_name references against a local
1236
+ * session registry payload. Contract-only hosted tools, such as subscribe_wake,
1237
+ * can omit a local registry binding by using the same name as a non-local tool.
1238
+ *
1239
+ * @param {unknown} payload
1240
+ * @param {{ registryPayload?: unknown }} [options]
1241
+ * @returns {any}
1242
+ */
1243
+ export function validateHostedSentiSessionConnectorContract(payload, { registryPayload } = {}) {
1244
+ const parsed = hostedSessionConnectorContractSchema.parse(payload);
1245
+ const releaseGateSet = new Set(parsed.release_gates);
1246
+
1247
+ for (const gate of REQUIRED_HOSTED_SESSION_CONNECTOR_RELEASE_GATES) {
1248
+ if (!releaseGateSet.has(gate)) {
1249
+ throw new Error(`Hosted connector contract is missing required release gate ${gate}.`);
1250
+ }
1251
+ }
1252
+
1253
+ for (const tool of parsed.tools) {
1254
+ const forbidden = new Set(tool.input_policy.forbidden_capability_args);
1255
+ for (const key of ["sessionId", "session_id", "agentId", "agent_id"]) {
1256
+ if (!forbidden.has(key)) {
1257
+ throw new Error(`Hosted connector tool ${tool.tool_name} must forbid capability argument ${key}.`);
1258
+ }
1259
+ }
1260
+ for (const arg of tool.input_policy.allowed_user_args) {
1261
+ if (forbidden.has(arg)) {
1262
+ throw new Error(`Hosted connector tool ${tool.tool_name} allows forbidden capability argument ${arg}.`);
1263
+ }
1264
+ }
1265
+ if (tool.operation === "subscribe_wake") {
1266
+ if (!tool.runner_lifecycle) {
1267
+ throw new Error(`Hosted connector tool ${tool.tool_name} must define scoped runner-token lifecycle controls.`);
1268
+ }
1269
+ if (!tool.runner_lifecycle.requires_scoped_runner_token) {
1270
+ throw new Error(`Hosted connector tool ${tool.tool_name} must require a scoped runner token.`);
1271
+ }
1272
+ if (!tool.runner_lifecycle.revoke_token_on_idle_teardown) {
1273
+ throw new Error(`Hosted connector tool ${tool.tool_name} must revoke runner tokens on idle teardown.`);
1274
+ }
1275
+ }
1276
+ }
1277
+
1278
+ if (registryPayload !== undefined) {
1279
+ const registry = validateMcpToolRegistry(registryPayload);
1280
+ const localToolNames = new Set(registry.tools.map((tool) => tool.name));
1281
+ const missingLocalTools = parsed.tools
1282
+ .filter((tool) => tool.operation !== "subscribe_wake" && tool.operation !== "presence_renew" && tool.operation !== "runner_teardown")
1283
+ .map((tool) => tool.local_tool_name)
1284
+ .filter((toolName) => !localToolNames.has(toolName));
1285
+ if (missingLocalTools.length > 0) {
1286
+ throw new Error(
1287
+ `Hosted connector references local tools not present in registry: ${[...new Set(missingLocalTools)].join(", ")}`
1288
+ );
1289
+ }
1290
+ }
1291
+
1292
+ return parsed;
1293
+ }
1294
+
978
1295
  /**
979
1296
  * Validate and normalize MCP server runtime config payload.
980
1297
  *
@@ -1069,6 +1386,21 @@ export async function resolveDefaultAidenIdAdapterContractPath({ cwd, outputDir,
1069
1386
  return path.join(outputRoot, "mcp", "aidenid-provisioning-adapter.json");
1070
1387
  }
1071
1388
 
1389
+ /**
1390
+ * Resolve default output path for hosted Senti session connector contracts.
1391
+ *
1392
+ * @param {{ cwd?: string, outputDir?: string, env?: NodeJS.ProcessEnv }} [options]
1393
+ * @returns {Promise<string>}
1394
+ */
1395
+ export async function resolveDefaultHostedSentiSessionConnectorContractPath({ cwd, outputDir, env } = {}) {
1396
+ const outputRoot = await resolveOutputRoot({
1397
+ cwd,
1398
+ outputDirOverride: outputDir,
1399
+ env,
1400
+ });
1401
+ return path.join(outputRoot, "mcp", "hosted-senti-session-connector.json");
1402
+ }
1403
+
1072
1404
  /**
1073
1405
  * Resolve default MCP server config output path for a specific server id.
1074
1406
  *
@@ -72,6 +72,12 @@ MCP / agent-security (clearance-layer specific):
72
72
  - Agent sandbox / microVM handed a long-lived or user credential instead of a short-lived, narrowly-scoped, per-session token; secrets injected via env or baked into a VM image/snapshot
73
73
  - Sandbox egress not default-denied, or the cloud metadata endpoint (169.254.169.254 / IMDS) reachable from guest code
74
74
  - VM memory snapshot reused across sessions or tenants (entropy/RNG/credential bleed)
75
+ - Hosted/bridge tool execution without SERVER-SIDE approval enforcement: requires_human_approval/scopes/kill_switch trusted from client-supplied metadata or skipped, or destructive tools (auth/config/session-kill/file-write/network-scan) runnable with no approval gate (must deny-by-default when policy metadata is missing or malformed)
76
+ - Token-like values UNREDACTED in tool output: stdout/stderr/parsed JSON/command echo/error messages returning bearer tokens, API keys, private keys, OTPs, session cookies, or local auth-store paths to the client or into stored artifacts
77
+ - Blind unpinned @latest CLI/dependency install inside a hosted runner, not resolved to an immutable version with checksum/signature/attestation verification (supply-chain RCE into every spawned VM)
78
+ - Notification/subscribe authorization derived from a sessionId (or other tool arg) instead of the caller's validated claims + server-side session membership (a sessionId is not a capability)
79
+ - Wake/notification payloads exposing message content or PII outside the subscriber's authorized scope (prefer counts/cursors/reasons; redact embedded content per scope)
80
+ - Idle teardown that kills the runner without revoking its scoped token, or session artifacts written by the guest/runner instead of by the control-plane under the owner's account
75
81
 
76
82
  Evidence standard: Every finding MUST include file:line, exploit scenario, and remediation.
77
83
  Do NOT report hypothetical issues without concrete code evidence.`,