@bradheitmann/odin-sentinel 0.4.4 → 0.4.6
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/AGENTS.md +64 -0
- package/CLAUDE.md +43 -0
- package/README.md +102 -335
- package/dist/src/mcp/server.js +43 -12
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/protocol/schemas.d.ts +2529 -4
- package/dist/src/protocol/schemas.js +214 -18
- package/dist/src/protocol/schemas.js.map +1 -1
- package/dist/src/protocol/service.d.ts +96 -2
- package/dist/src/protocol/service.js +516 -4
- package/dist/src/protocol/service.js.map +1 -1
- package/dist/src/protocol/surface-layout.d.ts +40 -1
- package/dist/src/protocol/surface-layout.js +98 -1
- package/dist/src/protocol/surface-layout.js.map +1 -1
- package/dist/src/protocol/validators.d.ts +3 -0
- package/dist/src/protocol/validators.js +28 -0
- package/dist/src/protocol/validators.js.map +1 -1
- package/dist/src/protocol/version.d.ts +3 -0
- package/dist/src/protocol/version.js +3 -0
- package/dist/src/protocol/version.js.map +1 -1
- package/dist/src/telemetry/config.d.ts +8 -0
- package/dist/src/telemetry/config.js +24 -0
- package/dist/src/telemetry/config.js.map +1 -1
- package/dist/src/telemetry/index.d.ts +5 -5
- package/dist/src/telemetry/index.js +3 -3
- package/dist/src/telemetry/index.js.map +1 -1
- package/dist/src/telemetry/redactor.js +25 -7
- package/dist/src/telemetry/redactor.js.map +1 -1
- package/dist/src/telemetry/report.d.ts +108 -0
- package/dist/src/telemetry/report.js +83 -3
- package/dist/src/telemetry/report.js.map +1 -1
- package/dist/src/telemetry/submit.d.ts +2 -0
- package/dist/src/telemetry/submit.js +79 -6
- package/dist/src/telemetry/submit.js.map +1 -1
- package/docs/guides/quick-start.md +112 -44
- package/docs/guides/quickstart-prompts.md +65 -0
- package/docs/guides/recommended-starter-team.md +45 -27
- package/docs/reference/client-compatibility.md +20 -43
- package/docs/reference/cost-and-privacy.md +26 -23
- package/docs/reference/distribution.md +40 -55
- package/docs/reference/public-surface-audit.md +35 -114
- package/package.json +19 -4
- package/protocol/SCP.md +8 -1
- package/protocol/bootstrap-skill.md +16 -11
- package/protocol/closeout.yaml +7 -1
- package/protocol/delegation.yaml +1 -1
- package/protocol/model-profiles.yaml +55 -1
- package/protocol/receipts/boot-receipt.yaml +42 -0
- package/protocol/receipts/team-manifest.yaml +41 -0
- package/protocol/roles.yaml +69 -1
- package/protocol/topology.yaml +78 -36
- package/scripts/audit/public-surface.mjs +48 -19
- package/scripts/audit/verify-pack.mjs +294 -27
- package/templates/dev-slice-template.md +56 -0
- package/templates/pm-role-template.md +61 -0
- package/templates/qa-slice-template.md +46 -0
- package/templates/team-manifest-template.yaml +163 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import YAML from "yaml";
|
|
2
2
|
import { createFileProtocolRepository } from "./repository.js";
|
|
3
|
-
import { asRecord, buildValidationResult, isVisibleRoleSlot, requireRecord, requireStringArray, validateFieldTypes, validateNonEmptyArrays, validateRequiredFields } from "./validators.js";
|
|
4
|
-
import { VERSION } from "./version.js";
|
|
3
|
+
import { asRecord, buildValidationResult, isVisibleRoleSlot, isVersionBelow, redactSecretLikeText, requireRecord, requireStringArray, validateFieldTypes, validateNonEmptyArrays, validateRequiredFields } from "./validators.js";
|
|
4
|
+
import { MINIMUM_COMPATIBLE_MCP_VERSION, PROTOCOL_SCHEMA_VERSION, PUBLIC_LATEST_VERSION, VERSION } from "./version.js";
|
|
5
5
|
const DEFAULT_ROLE_SLOT = "A/EXEC-PM";
|
|
6
6
|
const DEFAULT_HANDOFF_PATHS = [
|
|
7
7
|
"docs/handoffs/",
|
|
@@ -41,6 +41,94 @@ function safeErrorText(value) {
|
|
|
41
41
|
return "<empty>";
|
|
42
42
|
return sanitized.length > 120 ? `${sanitized.slice(0, 117)}...` : sanitized;
|
|
43
43
|
}
|
|
44
|
+
const GOVERNED_LAUNCH_PHASES = [
|
|
45
|
+
"SURFACE_PROVISIONED",
|
|
46
|
+
"OCCUPANT_READINESS",
|
|
47
|
+
"OCCUPANT_LAUNCH",
|
|
48
|
+
"BOOT_RECEIPT_VALIDATION",
|
|
49
|
+
"TEAM_ACTIVATION",
|
|
50
|
+
"ACTIVE_WATCH"
|
|
51
|
+
];
|
|
52
|
+
const ACTIVE_WATCH_TERMINAL_STATES = [
|
|
53
|
+
"RELEASED_BY_OPERATOR",
|
|
54
|
+
"HANDED_OFF",
|
|
55
|
+
"PARKED_IDLE",
|
|
56
|
+
"FAILED",
|
|
57
|
+
"WATCH_UNSUPPORTED"
|
|
58
|
+
];
|
|
59
|
+
const AUTH_STATUSES = [
|
|
60
|
+
"AUTH_READY",
|
|
61
|
+
"AUTH_PRESENT_UNVERIFIED",
|
|
62
|
+
"AUTH_MISSING",
|
|
63
|
+
"AUTH_PROVIDER_BLOCKED",
|
|
64
|
+
"AUTH_LOGIN_REQUIRED",
|
|
65
|
+
"AUTH_UNKNOWN"
|
|
66
|
+
];
|
|
67
|
+
const MODEL_STATUSES = [
|
|
68
|
+
"MODEL_READY",
|
|
69
|
+
"MODEL_SLOW",
|
|
70
|
+
"MODEL_STALLED",
|
|
71
|
+
"MODEL_REASONING_ONLY",
|
|
72
|
+
"STREAMING_PROTOCOL_MISMATCH",
|
|
73
|
+
"MODEL_UNREACHABLE"
|
|
74
|
+
];
|
|
75
|
+
function roleKind(roleSlot) {
|
|
76
|
+
return normalizeRoleName(roleSlot);
|
|
77
|
+
}
|
|
78
|
+
function isOdinRole(role) {
|
|
79
|
+
const normalized = roleKind(role);
|
|
80
|
+
return normalized === "EXEC_ODIN" || normalized === "TEAM_ODIN";
|
|
81
|
+
}
|
|
82
|
+
function isGovernedRole(role) {
|
|
83
|
+
return ["EXEC_PM", "EXEC_ODIN", "EXEC_ASST", "EXEC_RSCH", "EXEC_QA", "TEAM_PM", "TEAM_ODIN", "DEV_WORKER", "QA_WORKER", "SHADOW_REVIEWER"].includes(roleKind(role));
|
|
84
|
+
}
|
|
85
|
+
function scpContextSources(slot) {
|
|
86
|
+
const sources = new Set();
|
|
87
|
+
if (slot.scpSkillAvailable === true || slot.protocolTextSource === "native_skill") {
|
|
88
|
+
sources.add("native sentinel-coordination-protocol skill");
|
|
89
|
+
}
|
|
90
|
+
if (slot.fullProtocolTextInjected === true || slot.protocolTextSource === "injected_full_text") {
|
|
91
|
+
sources.add("full injected SCP protocol text");
|
|
92
|
+
}
|
|
93
|
+
if (slot.protocolTextSource === "mcp_resource") {
|
|
94
|
+
sources.add("odin-sentinel MCP bootstrap resource");
|
|
95
|
+
}
|
|
96
|
+
return Array.from(sources);
|
|
97
|
+
}
|
|
98
|
+
function defaultSafeOutcomes() {
|
|
99
|
+
return [
|
|
100
|
+
"choose a different harness",
|
|
101
|
+
"receive setup guidance without pasting secrets",
|
|
102
|
+
"mark slot VACANT_ROLE_SLOT",
|
|
103
|
+
"request EXEC PM-approved substitution"
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
function stringFieldPresent(value) {
|
|
107
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
108
|
+
}
|
|
109
|
+
function classifyProbeText(harness, text) {
|
|
110
|
+
const safeText = redactSecretLikeText(text).toLowerCase();
|
|
111
|
+
const classes = [];
|
|
112
|
+
if (/permission|allow|approve|deny|press y|waiting for confirmation/.test(safeText))
|
|
113
|
+
classes.push("BLOCKED_BY_PERMISSION");
|
|
114
|
+
if (/login|sign in|authenticate|kilo auth login|\/connect/.test(safeText))
|
|
115
|
+
classes.push("BLOCKED_BY_LOGIN");
|
|
116
|
+
if (/api key|apikey|credential|inference credential|provider config|openhands/.test(safeText))
|
|
117
|
+
classes.push("BLOCKED_BY_API_KEY");
|
|
118
|
+
if (/permission denied|eacces|operation not permitted/.test(safeText))
|
|
119
|
+
classes.push("BLOCKED_BY_PERMISSION");
|
|
120
|
+
if (/roleplay|fiction|cannot accept role|not a real protocol/.test(safeText))
|
|
121
|
+
classes.push("ROLE_COMPATIBILITY_FAILED");
|
|
122
|
+
if (harness.toLowerCase() === "kilocode" && /auth login|\/connect|login/.test(safeText))
|
|
123
|
+
classes.push("BLOCKED_BY_LOGIN");
|
|
124
|
+
if (harness.toLowerCase() === "openhands" && /api|credential|provider/.test(safeText))
|
|
125
|
+
classes.push("AUTH_PROVIDER_BLOCKED");
|
|
126
|
+
if (harness.toLowerCase() === "crush" && /permission|denied|approve/.test(safeText))
|
|
127
|
+
classes.push("BLOCKED_BY_PERMISSION");
|
|
128
|
+
if (harness.toLowerCase() === "pi" && /role|mcp|skill|runtime proof|fiction/.test(safeText))
|
|
129
|
+
classes.push("ROLE_COMPATIBILITY_FAILED");
|
|
130
|
+
return [...new Set(classes)];
|
|
131
|
+
}
|
|
44
132
|
function normalizePodCount(pods) {
|
|
45
133
|
const value = pods ?? 1;
|
|
46
134
|
if (!Number.isInteger(value) || value < 0 || value > 25) {
|
|
@@ -91,26 +179,121 @@ export function getStartupPacket(input = {}, repository = getDefaultRepository()
|
|
|
91
179
|
if (!roleModelProfile) {
|
|
92
180
|
throw new Error(`No model profile found for role: ${safeErrorText(role)}`);
|
|
93
181
|
}
|
|
182
|
+
const governedMode = input.governedMode ?? "GOVERNED_TEAM";
|
|
183
|
+
const layoutProfile = input.layoutProfile ?? "human_cmux_quad";
|
|
184
|
+
const activeWatch = getActiveWatchPacket({
|
|
185
|
+
role,
|
|
186
|
+
parkedMode: input.parkedMode,
|
|
187
|
+
manifest: {
|
|
188
|
+
executive_office: ["A/EXEC-PM", "A/EXEC-ODIN", "A/EXEC-ASST", "A/EXEC-RSCH", "A/EXEC-QA"],
|
|
189
|
+
development_pods: ["B", "C"]
|
|
190
|
+
}
|
|
191
|
+
});
|
|
94
192
|
return {
|
|
95
193
|
version: VERSION,
|
|
194
|
+
protocolVersion: PROTOCOL_SCHEMA_VERSION,
|
|
195
|
+
publicLatestVersion: PUBLIC_LATEST_VERSION,
|
|
196
|
+
minimumCompatibleMcpVersion: MINIMUM_COMPATIBLE_MCP_VERSION,
|
|
96
197
|
role,
|
|
97
198
|
pods,
|
|
199
|
+
governedMode,
|
|
200
|
+
layoutProfile,
|
|
98
201
|
resourcesToRead,
|
|
99
202
|
requiredActions,
|
|
100
203
|
defaultTopology: data.topology.default_topology,
|
|
101
204
|
modelProfile: roleModelProfile,
|
|
102
205
|
bootReceiptRequiredFields: data.bootReceipt.required_fields,
|
|
206
|
+
bootReceiptSchema: getBootReceiptSchema(repository),
|
|
207
|
+
teamManifestLocator: "odin://protocol/receipts/team-manifest",
|
|
208
|
+
activeWatch,
|
|
103
209
|
startupPrompt: [
|
|
104
210
|
"Use ODIN Sentinel coordination.",
|
|
105
211
|
"",
|
|
106
212
|
`You are ${role}.`,
|
|
213
|
+
`Governed mode: ${governedMode}.`,
|
|
214
|
+
`ODIN Sentinel MCP version: ${VERSION}; public/latest target: ${PUBLIC_LATEST_VERSION}; minimum compatible child MCP version: ${MINIMUM_COMPATIBLE_MCP_VERSION}.`,
|
|
215
|
+
`Expected layout profile: ${layoutProfile}. A/EXEC-PM stays in the same CMUX workspace as governed teams.`,
|
|
107
216
|
"Load startup requirements from the odin-sentinel MCP resources and tools. Do not assume external local extensions exist.",
|
|
217
|
+
"Before occupant launch, readiness must PASS or be explicitly WAIVED_BY_EXEC_PM / SUBSTITUTION_APPROVED_BY_EXEC_PM.",
|
|
218
|
+
"Valid SCP context source required for governed occupants: native sentinel-coordination-protocol skill, compatible odin-sentinel MCP, or full injected SCP protocol text.",
|
|
219
|
+
"Boot receipt schema: use write_scope: [] for no current write assignment; do not use null.",
|
|
220
|
+
"Team manifest locator: odin://protocol/receipts/team-manifest.",
|
|
108
221
|
input.repoPath ? `Repository: ${input.repoPath}` : "Repository: discover from current working directory.",
|
|
109
222
|
`Bootstrap executive office plus ${pods} development pod${pods === 1 ? "" : "s"} unless handoff or user instruction overrides this.`,
|
|
223
|
+
isOdinRole(role) ? String(activeWatch.promptText) : "Non-ODIN role: no ODIN active-watch authority.",
|
|
110
224
|
input.userInstruction ? `User instruction: ${input.userInstruction}` : "Ask for objectives if no handoff supplies them."
|
|
111
225
|
].join("\n")
|
|
112
226
|
};
|
|
113
227
|
}
|
|
228
|
+
export function getVersionMetadata() {
|
|
229
|
+
return {
|
|
230
|
+
name: "odin-sentinel",
|
|
231
|
+
version: VERSION,
|
|
232
|
+
serverVersion: VERSION,
|
|
233
|
+
protocolVersion: PROTOCOL_SCHEMA_VERSION,
|
|
234
|
+
publicLatestVersion: PUBLIC_LATEST_VERSION,
|
|
235
|
+
minimumCompatibleMcpVersion: MINIMUM_COMPATIBLE_MCP_VERSION,
|
|
236
|
+
driftChecks: {
|
|
237
|
+
observedTooOldExample: "0.2.1",
|
|
238
|
+
tooOldClass: "MCP_VERSION_TOO_OLD",
|
|
239
|
+
publicLatestDiffersFromProtocol: PUBLIC_LATEST_VERSION !== PROTOCOL_SCHEMA_VERSION
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
export function getBootReceiptSchema(repository = getDefaultRepository()) {
|
|
244
|
+
const data = loadProtocolData(repository);
|
|
245
|
+
return {
|
|
246
|
+
receipt_types: ["SCP_BOOT_RECEIPT", "SCP_MIN_BOOT_RECEIPT"],
|
|
247
|
+
minimumCompatibleMcpVersion: MINIMUM_COMPATIBLE_MCP_VERSION,
|
|
248
|
+
required_fields: data.bootReceipt.required_fields,
|
|
249
|
+
field_types: {
|
|
250
|
+
role: "string",
|
|
251
|
+
authority_layer: "string",
|
|
252
|
+
team: "string",
|
|
253
|
+
terminal_locator: "string",
|
|
254
|
+
branch: "string",
|
|
255
|
+
cwd: "string",
|
|
256
|
+
model_harness: "string",
|
|
257
|
+
permission_mode: "string",
|
|
258
|
+
may_implement: "boolean",
|
|
259
|
+
may_qa_accept: "boolean",
|
|
260
|
+
reports_to: "string",
|
|
261
|
+
write_scope: "string[]; use [] when unassigned; null is invalid",
|
|
262
|
+
evidence_path: "string",
|
|
263
|
+
current_task: "string"
|
|
264
|
+
},
|
|
265
|
+
allowed_lifecycle_states: [
|
|
266
|
+
"SURFACE_PROVISIONED",
|
|
267
|
+
"BOOTSTRAPPED_IDLE",
|
|
268
|
+
"ACTIVE_WATCH",
|
|
269
|
+
"VACANT_ROLE_SLOT",
|
|
270
|
+
"AGENT_SUBSTITUTION_REQUIRED",
|
|
271
|
+
"RELEASED_BY_OPERATOR",
|
|
272
|
+
"HANDED_OFF",
|
|
273
|
+
"PARKED_IDLE",
|
|
274
|
+
"FAILED",
|
|
275
|
+
"WATCH_UNSUPPORTED"
|
|
276
|
+
],
|
|
277
|
+
examples: getBootReceiptExamples()
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export function getBootReceiptExamples() {
|
|
281
|
+
const base = {
|
|
282
|
+
branch: "main",
|
|
283
|
+
cwd: "/path/to/repo",
|
|
284
|
+
permission_mode: "workspace-write",
|
|
285
|
+
evidence_path: ".odin/audit/session",
|
|
286
|
+
current_task: "bootstrap",
|
|
287
|
+
write_scope: []
|
|
288
|
+
};
|
|
289
|
+
return {
|
|
290
|
+
pm: { ...base, role: "A/EXEC-PM", authority_layer: "executive", team: "A", terminal_locator: "workspace:1 pane:a surface:pm", model_harness: "Codex CLI", may_implement: false, may_qa_accept: false, reports_to: "operator" },
|
|
291
|
+
odin: { ...base, role: "A/EXEC-ODIN", authority_layer: "meta_control", team: "A", terminal_locator: "workspace:1 pane:a surface:odin", model_harness: "Codex CLI", may_implement: false, may_qa_accept: false, reports_to: "operator", lifecycle_state: "ACTIVE_WATCH" },
|
|
292
|
+
dev_waiting_for_scope: { ...base, role: "B/DEV-1", authority_layer: "implementation", team: "B", terminal_locator: "workspace:1 pane:b surface:dev-1", model_harness: "Droid", may_implement: true, may_qa_accept: false, reports_to: "B/TEAM-PM", lifecycle_state: "BOOTSTRAPPED_IDLE", staffed_by: "A/EXEC-PM", parent_surface_ref: "pane:b", column_index: 1, team_letter: "B" },
|
|
293
|
+
qa: { ...base, role: "B/QA-1", authority_layer: "quality", team: "B", terminal_locator: "workspace:1 pane:b surface:qa-1", model_harness: "Crush", may_implement: false, may_qa_accept: true, reports_to: "B/TEAM-PM", staffed_by: "A/EXEC-PM", parent_surface_ref: "pane:b", column_index: 1, team_letter: "B" },
|
|
294
|
+
shadow: { ...base, role: "B/SHADOW-1", authority_layer: "review", team: "B", terminal_locator: "workspace:1 pane:b surface:shadow-1", model_harness: "Droid", may_implement: false, may_qa_accept: false, reports_to: "B/TEAM-PM", staffed_by: "A/EXEC-PM", parent_surface_ref: "pane:b", column_index: 1, team_letter: "B" }
|
|
295
|
+
};
|
|
296
|
+
}
|
|
114
297
|
export function getDelegationPacket(input) {
|
|
115
298
|
return {
|
|
116
299
|
receipt_type: "SCP-DELEGATE",
|
|
@@ -221,6 +404,34 @@ export function validateBootReceipt(receipt, repository = getDefaultRepository()
|
|
|
221
404
|
team_letter: "string"
|
|
222
405
|
});
|
|
223
406
|
const warnings = [];
|
|
407
|
+
if (receipt.write_scope === null) {
|
|
408
|
+
invalid.push("write_scope");
|
|
409
|
+
warnings.push("write_scope: null is invalid; use write_scope: [] for unassigned or no-write roles");
|
|
410
|
+
}
|
|
411
|
+
else if (typeof receipt.write_scope === "string") {
|
|
412
|
+
invalid.push("write_scope");
|
|
413
|
+
warnings.push("write_scope must be an array of strings; use [] when no write scope is assigned");
|
|
414
|
+
}
|
|
415
|
+
if (typeof receipt.lifecycle_state === "string" &&
|
|
416
|
+
![
|
|
417
|
+
"SURFACE_PROVISIONED",
|
|
418
|
+
"BOOTSTRAPPED_IDLE",
|
|
419
|
+
"ACTIVE_WATCH",
|
|
420
|
+
"VACANT_ROLE_SLOT",
|
|
421
|
+
"AGENT_SUBSTITUTION_REQUIRED",
|
|
422
|
+
"RELEASED_BY_OPERATOR",
|
|
423
|
+
"HANDED_OFF",
|
|
424
|
+
"PARKED_IDLE",
|
|
425
|
+
"FAILED",
|
|
426
|
+
"WATCH_UNSUPPORTED"
|
|
427
|
+
].includes(receipt.lifecycle_state)) {
|
|
428
|
+
invalid.push("lifecycle_state");
|
|
429
|
+
warnings.push("lifecycle_state is not one of the canonical ODIN/SCP states");
|
|
430
|
+
}
|
|
431
|
+
if (typeof receipt.mcp_version === "string" && isVersionBelow(receipt.mcp_version, MINIMUM_COMPATIBLE_MCP_VERSION)) {
|
|
432
|
+
invalid.push("mcp_version");
|
|
433
|
+
warnings.push(`MCP_VERSION_TOO_OLD: minimum compatible odin-sentinel MCP version is ${MINIMUM_COMPATIBLE_MCP_VERSION}`);
|
|
434
|
+
}
|
|
224
435
|
if (receipt.may_implement === true && receipt.may_qa_accept === true) {
|
|
225
436
|
invalid.push("may_qa_accept");
|
|
226
437
|
warnings.push("same receipt grants both implementation and QA acceptance authority");
|
|
@@ -257,6 +468,11 @@ export function validateBootReceipt(receipt, repository = getDefaultRepository()
|
|
|
257
468
|
export function validateTeamManifest(manifest, repository = getDefaultRepository()) {
|
|
258
469
|
const data = loadProtocolData(repository);
|
|
259
470
|
const required = requireStringArray(data.teamManifest.required_fields, "team-manifest.required_fields");
|
|
471
|
+
const roleSlotSchema = requireRecord(data.teamManifest.role_slot_schema, "team-manifest.role_slot_schema");
|
|
472
|
+
const requiredRoleSlotFields = requireStringArray(roleSlotSchema.required_fields, "team-manifest.role_slot_schema.required_fields");
|
|
473
|
+
const layoutLocatorFields = requireStringArray(roleSlotSchema.layout_locator_fields, "team-manifest.role_slot_schema.layout_locator_fields");
|
|
474
|
+
const readinessStatuses = requireStringArray(roleSlotSchema.readiness_statuses, "team-manifest.role_slot_schema.readiness_statuses");
|
|
475
|
+
const scpContextSources = requireStringArray(data.teamManifest.scp_context_sources, "team-manifest.scp_context_sources");
|
|
260
476
|
const missing = validateRequiredFields(manifest, required);
|
|
261
477
|
const invalid = validateFieldTypes(manifest, {
|
|
262
478
|
session_id: "string",
|
|
@@ -282,8 +498,293 @@ export function validateTeamManifest(manifest, repository = getDefaultRepository
|
|
|
282
498
|
}
|
|
283
499
|
}
|
|
284
500
|
}
|
|
501
|
+
if (Array.isArray(manifest.role_slots)) {
|
|
502
|
+
for (const [index, slotValue] of manifest.role_slots.entries()) {
|
|
503
|
+
const slot = asRecord(slotValue);
|
|
504
|
+
const roleSlot = typeof slot.role_slot === "string" ? slot.role_slot : undefined;
|
|
505
|
+
const governed = slot.governed !== false && (!roleSlot || isGovernedRole(roleSlot));
|
|
506
|
+
const layoutLocator = asRecord(slot.layout_locator);
|
|
507
|
+
for (const field of requiredRoleSlotFields) {
|
|
508
|
+
if (slot[field] === undefined || slot[field] === null || (typeof slot[field] === "string" && slot[field].trim() === "")) {
|
|
509
|
+
invalid.push(`role_slots.${index}.${field}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (!roleSlot || !isVisibleRoleSlot(roleSlot))
|
|
513
|
+
invalid.push(`role_slots.${index}.role_slot`);
|
|
514
|
+
if (!stringFieldPresent(slot.harness))
|
|
515
|
+
invalid.push(`role_slots.${index}.harness`);
|
|
516
|
+
if (!stringFieldPresent(slot.readiness_status) || !readinessStatuses.includes(String(slot.readiness_status))) {
|
|
517
|
+
invalid.push(`role_slots.${index}.readiness_status`);
|
|
518
|
+
}
|
|
519
|
+
if (governed && Object.keys(layoutLocator).length === 0) {
|
|
520
|
+
invalid.push(`role_slots.${index}.layout_locator`);
|
|
521
|
+
}
|
|
522
|
+
for (const field of layoutLocatorFields) {
|
|
523
|
+
if (governed && !stringFieldPresent(layoutLocator[field])) {
|
|
524
|
+
invalid.push(`role_slots.${index}.layout_locator.${field}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const contextSource = typeof slot.scp_context_source === "string" ? slot.scp_context_source.trim() : "";
|
|
528
|
+
const recognizedContextSource = scpContextSources.includes(contextSource);
|
|
529
|
+
const hasContext = recognizedContextSource ||
|
|
530
|
+
slot.scp_skill_available === true ||
|
|
531
|
+
slot.full_protocol_text_injected === true ||
|
|
532
|
+
slot.mcp_available === true;
|
|
533
|
+
if (governed && !recognizedContextSource) {
|
|
534
|
+
invalid.push(`role_slots.${index}.scp_context_source`);
|
|
535
|
+
}
|
|
536
|
+
if (governed && !hasContext) {
|
|
537
|
+
warnings.push(`role_slots.${index} lacks SCP context source proof`);
|
|
538
|
+
}
|
|
539
|
+
if (typeof slot.mcp_version === "string" && isVersionBelow(slot.mcp_version, MINIMUM_COMPATIBLE_MCP_VERSION)) {
|
|
540
|
+
invalid.push(`role_slots.${index}.mcp_version`);
|
|
541
|
+
warnings.push(`role_slots.${index} MCP_VERSION_TOO_OLD: minimum compatible version is ${MINIMUM_COMPATIBLE_MCP_VERSION}`);
|
|
542
|
+
}
|
|
543
|
+
if (roleSlot && isOdinRole(roleSlot) && !slot.watches) {
|
|
544
|
+
warnings.push(`role_slots.${index} ODIN slot should declare watcher assignments`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
285
548
|
return buildValidationResult(missing, invalid, warnings);
|
|
286
549
|
}
|
|
550
|
+
export function evaluateReadinessGate(input) {
|
|
551
|
+
const minimum = input.minimumMcpVersion ?? MINIMUM_COMPATIBLE_MCP_VERSION;
|
|
552
|
+
const userProvisioningAnswer = input.userProvisioningAnswer ?? "unknown";
|
|
553
|
+
const execPmAuthorized = input.execPmAuthorized === true;
|
|
554
|
+
const cmuxAvailable = input.cmuxAvailable === true;
|
|
555
|
+
const hasSlots = input.slots.length > 0;
|
|
556
|
+
const rows = input.slots.map((slot) => {
|
|
557
|
+
const harness = slot.harness ?? "unknown";
|
|
558
|
+
const classifications = [];
|
|
559
|
+
const notes = [];
|
|
560
|
+
const sources = scpContextSources(slot);
|
|
561
|
+
let status = "PASS";
|
|
562
|
+
if (!execPmAuthorized)
|
|
563
|
+
classifications.push("EXEC_PM_AUTHORIZATION_REQUIRED");
|
|
564
|
+
if (!cmuxAvailable)
|
|
565
|
+
classifications.push("CMUX_UNAVAILABLE");
|
|
566
|
+
const vacantSlot = slot.occupantState === "VACANT_ROLE_SLOT";
|
|
567
|
+
if (vacantSlot) {
|
|
568
|
+
classifications.push("VACANT_ROLE_SLOT");
|
|
569
|
+
notes.push("Vacant role slot is provisioned without an occupant. It may launch as a surface placeholder; activation is allowed only when the slot is not marked required.");
|
|
570
|
+
if (slot.required !== false)
|
|
571
|
+
classifications.push("VACANT_SLOT_REQUIREMENT_UNVERIFIED");
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
if (slot.mcpAvailable === true && typeof slot.mcpVersion !== "string")
|
|
575
|
+
classifications.push("MCP_VERSION_UNVERIFIED");
|
|
576
|
+
if (slot.mcpAvailable === true && typeof slot.mcpVersion === "string" && isVersionBelow(slot.mcpVersion, minimum))
|
|
577
|
+
classifications.push("MCP_VERSION_TOO_OLD");
|
|
578
|
+
if (slot.mcpAvailable === false)
|
|
579
|
+
classifications.push("MCP_UNAVAILABLE");
|
|
580
|
+
if (slot.scpSkillAvailable === false)
|
|
581
|
+
classifications.push("SCP_SKILL_MISSING");
|
|
582
|
+
if (sources.length === 0 && isGovernedRole(slot.roleSlot))
|
|
583
|
+
classifications.push("NON_GOVERNED_ONE_SHOT_ONLY");
|
|
584
|
+
if (slot.firstRunPermissionStatus === "PROMPT_WAITING" || slot.firstRunPermissionStatus === "DENIED")
|
|
585
|
+
classifications.push("BLOCKED_BY_PERMISSION");
|
|
586
|
+
if (slot.authStatus === "AUTH_LOGIN_REQUIRED")
|
|
587
|
+
classifications.push("BLOCKED_BY_LOGIN");
|
|
588
|
+
if (slot.authStatus === "AUTH_MISSING")
|
|
589
|
+
classifications.push("BLOCKED_BY_API_KEY");
|
|
590
|
+
if (slot.authStatus === "AUTH_PROVIDER_BLOCKED")
|
|
591
|
+
classifications.push("AUTH_PROVIDER_BLOCKED");
|
|
592
|
+
if (slot.authStatus === "AUTH_UNKNOWN")
|
|
593
|
+
classifications.push("BLOCKED_BY_AUTH");
|
|
594
|
+
if (slot.modelStatus === "MODEL_STALLED")
|
|
595
|
+
classifications.push("MODEL_STALLED");
|
|
596
|
+
if (slot.modelStatus === "STREAMING_PROTOCOL_MISMATCH" || slot.modelStatus === "MODEL_REASONING_ONLY")
|
|
597
|
+
classifications.push("STREAMING_PROTOCOL_MISMATCH");
|
|
598
|
+
if (slot.modelStatus === "MODEL_UNREACHABLE")
|
|
599
|
+
classifications.push("MODEL_UNREACHABLE");
|
|
600
|
+
if (slot.roleCompatibility === "REFUSES_ROLE" || slot.roleCompatibility === "UNPROVEN")
|
|
601
|
+
classifications.push("ROLE_COMPATIBILITY_FAILED");
|
|
602
|
+
if (userProvisioningAnswer !== "yes" && ["AUTH_MISSING", "AUTH_UNKNOWN", undefined].includes(slot.authStatus))
|
|
603
|
+
classifications.push("USER_INPUT_REQUIRED");
|
|
604
|
+
if (isOdinRole(slot.roleSlot) && slot.canHydrateDeferredMcpToolsAtBoot !== true && slot.fullProtocolTextInjected !== true && slot.scpSkillAvailable !== true) {
|
|
605
|
+
classifications.push("UNSUITABLE_FOR_ODIN_ROLE");
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const uniqueClassifications = [...new Set(classifications)];
|
|
609
|
+
const failureClassifications = uniqueClassifications.filter((item) => item !== "SCP_SKILL_MISSING" && item !== "NON_GOVERNED_ONE_SHOT_ONLY" && item !== "VACANT_ROLE_SLOT");
|
|
610
|
+
const unwaivableLaunchBlockers = new Set([
|
|
611
|
+
"MCP_VERSION_UNVERIFIED",
|
|
612
|
+
"MCP_VERSION_TOO_OLD",
|
|
613
|
+
"MCP_UNAVAILABLE",
|
|
614
|
+
"BLOCKED_BY_PERMISSION",
|
|
615
|
+
"MODEL_STALLED",
|
|
616
|
+
"STREAMING_PROTOCOL_MISMATCH",
|
|
617
|
+
"MODEL_UNREACHABLE",
|
|
618
|
+
"ROLE_COMPATIBILITY_FAILED",
|
|
619
|
+
"UNSUITABLE_FOR_ODIN_ROLE"
|
|
620
|
+
]);
|
|
621
|
+
const hasUnwaivableLaunchBlocker = uniqueClassifications.some((item) => unwaivableLaunchBlockers.has(item));
|
|
622
|
+
const approvedWaiver = execPmAuthorized && slot.waiver === "WAIVED_BY_EXEC_PM" && !hasUnwaivableLaunchBlocker;
|
|
623
|
+
const approvedSubstitution = execPmAuthorized &&
|
|
624
|
+
slot.waiver === "SUBSTITUTION_APPROVED_BY_EXEC_PM" &&
|
|
625
|
+
typeof slot.fallbackHarness === "string" &&
|
|
626
|
+
slot.fallbackHarness.trim().length > 0 &&
|
|
627
|
+
failureClassifications.length === 0;
|
|
628
|
+
if (uniqueClassifications.includes("NON_GOVERNED_ONE_SHOT_ONLY")) {
|
|
629
|
+
status = status === "PASS" ? "NON_GOVERNED_ONE_SHOT_ONLY" : status;
|
|
630
|
+
notes.push("Non-governed fallback constraints: one deterministic assignment, no ongoing authority, no management role, no cross-agent coordination, command/test/QA-style verification preferred, context cleared after response.");
|
|
631
|
+
}
|
|
632
|
+
if (failureClassifications.length > 0 && status === "PASS")
|
|
633
|
+
status = "FAIL";
|
|
634
|
+
if (approvedWaiver)
|
|
635
|
+
status = "WAIVED_BY_EXEC_PM";
|
|
636
|
+
if (approvedSubstitution)
|
|
637
|
+
status = "SUBSTITUTION_APPROVED_BY_EXEC_PM";
|
|
638
|
+
const occupantState = slot.occupantState ?? "";
|
|
639
|
+
const vacantSlotExplicitlyOptional = vacantSlot && slot.required === false;
|
|
640
|
+
const readyOccupant = ["BOOTSTRAPPED_IDLE", "ACTIVE_WATCH"].includes(occupantState) || vacantSlotExplicitlyOptional;
|
|
641
|
+
const substitutionActivationReady = vacantSlotExplicitlyOptional;
|
|
642
|
+
const launchAllowed = status === "PASS" || status === "WAIVED_BY_EXEC_PM" || status === "SUBSTITUTION_APPROVED_BY_EXEC_PM";
|
|
643
|
+
const activationAllowed = status === "PASS"
|
|
644
|
+
? readyOccupant
|
|
645
|
+
: status === "WAIVED_BY_EXEC_PM"
|
|
646
|
+
? readyOccupant && failureClassifications.length === 0
|
|
647
|
+
: status === "SUBSTITUTION_APPROVED_BY_EXEC_PM"
|
|
648
|
+
? substitutionActivationReady
|
|
649
|
+
: false;
|
|
650
|
+
return {
|
|
651
|
+
roleSlot: slot.roleSlot,
|
|
652
|
+
harness,
|
|
653
|
+
status,
|
|
654
|
+
launchAllowed,
|
|
655
|
+
activationAllowed,
|
|
656
|
+
classifications: uniqueClassifications,
|
|
657
|
+
safeOutcomes: status === "PASS" ? [] : defaultSafeOutcomes(),
|
|
658
|
+
scpContextSources: sources,
|
|
659
|
+
mcpVersion: slot.mcpVersion,
|
|
660
|
+
deferredMcpHydration: slot.canHydrateDeferredMcpToolsAtBoot === true ? "AT_BOOT" : slot.canHydrateDeferredMcpToolsAfterSecondTurn === true ? "AFTER_SECOND_TURN" : "UNPROVEN",
|
|
661
|
+
nativeSkillInvocation: slot.nativeSkillInvocation === true,
|
|
662
|
+
notes
|
|
663
|
+
};
|
|
664
|
+
});
|
|
665
|
+
const overallStatus = hasSlots && rows.every((row) => row.launchAllowed) && cmuxAvailable && execPmAuthorized ? "PASS" : "FAIL";
|
|
666
|
+
const activationStatus = hasSlots && rows.every((row) => row.activationAllowed) && cmuxAvailable && execPmAuthorized ? "TEAM_ACTIVATION_ALLOWED" : "TEAM_ACTIVATION_BLOCKED";
|
|
667
|
+
return {
|
|
668
|
+
version: VERSION,
|
|
669
|
+
minimumMcpVersion: minimum,
|
|
670
|
+
phases: GOVERNED_LAUNCH_PHASES,
|
|
671
|
+
overallStatus,
|
|
672
|
+
activationStatus,
|
|
673
|
+
execPmAuthorized,
|
|
674
|
+
cmuxAvailable,
|
|
675
|
+
userPrompt: "Are all intended harnesses provisioned with accounts, plans, API keys, or local inference credentials so they will not malfunction when spun up?",
|
|
676
|
+
readinessMatrix: rows,
|
|
677
|
+
zeroSecretOutput: true
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function manifestWatchTargets(role, manifest) {
|
|
681
|
+
const explicitSlots = Array.isArray(manifest?.role_slots)
|
|
682
|
+
? manifest.role_slots.flatMap((slotValue) => {
|
|
683
|
+
const slot = asRecord(slotValue);
|
|
684
|
+
const watches = Array.isArray(slot.watches) ? slot.watches : [];
|
|
685
|
+
return typeof slot.role_slot === "string" && slot.role_slot === role
|
|
686
|
+
? watches.filter((item) => typeof item === "string")
|
|
687
|
+
: [];
|
|
688
|
+
})
|
|
689
|
+
: [];
|
|
690
|
+
if (explicitSlots.length > 0)
|
|
691
|
+
return explicitSlots;
|
|
692
|
+
if (roleKind(role) === "EXEC_ODIN") {
|
|
693
|
+
return ["A/EXEC-PM", "A/EXEC-ASST", "A/EXEC-RSCH", "A/EXEC-QA", "B/ODIN", "C/ODIN"];
|
|
694
|
+
}
|
|
695
|
+
const team = role.includes("/") ? role.split("/")[0] : "B";
|
|
696
|
+
return [`${team}/TEAM-PM`, `${team}/DEV-1`, `${team}/QA-1`, `${team}/SHADOW-1`];
|
|
697
|
+
}
|
|
698
|
+
export function getActiveWatchPacket(input) {
|
|
699
|
+
const pollIntervalSeconds = input.pollIntervalSeconds ?? 30;
|
|
700
|
+
const targets = manifestWatchTargets(input.role, input.manifest);
|
|
701
|
+
const odinRole = isOdinRole(input.role);
|
|
702
|
+
const state = odinRole && input.parkedMode !== true ? "ACTIVE_WATCH" : input.parkedMode === true ? "PARKED_IDLE" : "WATCH_UNSUPPORTED";
|
|
703
|
+
const promptText = odinRole
|
|
704
|
+
? [
|
|
705
|
+
`You are ${input.role}. After a valid boot receipt, transition to ${state}.`,
|
|
706
|
+
`Poll every ${pollIntervalSeconds} seconds unless explicitly released, parked, or handed off.`,
|
|
707
|
+
`Watch targets from the team manifest: ${targets.join(", ") || "NONE"}.`,
|
|
708
|
+
"Classify permission prompts immediately as BLOCKED_BY_PERMISSION.",
|
|
709
|
+
"Classify auth/login/API-key screens as BLOCKED_BY_AUTH, BLOCKED_BY_LOGIN, or BLOCKED_BY_API_KEY.",
|
|
710
|
+
"Classify local inference stalls separately: MODEL_STALLED or STREAMING_PROTOCOL_MISMATCH.",
|
|
711
|
+
"WATCH_WARN after 5 minutes without meaningful visible progress; STALLED after 10 minutes without heartbeat/status unless a known long-running operation is declared.",
|
|
712
|
+
"Allowed interventions: corrective prompts for scope drift, authority drift, secret mishandling, stopped polling, blocked panes, stale proof, missing receipts, and context exhaustion.",
|
|
713
|
+
"Forbidden actions: implement product work, QA-accept work, route business priorities, or override EXEC PM launch/activation authority.",
|
|
714
|
+
"ODIN is a meta-control peer layer: not DEV/QA, not equal to EXEC PM for launch authority, and not too subordinate to object to PM or agent drift.",
|
|
715
|
+
"A turn ending without re-arming the watch loop, starting an approved monitor, or handing off to a named successor is a protocol violation.",
|
|
716
|
+
input.planMode === true ? "Plan-mode/read-only carve-out: if persistent polling is unavailable, emit a named re-arm instruction and explicit next wake condition." : "Persistent watch is expected."
|
|
717
|
+
].join("\n")
|
|
718
|
+
: "This role is not an ODIN role and has no ODIN active-watch authority.";
|
|
719
|
+
return {
|
|
720
|
+
role: input.role,
|
|
721
|
+
state,
|
|
722
|
+
pollIntervalSeconds,
|
|
723
|
+
watchWarnSeconds: 300,
|
|
724
|
+
stalledSeconds: 600,
|
|
725
|
+
targets,
|
|
726
|
+
terminalStates: ACTIVE_WATCH_TERMINAL_STATES,
|
|
727
|
+
classifications: ["BLOCKED_BY_PERMISSION", "BLOCKED_BY_AUTH", "BLOCKED_BY_LOGIN", "BLOCKED_BY_API_KEY", "MODEL_STALLED", "STREAMING_PROTOCOL_MISMATCH"],
|
|
728
|
+
mayIntervene: odinRole,
|
|
729
|
+
mayImplement: false,
|
|
730
|
+
mayQaAccept: false,
|
|
731
|
+
authority: { mayImplement: false, mayQaAccept: false },
|
|
732
|
+
promptText
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
export function getHarnessProbeMatrix(input = {}) {
|
|
736
|
+
const knownHarnesses = ["Codex", "Claude Code", "Droid", "Goose", "Crush", "OpenHands", "KiloCode", "Pi", "Aider", "NanoCoder"];
|
|
737
|
+
const intended = input.intendedHarnesses ?? knownHarnesses;
|
|
738
|
+
const installed = new Set((input.installedHarnesses ?? []).map((value) => value.toLowerCase()));
|
|
739
|
+
const observations = input.observations ?? [];
|
|
740
|
+
const timeout = input.visibleOutputTimeoutSeconds ?? 60;
|
|
741
|
+
const providerStatuses = Object.fromEntries(Object.entries(input.providerStatuses ?? {}).map(([name, present]) => [name, { present, value: present ? "present redacted" : "absent" }]));
|
|
742
|
+
const rows = intended.map((harness) => {
|
|
743
|
+
const observation = observations.find((item) => item.harness.toLowerCase() === harness.toLowerCase());
|
|
744
|
+
const classifications = new Set();
|
|
745
|
+
if (!installed.has(harness.toLowerCase()))
|
|
746
|
+
classifications.add("NOT_INSTALLED_OR_UNPROVEN");
|
|
747
|
+
if (input.userProvisioningAnswer !== "yes")
|
|
748
|
+
classifications.add("USER_INPUT_REQUIRED");
|
|
749
|
+
for (const item of classifyProbeText(harness, observation?.text ?? ""))
|
|
750
|
+
classifications.add(item);
|
|
751
|
+
if (observation?.reasoningContentOnly === true && observation.visibleContent !== true)
|
|
752
|
+
classifications.add("MODEL_REASONING_ONLY");
|
|
753
|
+
if (observation?.httpReachable === false)
|
|
754
|
+
classifications.add("MODEL_UNREACHABLE");
|
|
755
|
+
if (observation?.modelLoaded === false)
|
|
756
|
+
classifications.add("MODEL_UNREACHABLE");
|
|
757
|
+
if ((observation?.elapsedSeconds ?? 0) > timeout && observation?.visibleContent !== true)
|
|
758
|
+
classifications.add("MODEL_STALLED");
|
|
759
|
+
if (harness.toLowerCase() === "goose" && observation?.reasoningContentOnly === true && observation.visibleContent !== true)
|
|
760
|
+
classifications.add("STREAMING_PROTOCOL_MISMATCH");
|
|
761
|
+
const modelStatus = classifications.has("STREAMING_PROTOCOL_MISMATCH") ? "STREAMING_PROTOCOL_MISMATCH" :
|
|
762
|
+
classifications.has("MODEL_REASONING_ONLY") ? "MODEL_REASONING_ONLY" :
|
|
763
|
+
classifications.has("MODEL_STALLED") ? "MODEL_STALLED" :
|
|
764
|
+
classifications.has("MODEL_UNREACHABLE") ? "MODEL_UNREACHABLE" :
|
|
765
|
+
observation?.visibleContent === true ? "MODEL_READY" : "MODEL_SLOW";
|
|
766
|
+
return {
|
|
767
|
+
harness,
|
|
768
|
+
installed: installed.has(harness.toLowerCase()),
|
|
769
|
+
classifications: [...classifications],
|
|
770
|
+
modelStatus,
|
|
771
|
+
visibleOutputTimeoutSeconds: timeout,
|
|
772
|
+
canHydrateDeferredMcpToolsAtBoot: ["Codex", "Claude Code", "Droid"].includes(harness),
|
|
773
|
+
canHydrateDeferredMcpToolsAfterSecondTurn: true,
|
|
774
|
+
nativeSkillInvocation: ["Codex", "Claude Code"].includes(harness),
|
|
775
|
+
sentinelCoordinationProtocolSkill: "install before governed launch when the harness supports native skills",
|
|
776
|
+
safeNextActions: classifications.size === 0 ? [] : defaultSafeOutcomes(),
|
|
777
|
+
sanitizedObservation: observation?.text ? redactSecretLikeText(observation.text) : undefined
|
|
778
|
+
};
|
|
779
|
+
});
|
|
780
|
+
return {
|
|
781
|
+
zeroSecretOutput: true,
|
|
782
|
+
userPrompt: "Are all intended harnesses provisioned with accounts, plans, API keys, or local inference credentials so they will not malfunction when spun up?",
|
|
783
|
+
secretProviderStatuses: providerStatuses,
|
|
784
|
+
supportedProviders: ["Doppler", "1Password CLI (op)", "environment variable names", "direnv", "mise", "dotenv-style file presence", "GitHub auth", "local provider config files"],
|
|
785
|
+
rows
|
|
786
|
+
};
|
|
787
|
+
}
|
|
287
788
|
export function getCloseoutChecklist(mode, repository = getDefaultRepository()) {
|
|
288
789
|
const data = loadProtocolData(repository);
|
|
289
790
|
const modes = requireRecord(data.closeout.modes, "closeout.modes");
|
|
@@ -297,14 +798,19 @@ export function getRuntimeNotice() {
|
|
|
297
798
|
return {
|
|
298
799
|
inferenceProvider: "none",
|
|
299
800
|
hostedService: false,
|
|
300
|
-
telemetry:
|
|
301
|
-
networkCalls:
|
|
801
|
+
telemetry: "local_optional",
|
|
802
|
+
networkCalls: "none_by_default_optional_on_submit",
|
|
302
803
|
maintainerPaysForUserInference: false,
|
|
303
804
|
userPaysForHarnessInference: true,
|
|
304
805
|
externalOrchestrationBundled: false,
|
|
806
|
+
telemetryAutomaticCollection: false,
|
|
807
|
+
telemetrySubmissionRequiresEndpoint: true,
|
|
808
|
+
telemetrySubmissionRequiresExplicitInvocation: true,
|
|
305
809
|
notes: [
|
|
306
810
|
"ODIN Sentinel is a local stdio MCP server.",
|
|
307
811
|
"It serves protocol resources and validation tools; it does not proxy model calls.",
|
|
812
|
+
"Session reports are compiled locally; there is no automatic telemetry collection.",
|
|
813
|
+
"The submit_session_report tool may make a network call only after an endpoint is configured and the user explicitly invokes submission for that session.",
|
|
308
814
|
"Model and harness profiles are capability preferences, not hosted inference.",
|
|
309
815
|
"ODIN Sentinel is standalone and does not require any external orchestration repo."
|
|
310
816
|
]
|
|
@@ -337,6 +843,12 @@ export function createProtocolService(repository = createFileProtocolRepository(
|
|
|
337
843
|
loadProtocolData: () => loadProtocolData(repository),
|
|
338
844
|
getRoleProfile: (role) => getRoleProfile(role, repository),
|
|
339
845
|
getStartupPacket: (input = {}) => getStartupPacket(input, repository),
|
|
846
|
+
getVersionMetadata,
|
|
847
|
+
getBootReceiptSchema: () => getBootReceiptSchema(repository),
|
|
848
|
+
getBootReceiptExamples,
|
|
849
|
+
evaluateReadinessGate,
|
|
850
|
+
getActiveWatchPacket,
|
|
851
|
+
getHarnessProbeMatrix,
|
|
340
852
|
getDelegationPacket,
|
|
341
853
|
validateDelegationPacket: (packet) => validateDelegationPacket(packet, repository),
|
|
342
854
|
validateBootReceipt: (receipt) => validateBootReceipt(receipt, repository),
|