sentinelayer-cli 0.30.13 → 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.
@@ -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({
@@ -500,6 +609,266 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
500
609
  server: "sentinelayer-session-mcp",
501
610
  },
502
611
  },
612
+ {
613
+ name: "session_action",
614
+ title: "Record Senti Session Action",
615
+ description:
616
+ "Record a low-noise message action such as ack, working_on, disregard, view, like, dislike, or reply against a target session event.",
617
+ input_schema: {
618
+ type: "object",
619
+ additionalProperties: false,
620
+ required: ["sessionId", "agentId", "actionType"],
621
+ properties: {
622
+ sessionId: { type: "string" },
623
+ agentId: { type: "string" },
624
+ actionType: {
625
+ type: "string",
626
+ enum: ["ack", "working_on", "reply", "like", "dislike", "disregard", "view"],
627
+ },
628
+ targetSequenceId: { type: "integer", minimum: 1 },
629
+ targetCursor: { type: "string" },
630
+ targetActionId: { type: "string" },
631
+ note: { type: "string" },
632
+ idempotencyKey: { type: "string" },
633
+ timeoutMs: { type: "integer", minimum: 1 },
634
+ dryRun: { type: "boolean" },
635
+ },
636
+ },
637
+ transport: {
638
+ type: "internal",
639
+ method: "POST",
640
+ url: "sentinelayer://session/session_action",
641
+ timeout_ms: 15000,
642
+ auth: { mode: "none" },
643
+ },
644
+ budgets: {
645
+ max_calls_per_run: 40,
646
+ max_runtime_ms: 15000,
647
+ },
648
+ security: {
649
+ requires_human_approval: false,
650
+ kill_switch: "enabled",
651
+ scopes: ["session:write", "session:action"],
652
+ },
653
+ metadata: {
654
+ provider: "sentinelayer",
655
+ adapter: "sentinelayer-cli",
656
+ server: "sentinelayer-session-mcp",
657
+ },
658
+ },
659
+ {
660
+ name: "session_react",
661
+ title: "React To Senti Message",
662
+ description:
663
+ "Acknowledge or react to a target session event with ack, like, or dislike.",
664
+ input_schema: {
665
+ type: "object",
666
+ additionalProperties: false,
667
+ required: ["sessionId", "agentId", "reaction"],
668
+ properties: {
669
+ sessionId: { type: "string" },
670
+ agentId: { type: "string" },
671
+ reaction: { type: "string", enum: ["ack", "like", "dislike"] },
672
+ targetSequenceId: { type: "integer", minimum: 1 },
673
+ targetCursor: { type: "string" },
674
+ targetActionId: { type: "string" },
675
+ idempotencyKey: { type: "string" },
676
+ timeoutMs: { type: "integer", minimum: 1 },
677
+ dryRun: { type: "boolean" },
678
+ },
679
+ },
680
+ transport: {
681
+ type: "internal",
682
+ method: "POST",
683
+ url: "sentinelayer://session/session_react",
684
+ timeout_ms: 15000,
685
+ auth: { mode: "none" },
686
+ },
687
+ budgets: {
688
+ max_calls_per_run: 60,
689
+ max_runtime_ms: 15000,
690
+ },
691
+ security: {
692
+ requires_human_approval: false,
693
+ kill_switch: "enabled",
694
+ scopes: ["session:write", "session:action"],
695
+ },
696
+ metadata: {
697
+ provider: "sentinelayer",
698
+ adapter: "sentinelayer-cli",
699
+ server: "sentinelayer-session-mcp",
700
+ },
701
+ },
702
+ {
703
+ name: "session_reply",
704
+ title: "Reply In Senti Thread",
705
+ description:
706
+ "Add a threaded reply/comment under a specific session event using the session action channel.",
707
+ input_schema: {
708
+ type: "object",
709
+ additionalProperties: false,
710
+ required: ["sessionId", "agentId", "targetSequenceId", "message"],
711
+ properties: {
712
+ sessionId: { type: "string" },
713
+ agentId: { type: "string" },
714
+ targetSequenceId: { type: "integer", minimum: 1 },
715
+ targetCursor: { type: "string" },
716
+ targetActionId: { type: "string" },
717
+ message: { type: "string" },
718
+ idempotencyKey: { type: "string" },
719
+ timeoutMs: { type: "integer", minimum: 1 },
720
+ dryRun: { type: "boolean" },
721
+ },
722
+ },
723
+ transport: {
724
+ type: "internal",
725
+ method: "POST",
726
+ url: "sentinelayer://session/session_reply",
727
+ timeout_ms: 15000,
728
+ auth: { mode: "none" },
729
+ },
730
+ budgets: {
731
+ max_calls_per_run: 40,
732
+ max_runtime_ms: 15000,
733
+ },
734
+ security: {
735
+ requires_human_approval: false,
736
+ kill_switch: "enabled",
737
+ scopes: ["session:write", "session:action"],
738
+ },
739
+ metadata: {
740
+ provider: "sentinelayer",
741
+ adapter: "sentinelayer-cli",
742
+ server: "sentinelayer-session-mcp",
743
+ },
744
+ },
745
+ {
746
+ name: "session_lock",
747
+ title: "Lock Senti Files",
748
+ description:
749
+ "Claim session-scoped file locks before editing files, using the same fail-closed lock registry as the CLI.",
750
+ input_schema: {
751
+ type: "object",
752
+ additionalProperties: false,
753
+ required: ["sessionId", "agentId", "files"],
754
+ properties: {
755
+ sessionId: { type: "string" },
756
+ agentId: { type: "string" },
757
+ files: {
758
+ anyOf: [
759
+ { type: "string" },
760
+ { type: "array", items: { type: "string" }, minItems: 1 },
761
+ ],
762
+ },
763
+ intent: { type: "string" },
764
+ ttlSeconds: { type: "integer", minimum: 1 },
765
+ syncRemote: { type: "boolean" },
766
+ awaitRemoteSync: { type: "boolean" },
767
+ },
768
+ },
769
+ transport: {
770
+ type: "internal",
771
+ method: "POST",
772
+ url: "sentinelayer://session/session_lock",
773
+ timeout_ms: 15000,
774
+ auth: { mode: "none" },
775
+ },
776
+ budgets: {
777
+ max_calls_per_run: 20,
778
+ max_runtime_ms: 15000,
779
+ },
780
+ security: {
781
+ requires_human_approval: false,
782
+ kill_switch: "enabled",
783
+ scopes: ["session:lock"],
784
+ },
785
+ metadata: {
786
+ provider: "sentinelayer",
787
+ adapter: "sentinelayer-cli",
788
+ server: "sentinelayer-session-mcp",
789
+ },
790
+ },
791
+ {
792
+ name: "session_unlock",
793
+ title: "Unlock Senti Files",
794
+ description:
795
+ "Release session-scoped file locks held by an agent.",
796
+ input_schema: {
797
+ type: "object",
798
+ additionalProperties: false,
799
+ required: ["sessionId", "agentId", "files"],
800
+ properties: {
801
+ sessionId: { type: "string" },
802
+ agentId: { type: "string" },
803
+ files: {
804
+ anyOf: [
805
+ { type: "string" },
806
+ { type: "array", items: { type: "string" }, minItems: 1 },
807
+ ],
808
+ },
809
+ reason: { type: "string" },
810
+ force: { type: "boolean" },
811
+ syncRemote: { type: "boolean" },
812
+ awaitRemoteSync: { type: "boolean" },
813
+ },
814
+ },
815
+ transport: {
816
+ type: "internal",
817
+ method: "POST",
818
+ url: "sentinelayer://session/session_unlock",
819
+ timeout_ms: 15000,
820
+ auth: { mode: "none" },
821
+ },
822
+ budgets: {
823
+ max_calls_per_run: 20,
824
+ max_runtime_ms: 15000,
825
+ },
826
+ security: {
827
+ requires_human_approval: false,
828
+ kill_switch: "enabled",
829
+ scopes: ["session:lock"],
830
+ },
831
+ metadata: {
832
+ provider: "sentinelayer",
833
+ adapter: "sentinelayer-cli",
834
+ server: "sentinelayer-session-mcp",
835
+ },
836
+ },
837
+ {
838
+ name: "session_locks",
839
+ title: "List Senti File Locks",
840
+ description:
841
+ "List active file locks for a session.",
842
+ input_schema: {
843
+ type: "object",
844
+ additionalProperties: false,
845
+ required: ["sessionId"],
846
+ properties: {
847
+ sessionId: { type: "string" },
848
+ },
849
+ },
850
+ transport: {
851
+ type: "internal",
852
+ method: "POST",
853
+ url: "sentinelayer://session/session_locks",
854
+ timeout_ms: 15000,
855
+ auth: { mode: "none" },
856
+ },
857
+ budgets: {
858
+ max_calls_per_run: 20,
859
+ max_runtime_ms: 15000,
860
+ },
861
+ security: {
862
+ requires_human_approval: false,
863
+ kill_switch: "enabled",
864
+ scopes: ["session:lock"],
865
+ },
866
+ metadata: {
867
+ provider: "sentinelayer",
868
+ adapter: "sentinelayer-cli",
869
+ server: "sentinelayer-session-mcp",
870
+ },
871
+ },
503
872
  {
504
873
  name: "attention_request",
505
874
  title: "Request Senti Attention",
@@ -552,6 +921,152 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
552
921
  };
553
922
  }
554
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
+
555
1070
  /**
556
1071
  * Create an AIdenID adapter contract template that binds MCP tools to provisioning endpoints.
557
1072
  *
@@ -715,6 +1230,68 @@ export function validateAidenIdAdapterContract(payload, { registryPayload } = {}
715
1230
  return parsed;
716
1231
  }
717
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
+
718
1295
  /**
719
1296
  * Validate and normalize MCP server runtime config payload.
720
1297
  *
@@ -809,6 +1386,21 @@ export async function resolveDefaultAidenIdAdapterContractPath({ cwd, outputDir,
809
1386
  return path.join(outputRoot, "mcp", "aidenid-provisioning-adapter.json");
810
1387
  }
811
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
+
812
1404
  /**
813
1405
  * Resolve default MCP server config output path for a specific server id.
814
1406
  *