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 +1 -1
- package/src/commands/mcp.js +103 -0
- package/src/mcp/registry.js +332 -0
- package/src/review/persona-prompts.js +6 -0
package/package.json
CHANGED
package/src/commands/mcp.js
CHANGED
|
@@ -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(
|
package/src/mcp/registry.js
CHANGED
|
@@ -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.`,
|