Compare commits

..
Author SHA1 Message Date
m 55f2f9affa fix: project explicit remembered assertions
CI / test (push) Failing after 26s
2026-08-29 15:07:47 +03:00
m e47e033dda merge: synchronize integration with current main
CI / test (push) Failing after 31s
2026-08-29 14:52:15 +03:00
m 2dc5587fd1 Clarify chat response progress
CI / test (push) Failing after 30s
2026-08-29 14:31:03 +03:00
m 992c84240b Preserve chat responsive guardrail 2026-08-29 14:30:29 +03:00
m cfeb6aeba7 Put chat first on mobile screens 2026-08-29 14:30:10 +03:00
m f0d45ca9f1 Clarify chat message roles and timing 2026-08-29 14:29:39 +03:00
m e20c9fdfc0 Improve Domovoy chat composer usability 2026-08-29 14:28:32 +03:00
m cb50a120e5 merge: record Gen2 deployment recovery evidence
CI / test (push) Failing after 29s
2026-08-29 12:49:59 +03:00
m b30eb38eca docs: record Gen2 deployment recovery evidence
CI / test (push) Failing after 28s
2026-08-29 12:49:51 +03:00
m 2083096aed merge: preserve stable rollback target
CI / test (push) Failing after 28s
2026-08-29 12:44:33 +03:00
m 671ecb4968 fix(supervisor): preserve stable rollback target
CI / test (push) Failing after 32s
2026-08-29 12:44:25 +03:00
m af9d5809e9 merge: stabilize supervisor control contract
CI / test (push) Failing after 27s
2026-08-29 12:41:30 +03:00
m 990928618d fix(supervisor): stabilize control JSON contract
CI / test (push) Failing after 31s
2026-08-29 12:41:22 +03:00
m 9fac8284f5 merge: initialize supervisor state volume permissions
CI / test (push) Failing after 27s
2026-08-29 12:39:01 +03:00
m 7d9a5edee5 fix(deploy): initialize supervisor volume permissions
CI / test (push) Failing after 32s
2026-08-29 12:38:53 +03:00
m 32d3f318a5 merge: independent supervisor control plane
CI / test (push) Failing after 31s
2026-08-29 12:33:54 +03:00
m 3a563a29ee feat(deploy): add independent supervisor control plane
CI / test (push) Failing after 28s
2026-08-29 12:33:44 +03:00
m 734db5d779 merge: synchronize integration with current main
CI / test (push) Failing after 28s
2026-08-29 12:25:26 +03:00
m 36fc8ec8c6 merge: resume Gen2 controlled canary
CI / test (push) Failing after 27s
2026-08-29 12:16:21 +03:00
m b8c001be4c chore(gen2): resume controlled production canary
CI / test (push) Failing after 26s
2026-08-29 12:16:09 +03:00
m eeeba6b7c0 merge: Gen2 database provisioning remediation
CI / test (push) Failing after 31s
2026-08-29 12:12:43 +03:00
m fce3ec569c fix(database): provision Gen2 owner roles before migrations
CI / test (push) Failing after 27s
2026-08-29 12:12:39 +03:00
m a925d787dc GEN2-027: prepare controlled production canary
CI / test (push) Failing after 26s
2026-08-29 12:01:37 +03:00
m 43556e1603 GEN2-026: route household context through scoped Gen2 projection
CI / test (push) Failing after 28s
2026-08-29 12:00:04 +03:00
m dd00dabf32 GEN2-025: normalize multimodal ingress provenance
CI / test (push) Failing after 27s
2026-08-29 11:58:34 +03:00
m 33365ce7ee GEN2-024: establish principal realms and source trust
CI / test (push) Failing after 26s
2026-08-29 11:57:19 +03:00
m 5017a854ee GEN2-023: make conversational configuration intent explicit
CI / test (push) Failing after 29s
2026-08-29 11:55:38 +03:00
m 839b35f310 GEN2 M4 remediation: durable recovery and governed evolution
CI / test (push) Failing after 26s
2026-08-29 11:47:05 +03:00
m 96687140ce GEN2-022: govern self evolution through development gates
CI / test (push) Failing after 27s
2026-08-29 11:31:45 +03:00
m b282038949 GEN2-021: add independent supervisor lifeboat
CI / test (push) Failing after 27s
2026-08-29 11:30:49 +03:00
m 4de506a2af GEN2-020: separate execution receipts from world outcomes
CI / test (push) Failing after 26s
2026-08-29 11:30:05 +03:00
59 changed files with 1919 additions and 44 deletions
+18
View File
@@ -0,0 +1,18 @@
package multimodal
import (
"crypto/sha256"
"encoding/hex"
"git.cin.su/m/Assistant/contracts/artifact"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/interaction"
)
func NormalizeIngress(id string, modality interaction.IngressModality, sourceRef, deviceRef, locationRef, principalID, contentType string, data []byte, response capability.MultimodalResponse, at int64) (interaction.IngressEnvelope, artifact.MediaArtifact) {
hash := sha256.Sum256(data)
artifactValue := artifact.MediaArtifact{ID: artifact.ArtifactID("artifact:media:" + hex.EncodeToString(hash[:8])), ContentType: contentType, SizeBytes: int64(len(data)), ContentDigest: hex.EncodeToString(hash[:]), SourceRef: sourceRef, RecordedAtUnixMillis: at}
if len(data) == 0 {
return interaction.IngressEnvelope{ID: id, Modality: modality, SourceRef: sourceRef, DeviceRef: deviceRef, LocationRef: locationRef, PrincipalID: principalID, Degraded: true, RecordedAtUnixMillis: at}, artifact.MediaArtifact{}
}
return interaction.IngressEnvelope{ID: id, Modality: modality, SourceRef: sourceRef, DeviceRef: deviceRef, LocationRef: locationRef, PrincipalID: principalID, ArtifactRef: string(artifactValue.ID), ObservationRef: response.EvidenceRef, ProviderRef: response.Provider, ModelRef: response.Model, ModelVersion: response.ModelVersion, ConfidenceMillis: response.ConfidenceMillis, RecordedAtUnixMillis: at}, artifactValue
}
+22
View File
@@ -0,0 +1,22 @@
package multimodal_test
import (
"git.cin.su/m/Assistant/adapters/multimodal"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/interaction"
"testing"
)
func TestIngressNormalizesMediaArtifactAndObservationProvenance(t *testing.T) {
e, a := multimodal.NormalizeIngress("ingress-1", interaction.IngressImage, "camera:street", "device:cam", "world:street", "principal", "image/png", []byte("image"), capability.MultimodalResponse{Provider: "local", Model: "vision", ModelVersion: "1", EvidenceRef: "observation-1", ConfidenceMillis: 800}, 1)
if !e.Valid() || !a.Valid() || e.ArtifactRef == "" || e.ObservationRef != "observation-1" || e.DeviceRef == "" || e.LocationRef == "" {
t.Fatalf("envelope=%+v artifact=%+v", e, a)
}
}
func TestUnavailableIngressDegradesWithoutFabricatingObservation(t *testing.T) {
e, a := multimodal.NormalizeIngress("ingress-1", interaction.IngressAudio, "mic", "device:mic", "", "principal", "audio/wav", nil, capability.MultimodalResponse{}, 1)
if !e.Degraded || e.ObservationRef != "" || a.Valid() {
t.Fatalf("envelope=%+v artifact=%+v", e, a)
}
}
+115
View File
@@ -25,6 +25,121 @@ var (
ErrDevelopmentCandidateConflict = errors.New("development candidate conflict")
)
func (r *WorkRepository) SaveInvestigation(ctx context.Context, value work.Investigation, expectedRevision int64) error {
if !value.Valid() || expectedRevision < 0 {
return errors.New("invalid investigation revision")
}
attempts := make([]string, 0, len(value.AttemptRefs))
for _, item := range value.AttemptRefs {
attempts = append(attempts, string(item))
}
hypotheses := make([]string, 0, len(value.HypothesisRefs))
for _, item := range value.HypothesisRefs {
hypotheses = append(hypotheses, string(item))
}
findings := make([]string, 0, len(value.FindingRefs))
for _, item := range value.FindingRefs {
findings = append(findings, string(item))
}
blockers := append([]string(nil), value.Blockers...)
checkpoints := make([]string, 0, len(value.CheckpointRefs))
for _, item := range value.CheckpointRefs {
checkpoints = append(checkpoints, string(item))
}
if expectedRevision == 0 {
result, err := r.pool.Exec(ctx, `INSERT INTO work.long_running_work(investigation_id,objective,hypothesis_refs,attempt_refs,finding_refs,blockers,checkpoint_refs,next_step,state,revision,time_budget_millis,cost_budget_micros,max_attempts,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT(investigation_id) DO NOTHING`, value.ID, value.Objective, hypotheses, attempts, findings, blockers, checkpoints, value.NextStep, value.State, value.Revision, value.TimeBudgetMillis, value.CostBudgetMicros, value.MaxAttempts, value.RecordedAtUnixMillis)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return errors.New("investigation already exists")
}
return nil
}
result, err := r.pool.Exec(ctx, `UPDATE work.long_running_work SET objective=$2,hypothesis_refs=$3,attempt_refs=$4,finding_refs=$5,blockers=$6,checkpoint_refs=$7,next_step=$8,state=$9,revision=$10,time_budget_millis=$11,cost_budget_micros=$12,max_attempts=$13,recorded_at_unix_millis=$14 WHERE investigation_id=$1 AND revision=$15`, value.ID, value.Objective, hypotheses, attempts, findings, blockers, checkpoints, value.NextStep, value.State, value.Revision, value.TimeBudgetMillis, value.CostBudgetMicros, value.MaxAttempts, value.RecordedAtUnixMillis, expectedRevision)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return errors.New("investigation revision conflict")
}
return nil
}
func (r *WorkRepository) LoadInvestigation(ctx context.Context, id work.LongRunningWorkID) (work.Investigation, error) {
var value work.Investigation
var hypotheses, attempts, findings, blockers, checkpoints []string
err := r.pool.QueryRow(ctx, `SELECT investigation_id,objective,hypothesis_refs,attempt_refs,finding_refs,blockers,checkpoint_refs,next_step,state,revision,time_budget_millis,cost_budget_micros,max_attempts,recorded_at_unix_millis FROM work.long_running_work WHERE investigation_id=$1`, id).Scan(&value.ID, &value.Objective, &hypotheses, &attempts, &findings, &blockers, &checkpoints, &value.NextStep, &value.State, &value.Revision, &value.TimeBudgetMillis, &value.CostBudgetMicros, &value.MaxAttempts, &value.RecordedAtUnixMillis)
if err != nil {
return work.Investigation{}, fmt.Errorf("load investigation: %w", err)
}
for _, item := range hypotheses {
value.HypothesisRefs = append(value.HypothesisRefs, work.EvidenceRef(item))
}
for _, item := range attempts {
value.AttemptRefs = append(value.AttemptRefs, work.AgentRunID(item))
}
for _, item := range findings {
value.FindingRefs = append(value.FindingRefs, work.EvidenceRef(item))
}
value.Blockers = append(value.Blockers, blockers...)
for _, item := range checkpoints {
value.CheckpointRefs = append(value.CheckpointRefs, work.AgentCheckpointID(item))
}
return value, nil
}
// SaveInvestigationCheckpoint atomically records the checkpoint and marks its
// attempts verified. The unique key makes retries idempotent.
func (r *WorkRepository) SaveInvestigationCheckpoint(ctx context.Context, checkpoint work.InvestigationCheckpoint) error {
if !checkpoint.Valid() {
return errors.New("invalid investigation checkpoint")
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `INSERT INTO work.investigation_checkpoints(checkpoint_id,investigation_id,completed_attempt_refs,completed_finding_refs,next_step,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(checkpoint_id) DO NOTHING`, checkpoint.ID, checkpoint.InvestigationID, stringIDs(checkpoint.CompletedAttemptRefs), stringEvidence(checkpoint.CompletedFindingRefs), checkpoint.NextStep, checkpoint.RecordedAtUnixMillis); err != nil {
return err
}
for _, attempt := range checkpoint.CompletedAttemptRefs {
if _, err = tx.Exec(ctx, `INSERT INTO work.investigation_attempts(investigation_id,attempt_ref,verified_at_unix_millis) VALUES($1,$2,$3) ON CONFLICT(investigation_id,attempt_ref) DO NOTHING`, checkpoint.InvestigationID, attempt, checkpoint.RecordedAtUnixMillis); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (r *WorkRepository) UnverifiedInvestigationAttempts(ctx context.Context, id work.LongRunningWorkID, candidates []work.AgentRunID) ([]work.AgentRunID, error) {
result := make([]work.AgentRunID, 0, len(candidates))
for _, candidate := range candidates {
var exists bool
if err := r.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM work.investigation_attempts WHERE investigation_id=$1 AND attempt_ref=$2)`, id, candidate).Scan(&exists); err != nil {
return nil, err
}
if !exists {
result = append(result, candidate)
}
}
return result, nil
}
func stringIDs(values []work.AgentRunID) []string {
result := make([]string, 0, len(values))
for _, value := range values {
result = append(result, string(value))
}
return result
}
func stringEvidence(values []work.EvidenceRef) []string {
result := make([]string, 0, len(values))
for _, value := range values {
result = append(result, string(value))
}
return result
}
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
pool, err := openPool(ctx, config)
if err != nil {
+1 -1
View File
@@ -533,7 +533,7 @@ func main() {
log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory))
}
conversationHistory = append(conversationHistory, recentUserHistory...)
gen2Context := interactiondomain.BuildGen2Context(conversationHistory)
gen2Context := interactiondomain.BuildScopedGen2Context(conversationHistory, string(session.PrincipalID))
if !creativeUserRequest && locationReply == "" {
if continuation := interactiondomain.ContinueLocalSearch(effectiveTaskMessage, conversationHistory); continuation != "" {
continuationMessage = continuation
+25 -8
View File
@@ -67,14 +67,26 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 9px; color: var(--muted); line-height: 1.45; }
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; }
.surface-panel { min-height: 0; position: relative; overflow-y: auto; }
.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; }
.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: 28px 18px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
.message.user { align-self: flex-end; background: #151e1a; }
.message.assistant { align-self: flex-start; background: #0f1613; }
.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; background: radial-gradient(circle at 50% 0%, rgba(201, 168, 95, .045), transparent 42%); }
.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: clamp(28px, 5vh, 56px) 18px 24px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; scrollbar-color: #39483f transparent; }
.message { max-width: min(720px, 92%); padding: 13px 16px 14px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; }
.message.user { align-self: flex-end; background: #18221c; border-color: #3a493d; }
.message.assistant { align-self: flex-start; background: #111a15; box-shadow: 0 8px 20px rgba(0, 0, 0, .12); }
.message-head { display: flex; align-items: baseline; gap: 9px; margin-bottom: 8px; }
.message-role { color: var(--signal); font: 600 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.message.user .message-role { color: var(--good); }
.message-time { color: #68776d; font: 9px/1 var(--mono); }
.message-content { white-space: pre-wrap; overflow-wrap: anywhere; }
.message small { display: block; margin-top: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; }
.composer { width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 10px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
.composer button { height: 48px; align-self: end; }
.composer { width: min(100%, 920px); margin: 0 auto; padding: 10px 18px 8px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; border-top: 1px solid rgba(42, 53, 47, .85); background: linear-gradient(var(--base), rgba(11, 16, 14, .98)); }
.composer-input-wrap { min-width: 0; border: 1px solid #3a493f; border-radius: 5px; background: #101813; transition: border-color .15s ease, box-shadow .15s ease; }
.composer-input-wrap:focus-within { border-color: var(--signal); box-shadow: 0 0 0 3px rgba(201, 168, 95, .12); }
.composer textarea { min-height: 48px; max-height: 220px; resize: none; border: 0; background: transparent; }
.composer-hint { display: flex; justify-content: space-between; gap: 12px; padding: 0 12px 8px; color: #68776d; font: 9px/1 var(--mono); }
.composer-hint output { color: var(--muted); white-space: nowrap; }
.composer button { height: 48px; align-self: end; display: inline-flex; align-items: center; justify-content: space-between; gap: 8px; padding-inline: 15px; }
.composer button[aria-busy="true"] { color: #4b432d; background: #ad9353; }
.composer button span:last-child { opacity: .65; font: 16px/1 var(--mono); }
.composer button:disabled { cursor: wait; filter: saturate(.3); }
.media-tools { width: min(100%, 920px); margin: 0 auto; padding: 0 18px 10px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .06em; }
.media-tools label { display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 0 8px; border: 1px solid var(--line); color: var(--ink); cursor: pointer; }
@@ -107,8 +119,9 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.empty-state.error { color: var(--bad); }
@media (max-width: 640px) {
.chat-view { height: auto; min-height: 100vh; overflow: visible; display: block; }
.composer { grid-template-columns: 1fr; }
.composer { grid-template-columns: 1fr; padding-inline: 12px; }
.composer button { width: 100%; }
.composer-hint { font-size: 8px; }
.conversation-starters { align-items: stretch; }
.conversation-starters span { width: 100%; }
.chat-header { gap: 10px; padding: 0 12px; }
@@ -117,8 +130,12 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.chat-header > .operator-link, .build-meta { display: none; }
.chat-header > #logout { flex: 0 0 38px; }
.chat-layout { height: auto; display: block; }
.chat-layout { display: flex; flex-direction: column; }
.chat-main { order: 1; }
.chat-side, .chat-main { min-height: auto; max-height: none; overflow: visible; }
.chat-left, .chat-right { border: 0; border-bottom: 1px solid var(--line); }
.chat-left { order: 2; }
.chat-right { order: 3; }
.chat-main { height: 70vh; min-height: 520px; }
.chat-left { padding-bottom: 20px; }
.chat-right { padding-top: 20px; }
+36 -1
View File
@@ -6,6 +6,8 @@ const messages = document.querySelector("#messages");
const chatForm = document.querySelector("#chat-form");
const messageInput = document.querySelector("#message");
const sendButton = document.querySelector("#send");
const sendLabel = document.querySelector("#send-label");
const messageCount = document.querySelector("#message-count");
const logoutButton = document.querySelector("#logout");
const activeFocus = document.querySelector("#active-focus");
const cognitiveStatus = document.querySelector("#cognitive-status");
@@ -24,6 +26,13 @@ let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
let localHistory = [];
let cognitiveStatusTimer = null;
function resizeComposerInput() {
if (!messageInput) return;
messageInput.style.height = "auto";
messageInput.style.height = `${Math.min(messageInput.scrollHeight, 220)}px`;
if (messageCount) messageCount.textContent = `${messageInput.value.length.toLocaleString("ru-RU")} / 16 384`;
}
class ChatApiError extends Error {
constructor(status, detail = "") {
super(detail || `chat api returned ${status}`);
@@ -95,6 +104,7 @@ function showChat() {
}
startCognitiveStatusPolling();
messageInput.focus();
resizeComposerInput();
}
function showLogin() {
@@ -106,7 +116,22 @@ function showLogin() {
function appendMessage(kind, text, meta = "", media = null) {
const node = document.createElement("div");
node.className = `message ${kind}`;
node.textContent = text;
const head = document.createElement("div");
head.className = "message-head";
const role = document.createElement("strong");
role.className = "message-role";
role.textContent = kind === "user" ? "Вы" : "Домовой";
const time = document.createElement("time");
time.className = "message-time";
const now = new Date();
time.dateTime = now.toISOString();
time.textContent = now.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"});
head.append(role, time);
node.appendChild(head);
const content = document.createElement("div");
content.className = "message-content";
content.textContent = text;
node.appendChild(content);
if (meta) {
const small = document.createElement("small");
small.textContent = meta;
@@ -279,6 +304,9 @@ async function sendChatMessage(text, options = {}) {
if (!clean) return;
if (!options.silentUser) appendMessage("user", clean);
sendButton.disabled = true;
sendButton.setAttribute("aria-busy", "true");
if (sendLabel) sendLabel.textContent = "Домовой обрабатывает";
messages.setAttribute("aria-busy", "true");
try {
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean})});
conversationID = result.conversation_id;
@@ -299,6 +327,9 @@ async function sendChatMessage(text, options = {}) {
}
} finally {
sendButton.disabled = false;
sendButton.removeAttribute("aria-busy");
if (sendLabel) sendLabel.textContent = "Отправить";
messages.removeAttribute("aria-busy");
messageInput.focus();
}
}
@@ -334,9 +365,12 @@ chatForm.addEventListener("submit", async (event) => {
const text = messageInput.value.trim();
if (!text) return;
messageInput.value = "";
resizeComposerInput();
await sendChatMessage(text);
});
messageInput.addEventListener("input", resizeComposerInput);
async function processMedia(input, path, label) {
const file = input?.files?.[0];
if (!file || !mediaStatus) return;
@@ -405,3 +439,4 @@ if (token) {
} else {
showLogin();
}
resizeComposerInput();
+5 -2
View File
@@ -57,8 +57,11 @@
<section id="chat-panel" class="surface-panel active-panel">
<div id="messages" class="messages" aria-live="polite"></div>
<form id="chat-form" class="composer">
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<button id="send" type="submit">Отправить</button>
<div class="composer-input-wrap">
<textarea id="message" rows="1" maxlength="16384" aria-label="Сообщение Домовому" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<div class="composer-hint"><span>Enter — отправить · Shift+Enter — новая строка</span><output id="message-count" aria-live="polite">0 / 16384</output></div>
</div>
<button id="send" type="submit"><span id="send-label">Отправить</span><span aria-hidden="true">↵</span></button>
</form>
<div class="media-tools" aria-label="Мультимодальные сообщения">
<label>Аудио <input id="audio-input" type="file" accept="audio/*"></label>
+212
View File
@@ -0,0 +1,212 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"time"
"git.cin.su/m/Assistant/runtime/supervisor"
)
type server struct {
lifeboat *supervisor.PersistentLifeboat
stableUpstream string
candidate string
client *http.Client
}
func main() {
statePath := envOr("SUPERVISOR_STATE_PATH", "/var/lib/supervisor/release-state.json")
stable := strings.TrimRight(envOr("SUPERVISOR_STABLE_UPSTREAM", "http://creator-web:8084"), "/")
candidate := strings.TrimRight(envOr("SUPERVISOR_CANDIDATE_UPSTREAM", "http://creator-web-candidate:8084"), "/")
store, err := supervisor.NewDurableStore(statePath)
if err != nil {
fatal(err)
}
lifeboat := supervisor.NewPersistentLifeboat(store)
if _, err := lifeboat.Snapshot(); errors.Is(err, os.ErrNotExist) {
baseline := supervisor.Release{ID: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap"), Version: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap")}
if ok, startErr := lifeboat.EstablishBaseline(baseline); startErr != nil || !ok {
fatal(fmt.Errorf("initialize baseline: %v", startErr))
}
if ok, observeErr := lifeboat.Record(supervisor.Observation{ReleaseID: baseline.ID, Ready: true, Reason: "supervisor baseline"}); observeErr != nil || !ok {
fatal(fmt.Errorf("initialize baseline observation: %v", observeErr))
}
if ok, trafficErr := lifeboat.SetTraffic(supervisor.TrafficStable, stable, candidate); trafficErr != nil || !ok {
fatal(fmt.Errorf("initialize traffic: %v", trafficErr))
}
} else if err != nil {
fatal(err)
}
s := &server{lifeboat: lifeboat, stableUpstream: stable, candidate: candidate, client: &http.Client{Timeout: 5 * time.Second}}
public := http.NewServeMux()
public.HandleFunc("GET /healthz", s.health)
public.HandleFunc("GET /readyz", s.ready)
public.HandleFunc("/", s.proxy)
control := http.NewServeMux()
control.HandleFunc("GET /v1/state", s.state)
control.HandleFunc("POST /v1/releases/start", s.start)
control.HandleFunc("POST /v1/releases/baseline", s.baseline)
control.HandleFunc("POST /v1/releases/observe", s.observe)
control.HandleFunc("POST /v1/releases/promote", s.promote)
control.HandleFunc("POST /v1/releases/rollback", s.rollback)
control.HandleFunc("POST /v1/traffic/canary", s.canary)
control.HandleFunc("POST /v1/traffic/promote", s.promoteTraffic)
control.HandleFunc("POST /v1/traffic/rollback", s.rollbackTraffic)
go func() {
if err := http.ListenAndServe(envOr("SUPERVISOR_CONTROL_ADDR", ":8095"), control); err != nil {
fatal(err)
}
}()
if err := http.ListenAndServe(envOr("LISTEN_ADDR", ":8080"), public); err != nil {
fatal(err)
}
}
func (s *server) health(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, map[string]string{"runtime": "supervisor", "status": "ok"})
}
func (s *server) ready(w http.ResponseWriter, r *http.Request) {
state, err := s.lifeboat.Snapshot()
if err != nil || state.StableUpstream == "" {
http.Error(w, "supervisor state unavailable", http.StatusServiceUnavailable)
return
}
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, state.StableUpstream+"/healthz", nil)
if err != nil {
http.Error(w, "stable upstream unavailable", http.StatusServiceUnavailable)
return
}
response, err := s.client.Do(request)
if err != nil || response.StatusCode/100 != 2 {
if response != nil {
response.Body.Close()
}
http.Error(w, "stable upstream unavailable", http.StatusServiceUnavailable)
return
}
response.Body.Close()
writeJSON(w, map[string]string{"runtime": "supervisor", "status": "ready"})
}
func (s *server) proxy(w http.ResponseWriter, r *http.Request) {
state, err := s.lifeboat.Snapshot()
if err != nil {
http.Error(w, "supervisor state unavailable", http.StatusServiceUnavailable)
return
}
target := state.StableUpstream
selected := "stable"
if state.TrafficMode == supervisor.TrafficCandidate || (state.TrafficMode == supervisor.TrafficCanaryHeader && strings.EqualFold(r.Header.Get("X-Assistant-Canary"), "true")) {
target = state.CandidateUpstream
selected = "candidate"
}
parsed, err := url.Parse(target)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
http.Error(w, "invalid upstream", http.StatusServiceUnavailable)
return
}
proxy := httputil.NewSingleHostReverseProxy(parsed)
proxy.ModifyResponse = func(response *http.Response) error {
response.Header.Set("X-Assistant-Traffic-Target", selected)
return nil
}
proxy.ErrorHandler = func(writer http.ResponseWriter, _ *http.Request, _ error) {
http.Error(writer, "upstream unavailable", http.StatusBadGateway)
}
proxy.ServeHTTP(w, r)
}
func (s *server) state(w http.ResponseWriter, _ *http.Request) {
state, err := s.lifeboat.Snapshot()
if err != nil {
http.Error(w, "state unavailable", 503)
return
}
writeJSON(w, state)
}
func (s *server) start(w http.ResponseWriter, r *http.Request) {
var release supervisor.Release
if !decode(w, r, &release) {
return
}
ok, err := s.lifeboat.Start(release)
respond(w, ok, err)
}
func (s *server) baseline(w http.ResponseWriter, r *http.Request) {
var release supervisor.Release
if !decode(w, r, &release) {
return
}
ok, err := s.lifeboat.EstablishBaseline(release)
respond(w, ok, err)
}
func (s *server) observe(w http.ResponseWriter, r *http.Request) {
var observation supervisor.Observation
if !decode(w, r, &observation) {
return
}
ok, err := s.lifeboat.Record(observation)
respond(w, ok, err)
}
func (s *server) promote(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.Promote()
respond(w, ok, err)
}
func (s *server) rollback(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.Rollback()
respond(w, ok, err)
}
func (s *server) canary(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.SetTraffic(supervisor.TrafficCanaryHeader, s.stableUpstream, s.candidate)
respond(w, ok, err)
}
func (s *server) promoteTraffic(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.Promote()
respond(w, ok, err)
}
func (s *server) rollbackTraffic(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.RollbackTraffic()
respond(w, ok, err)
}
func decode(w http.ResponseWriter, r *http.Request, value any) bool {
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(value); err != nil {
http.Error(w, "invalid control request", 400)
return false
}
return true
}
func respond(w http.ResponseWriter, ok bool, err error) {
if err != nil {
http.Error(w, "control state unavailable", 503)
return
}
if !ok {
http.Error(w, "control transition rejected", 409)
return
}
writeJSON(w, map[string]bool{"ok": true})
}
func writeJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
}
func envOr(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}
func fatal(err error) { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
+12
View File
@@ -5,6 +5,18 @@ import "git.cin.su/m/Assistant/platform/ids"
import "errors"
type ArtifactID ids.ID
type MediaArtifact struct {
ID ArtifactID
ContentType string
SizeBytes int64
ContentDigest string
SourceRef string
RecordedAtUnixMillis int64
}
func (a MediaArtifact) Valid() bool {
return a.ID != "" && a.ContentType != "" && a.SizeBytes > 0 && a.ContentDigest != "" && a.SourceRef != "" && a.RecordedAtUnixMillis > 0
}
type AgentArtifactAttestation struct {
ArtifactID ArtifactID
+11 -7
View File
@@ -15,11 +15,15 @@ const (
)
type MultimodalResponse struct {
Operation MultimodalOperation `json:"operation"`
Text string `json:"text"`
Provider string `json:"provider,omitempty"`
ObservedAt int64 `json:"observed_at_unix_millis"`
ConfidenceNote string `json:"confidence_note,omitempty"`
ContentType string `json:"content_type,omitempty"`
DataBase64 string `json:"data_base64,omitempty"`
Operation MultimodalOperation `json:"operation"`
Text string `json:"text"`
Provider string `json:"provider,omitempty"`
ObservedAt int64 `json:"observed_at_unix_millis"`
ConfidenceNote string `json:"confidence_note,omitempty"`
ContentType string `json:"content_type,omitempty"`
DataBase64 string `json:"data_base64,omitempty"`
Model string `json:"model,omitempty"`
ModelVersion string `json:"model_version,omitempty"`
ConfidenceMillis int64 `json:"confidence_millis,omitempty"`
EvidenceRef string `json:"evidence_ref,omitempty"`
}
+32
View File
@@ -0,0 +1,32 @@
package commit
type ExecutorResult string
type AmbiguityState string
const (
ExecutorAccepted ExecutorResult = "ACCEPTED"
ExecutorReportedSuccess ExecutorResult = "REPORTED_SUCCESS"
ExecutorReportedFailure ExecutorResult = "REPORTED_FAILURE"
ExecutorNoResponse ExecutorResult = "NO_RESPONSE"
ExecutorAmbiguous ExecutorResult = "AMBIGUOUS"
)
type ExecutionReceipt struct {
ID string
ActionAttemptRef ActionAttemptID
ExecutorImplementationRef string
ExternalOperationRef string
ExternalRequestID string
RequestDigest string
ResponseDigest string
DispatchAtUnixMillis int64
CompletedAtUnixMillis *int64
ExecutorResult ExecutorResult
Ambiguity AmbiguityState
ReconciliationKey string
RawArtifactRef string
}
func (r ExecutionReceipt) Valid() bool {
return r.ID != "" && r.ActionAttemptRef != "" && r.ExecutorImplementationRef != "" && r.RequestDigest != "" && r.ExecutorResult != "" && r.ReconciliationKey != "" && r.DispatchAtUnixMillis > 0
}
+3
View File
@@ -81,6 +81,9 @@ type BootstrapConfig struct {
RecoveryLocator string
}
// ConversationalConfigurationCommand is an intent, not an assignment. Only
// Configuration may turn a SET command into durable state.
func (d SettingDefinition) Validate() error {
if d.ID == "" || d.OwnerNamespace == "" || d.StableKey == "" || !ids.Revision(d.Revision).Valid() || d.ValueType == "" || d.Lifecycle == "" || d.RecordedAtUnixMillis < 1 {
return fmt.Errorf("invalid setting definition")
+30
View File
@@ -0,0 +1,30 @@
package epistemics
type WorldOutcomeState string
const (
OutcomeVerifiedSuccess WorldOutcomeState = "VERIFIED_SUCCESS"
OutcomeVerifiedPartial WorldOutcomeState = "VERIFIED_PARTIAL"
OutcomeVerifiedFailure WorldOutcomeState = "VERIFIED_FAILURE"
OutcomeUnverified WorldOutcomeState = "UNVERIFIED"
OutcomeUnknownState WorldOutcomeState = "UNKNOWN"
OutcomeConflicted WorldOutcomeState = "CONFLICTED"
)
type WorldOutcomeAssessment struct {
ID string
PrincipalID string
ActionAttemptRef string
ExecutionReceiptRefs []string
ExpectedEffectRef string
ObservationRefs []ObservationID
ClaimRefs []ClaimID
State WorldOutcomeState
ConfidenceMillis int64
EvidenceRefs []EvidenceRef
AssessedAt int64
}
func (a WorldOutcomeAssessment) Valid() bool {
return a.ID != "" && a.PrincipalID != "" && a.ActionAttemptRef != "" && a.State != "" && a.ConfidenceMillis >= 0 && a.ConfidenceMillis <= 1000 && a.AssessedAt > 0
}
+34 -15
View File
@@ -17,23 +17,27 @@ type CoverageState string
type ConflictState string
type EvidenceKind string
type ResearchFindingState string
type ReliabilityState string
const (
FreshnessCurrent FreshnessState = "CURRENT"
FreshnessStale FreshnessState = "STALE"
FreshnessUnknown FreshnessState = "UNKNOWN"
CoverageComplete CoverageState = "COMPLETE"
CoveragePartial CoverageState = "PARTIAL"
CoverageUnknown CoverageState = "UNKNOWN"
ConflictNone ConflictState = "NONE"
ConflictPresent ConflictState = "PRESENT"
ConflictUnknown ConflictState = "UNKNOWN"
EvidenceObserved EvidenceKind = "OBSERVED"
EvidenceAssumed EvidenceKind = "ASSUMED"
FindingSupported ResearchFindingState = "SUPPORTED"
FindingStale ResearchFindingState = "STALE"
FindingConflicted ResearchFindingState = "CONFLICTED"
FindingUnknown ResearchFindingState = "UNKNOWN"
FreshnessCurrent FreshnessState = "CURRENT"
FreshnessStale FreshnessState = "STALE"
FreshnessUnknown FreshnessState = "UNKNOWN"
CoverageComplete CoverageState = "COMPLETE"
CoveragePartial CoverageState = "PARTIAL"
CoverageUnknown CoverageState = "UNKNOWN"
ConflictNone ConflictState = "NONE"
ConflictPresent ConflictState = "PRESENT"
ConflictUnknown ConflictState = "UNKNOWN"
EvidenceObserved EvidenceKind = "OBSERVED"
EvidenceAssumed EvidenceKind = "ASSUMED"
FindingSupported ResearchFindingState = "SUPPORTED"
FindingStale ResearchFindingState = "STALE"
FindingConflicted ResearchFindingState = "CONFLICTED"
FindingUnknown ResearchFindingState = "UNKNOWN"
ReliabilityCurrent ReliabilityState = "CURRENT"
ReliabilityAging ReliabilityState = "AGING"
ReliabilityUnknown ReliabilityState = "UNKNOWN"
)
type ResearchPlan struct {
@@ -52,6 +56,21 @@ type Source struct {
Publisher string
}
type SourceReliabilityAssessment struct {
SourceRef string
DomainRef string
Freshness FreshnessState
HistoryScoreMillis int64
ReliabilityScoreMillis int64
State ReliabilityState
EvidenceRefs []string
AssessedAtUnixMillis int64
}
func (a SourceReliabilityAssessment) Valid() bool {
return a.SourceRef != "" && a.DomainRef != "" && a.Freshness != "" && a.HistoryScoreMillis >= 0 && a.HistoryScoreMillis <= 1000 && a.ReliabilityScoreMillis >= 0 && a.ReliabilityScoreMillis <= 1000 && a.State != "" && a.AssessedAtUnixMillis > 0
}
type SourceSnapshot struct {
ID SourceSnapshotID
SourceID SourceID
+24
View File
@@ -6,6 +6,30 @@ type ActorID ids.ID
type PrincipalID ids.ID
type IdentityID ids.ID
type DataRealmKind string
const (
DataRealmPersonal DataRealmKind = "PERSONAL"
DataRealmShared DataRealmKind = "SHARED"
DataRealmPublic DataRealmKind = "PUBLIC"
)
// PrincipalDataRealm is an identity-owned access boundary. It does not make
// a speaker or World person referent an authenticated principal.
type PrincipalDataRealm struct {
ID string
OwnerPrincipalID PrincipalID
Kind DataRealmKind
MemberPrincipalIDs []PrincipalID
Shared bool
Revision int64
RecordedAtUnixMillis int64
}
func (r PrincipalDataRealm) Valid() bool {
return r.ID != "" && r.OwnerPrincipalID != "" && (r.Kind == DataRealmPersonal || r.Kind == DataRealmShared || r.Kind == DataRealmPublic) && r.Revision > 0 && r.RecordedAtUnixMillis > 0 && (r.Kind != DataRealmShared || r.Shared)
}
// PrincipalBinding keeps the operational actor separate from the interest on
// whose behalf it acts. A Creator is not exempt from this distinction.
type PrincipalBinding struct {
+52
View File
@@ -25,6 +25,44 @@ type SemanticApplicability string
type SemanticCandidateSource string
type SemanticCandidateSelectionState string
type InformationRequestKind string
type ConfigurationCommandKind string
type SpeakerAttribution struct {
SourceRef string
PrincipalCandidate string
ConfidenceMillis int64
Authenticated bool
ObservationRef string
}
type IngressModality string
const (
IngressText IngressModality = "TEXT"
IngressAudio IngressModality = "AUDIO"
IngressImage IngressModality = "IMAGE"
IngressVideo IngressModality = "VIDEO"
IngressDevice IngressModality = "DEVICE"
)
type IngressEnvelope struct {
ID string
Modality IngressModality
SourceRef string
DeviceRef string
LocationRef string
PrincipalID string
ArtifactRef string
ObservationRef string
ProviderRef string
ModelRef string
ModelVersion string
ConfidenceMillis int64
Degraded bool
RecordedAtUnixMillis int64
}
func (e IngressEnvelope) Valid() bool {
return e.ID != "" && e.Modality != "" && e.SourceRef != "" && e.ConfidenceMillis >= 0 && e.ConfidenceMillis <= 1000 && e.RecordedAtUnixMillis > 0 && (!e.Degraded || e.ObservationRef == "")
}
const (
ChannelWebChat Channel = "WEB_CHAT"
@@ -75,6 +113,8 @@ const (
InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY"
InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE"
InformationRequestResearchFailureExplanation InformationRequestKind = "RESEARCH_FAILURE_EXPLANATION"
ConfigurationCommandSet ConfigurationCommandKind = "SET"
ConfigurationCommandQuery ConfigurationCommandKind = "QUERY"
)
type Conversation struct {
@@ -106,6 +146,18 @@ type OriginalHumanIntent struct {
RecordedAtUnixMillis int64
Text string
}
// ConfigurationCommand is Interaction-owned intent. Configuration remains
// the only owner allowed to materialise a SettingAssignment.
type ConfigurationCommand struct {
Kind ConfigurationCommandKind
PrincipalID string
DefinitionRef string
TypedValue string
TemporaryOverride bool
SessionRef string
SourceRef string
}
type InteractionRelation struct {
ID InteractionRelationID
FromUtteranceID HumanUtteranceID
+39
View File
@@ -0,0 +1,39 @@
package work
type EvolutionRunStage string
const (
EvolutionNeedRecorded EvolutionRunStage = "IMPROVEMENT_NEED"
EvolutionCandidateBuilt EvolutionRunStage = "CANDIDATE"
EvolutionIndependentlyEvaluated EvolutionRunStage = "INDEPENDENT_EVALUATION"
EvolutionSandboxed EvolutionRunStage = "SANDBOX_BUILD"
EvolutionBenchmarked EvolutionRunStage = "BENCHMARK"
EvolutionCanaried EvolutionRunStage = "CANARY"
EvolutionPromoted EvolutionRunStage = "PROMOTION"
EvolutionRolledBack EvolutionRunStage = "ROLLBACK"
)
type GovernedEvolutionResult struct {
NeedRef string
CandidateRef string
EvaluationRef string
Stages []EvolutionRunStage
EvidenceRefs []EvidenceRef
LearningLedgerOutcomeRef string
Promoted bool
RollbackRef string
}
// GovernedEvolutionOps are explicit gates. The caller supplies existing
// development executor/supervisor operations; this interface grants no new
// authority and has no autonomous promotion implementation.
type GovernedEvolutionOps struct {
CreateCandidate func() (EvolutionCandidate, error)
Evaluate func(EvolutionCandidate) (EvolutionEvaluation, error)
SandboxBuild func(EvolutionCandidate) (EvidenceRef, error)
Benchmark func(EvolutionCandidate) (EvidenceRef, bool, error)
Canary func(EvolutionCandidate) (EvidenceRef, bool, error)
Promote func(EvolutionCandidate, EvolutionEvaluation) (string, error)
Rollback func(EvolutionCandidate) (string, error)
LearningLedgerOutcome func(GovernedEvolutionResult) (string, error)
}
+16
View File
@@ -30,6 +30,22 @@ type Investigation struct {
RecordedAtUnixMillis int64
}
// InvestigationCheckpoint is the durable boundary between attempts. A
// checkpoint is written only after an attempt has been verified, so a worker
// restart can resume at NextStep without replaying that attempt.
type InvestigationCheckpoint struct {
ID AgentCheckpointID
InvestigationID LongRunningWorkID
CompletedAttemptRefs []AgentRunID
CompletedFindingRefs []EvidenceRef
NextStep string
RecordedAtUnixMillis int64
}
func (c InvestigationCheckpoint) Valid() bool {
return c.ID != "" && c.InvestigationID != "" && c.NextStep != "" && c.RecordedAtUnixMillis > 0
}
func (w Investigation) Valid() bool {
return w.ID != "" && w.Objective != "" && w.State != "" && w.Revision > 0 && w.TimeBudgetMillis >= 0 && w.CostBudgetMicros >= 0 && w.MaxAttempts >= 0 && w.RecordedAtUnixMillis > 0
}
+7 -1
View File
@@ -4,7 +4,10 @@ type PerspectiveID string
type PerspectiveSpec struct {
ID PerspectiveID
ObjectiveLens string
FocusRef string
RetrievalProfileRef string
SkillRefs []string
ToolRefs []string
EvaluationCriteria []string
CapabilityRequirements []string
ExpectedOutputShape string
@@ -13,6 +16,9 @@ type PerspectiveSpec struct {
type SpecialistAssignment struct {
Perspective PerspectiveSpec
SubtaskRef string
FocusRef string
SkillRefs []string
ToolRefs []string
EvidenceRefs []EvidenceRef
}
type TeamDecision struct {
@@ -22,5 +28,5 @@ type TeamDecision struct {
}
func (p PerspectiveSpec) Valid() bool {
return p.ID != "" && p.ObjectiveLens != "" && len(p.EvaluationCriteria) > 0 && p.ExpectedOutputShape != ""
return p.ID != "" && p.ObjectiveLens != "" && p.FocusRef != "" && len(p.SkillRefs) > 0 && len(p.ToolRefs) > 0 && len(p.EvaluationCriteria) > 0 && p.ExpectedOutputShape != ""
}
+36
View File
@@ -0,0 +1,36 @@
package work
type EvolutionChangeLevel string
const (
EvolutionKnowledge EvolutionChangeLevel = "KNOWLEDGE"
EvolutionBehavior EvolutionChangeLevel = "BEHAVIOR"
EvolutionCapability EvolutionChangeLevel = "CAPABILITY"
EvolutionCore EvolutionChangeLevel = "CORE"
)
type EvolutionCandidate struct {
ID string
ProposalRef string
ChangeLevel EvolutionChangeLevel
CreatorIdentity string
BaseVersionRef string
CandidateVersionRef string
MustNotWorsen []string
SandboxEvidenceRefs []EvidenceRef
BenchmarkEvidenceRefs []EvidenceRef
RollbackRef string
State string
}
type EvolutionEvaluation struct {
CandidateRef string
EvaluatorIdentity string
Approved bool
EvidenceRefs []EvidenceRef
MustNotWorsenPassed bool
RecordedAtUnixMillis int64
}
func (e EvolutionEvaluation) ValidFor(c EvolutionCandidate) bool {
return e.CandidateRef == c.ID && e.EvaluatorIdentity != "" && e.EvaluatorIdentity != c.CreatorIdentity && e.Approved && e.MustNotWorsenPassed && len(e.EvidenceRefs) > 0 && e.RecordedAtUnixMillis > 0
}
+10
View File
@@ -104,6 +104,16 @@ type ValueTypeSpec struct {
type WorldObjectID ids.StableID
type KindDefinitionID ids.StableID
type WorldAliasID ids.StableID
type PersonReferent struct {
WorldObjectRef SemanticReference
DisplayName string
ConfidenceMillis int64
}
func (p PersonReferent) Valid() bool {
return p.WorldObjectRef.Valid() && p.DisplayName != "" && p.ConfidenceMillis >= 0 && p.ConfidenceMillis <= 1000
}
type FacetDefinitionID ids.StableID
type RelationDefinitionID ids.StableID
type ResolutionDecisionID ids.StableID
@@ -0,0 +1,58 @@
-- Gen2 owner-role provisioning for the dedicated bootstrap/migration principal.
-- This is intentionally separate from feature migrations and may be run repeatedly.
BEGIN;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'world_role') THEN
CREATE ROLE world_role LOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'configuration_role') THEN
CREATE ROLE configuration_role LOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'self_role') THEN
CREATE ROLE self_role LOGIN;
END IF;
END
$$;
DO $$
DECLARE
role_name TEXT;
can_login BOOLEAN;
BEGIN
FOREACH role_name IN ARRAY ARRAY['world_role', 'configuration_role', 'self_role'] LOOP
SELECT rolcanlogin INTO can_login FROM pg_roles WHERE rolname = role_name;
IF can_login IS DISTINCT FROM TRUE THEN
RAISE EXCEPTION 'Gen2 owner role % exists but is not a V1-compatible LOGIN role', role_name;
END IF;
END LOOP;
END
$$;
CREATE SCHEMA IF NOT EXISTS world AUTHORIZATION world_role;
CREATE SCHEMA IF NOT EXISTS configuration AUTHORIZATION configuration_role;
CREATE SCHEMA IF NOT EXISTS self AUTHORIZATION self_role;
REVOKE ALL ON SCHEMA world, configuration, self FROM PUBLIC;
DO $$
DECLARE
schema_name TEXT;
expected_owner TEXT;
actual_owner TEXT;
BEGIN
FOREACH schema_name IN ARRAY ARRAY['world', 'configuration', 'self'] LOOP
expected_owner := schema_name || '_role';
SELECT pg_get_userbyid(nspowner)
INTO actual_owner
FROM pg_namespace
WHERE nspname = schema_name;
IF actual_owner IS DISTINCT FROM expected_owner THEN
RAISE EXCEPTION 'Gen2 schema % is owned by %, expected %', schema_name, actual_owner, expected_owner;
END IF;
END LOOP;
END
$$;
COMMIT;
@@ -0,0 +1,22 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.long_running_work (
investigation_id TEXT PRIMARY KEY, objective TEXT NOT NULL,
hypothesis_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], attempt_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
finding_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], blockers TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
checkpoint_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], next_step TEXT NOT NULL, state TEXT NOT NULL,
revision BIGINT NOT NULL, time_budget_millis BIGINT NOT NULL, cost_budget_micros BIGINT NOT NULL,
max_attempts INTEGER NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (investigation_id <> ''), CHECK (objective <> ''), CHECK (next_step <> ''), CHECK (revision > 0), CHECK (time_budget_millis >= 0), CHECK (cost_budget_micros >= 0), CHECK (max_attempts >= 0), CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.investigation_checkpoints (
checkpoint_id TEXT PRIMARY KEY, investigation_id TEXT NOT NULL, completed_attempt_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
completed_finding_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], next_step TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (checkpoint_id <> ''), CHECK (investigation_id <> ''), CHECK (next_step <> ''), CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.investigation_attempts (
investigation_id TEXT NOT NULL, attempt_ref TEXT NOT NULL, verified_at_unix_millis BIGINT NOT NULL,
PRIMARY KEY (investigation_id, attempt_ref), CHECK (investigation_id <> ''), CHECK (attempt_ref <> ''), CHECK (verified_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,11 @@
BEGIN;
SET ROLE identity_role;
CREATE TABLE IF NOT EXISTS identity.principal_data_realms (
realm_id TEXT PRIMARY KEY, owner_principal_id TEXT NOT NULL, realm_kind TEXT NOT NULL,
member_principal_ids TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], shared BOOLEAN NOT NULL DEFAULT FALSE,
revision BIGINT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (realm_id <> ''), CHECK (owner_principal_id <> ''), CHECK (realm_kind IN ('PERSONAL','SHARED','PUBLIC')),
CHECK (realm_kind <> 'SHARED' OR shared), CHECK (revision > 0), CHECK (recorded_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
+10
View File
@@ -25,6 +25,16 @@ psql_exec() {
docker exec -i "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_SUPERUSER" -d "$DATABASE" "$@"
}
# Owner roles and schemas are provisioned by the dedicated bootstrap boundary,
# before feature migrations run. This keeps role creation out of migrations and
# makes existing PostgreSQL volumes converge idempotently as well.
GEN2_BOOTSTRAP="$REPO_DIR/database/bootstrap/002_gen2_roles_and_schemas.sql"
if [[ ! -f "$GEN2_BOOTSTRAP" ]]; then
echo "migration runner: Gen2 bootstrap is missing" >&2
exit 1
fi
psql_exec < "$GEN2_BOOTSTRAP"
psql_exec -c "CREATE TABLE IF NOT EXISTS public.assistant_migration_history (migration_id TEXT PRIMARY KEY, applied_at_unix_millis BIGINT NOT NULL CHECK (applied_at_unix_millis > 0));"
if [[ -n "${MIGRATION_BASELINE:-}" ]]; then
+31 -2
View File
@@ -277,7 +277,7 @@ services:
- --port
- "8086"
- --model
- /models/Qwen3-8B-Q4_K_M.gguf
- /models/Qwen3.8-27B-UD-Q2_K_XL.gguf
- --alias
- domovoy-lifeboat
- --jinja
@@ -299,6 +299,29 @@ services:
timeout: 3s
retries: 10
supervisor:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: supervisor, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
environment:
LISTEN_ADDR: ":8080"
SUPERVISOR_CONTROL_ADDR: ":8095"
SUPERVISOR_STATE_PATH: /var/lib/supervisor/release-state.json
SUPERVISOR_STABLE_UPSTREAM: http://creator-web:8084
SUPERVISOR_CANDIDATE_UPSTREAM: http://creator-web-candidate:8084
SUPERVISOR_BASELINE_RELEASE: ${ASSISTANT_SOURCE_REVISION:-unknown}
ports:
- "${CREATOR_WEB_PORT:-18080}:8080"
- "127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}:8095"
depends_on:
creator-web: { condition: service_healthy }
volumes:
- assistant_supervisor_state:/var/lib/supervisor
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
interval: 10s
timeout: 5s
retries: 20
creator-web:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
@@ -310,7 +333,6 @@ services:
COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083
AGENT_WORKER_URL: http://agent-worker:8090
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
@@ -318,6 +340,12 @@ services:
timeout: 3s
retries: 10
creator-web-candidate:
profiles: ["assistant-canary"]
extends:
service: creator-web
restart: "no"
volumes:
assistant_pg:
assistant_nats:
@@ -333,6 +361,7 @@ volumes:
external: true
assistant_research_sources:
name: assistant_research_sources
assistant_supervisor_state:
networks:
llm-local:
+1
View File
@@ -4,5 +4,6 @@ LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
COPY database/bootstrap/001_roles_and_schemas.sql /docker-entrypoint-initdb.d/001_roles_and_schemas.sql
COPY database/bootstrap/002_gen2_roles_and_schemas.sql /docker-entrypoint-initdb.d/002_gen2_roles_and_schemas.sql
COPY deploy/images/002_owner_passwords.sh /docker-entrypoint-initdb.d/002_owner_passwords.sh
RUN chmod 0555 /docker-entrypoint-initdb.d/002_owner_passwords.sh
+1
View File
@@ -11,6 +11,7 @@ ARG SOURCE_REVISION=unknown
LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
RUN addgroup -S assistant && adduser -S -G assistant assistant
RUN mkdir -p /var/lib/supervisor && chown assistant:assistant /var/lib/supervisor
USER assistant
COPY --from=build /out/runtime /usr/local/bin/assistant-runtime
ENTRYPOINT ["/usr/local/bin/assistant-runtime"]
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
COMPOSE_FILE="$BASE_DIR/deploy/docker-compose.prod.yml"
SUPERVISOR_CONTROL_URL="${SUPERVISOR_CONTROL_URL:-http://127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}}"
REVISION="${1:?candidate revision is required}"
set -a
. "$ENV_FILE"
set +a
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
control() { curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL$1" "${@:2}" >/dev/null; }
fail_rollback() {
control /v1/traffic/rollback || true
control /v1/releases/rollback || true
echo "production canary failed; stable traffic rollback requested" >&2
exit 1
}
compose --profile assistant-canary build --build-arg "SOURCE_REVISION=$REVISION" creator-web-candidate
compose --profile assistant-canary up -d --no-build creator-web-candidate
for attempt in $(seq 1 30); do
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' assistant-creator-web-candidate-1 2>/dev/null || true)"
[[ "$status" == healthy ]] && break
[[ "$attempt" == 30 ]] && fail_rollback
sleep 2
done
control /v1/releases/baseline -H 'Content-Type: application/json' --data "{\"id\":\"stable-$REVISION\",\"version\":\"$REVISION\"}" || fail_rollback
control /v1/releases/start -H 'Content-Type: application/json' --data "{\"id\":\"candidate-$REVISION\",\"version\":\"$REVISION\"}" || fail_rollback
control /v1/releases/observe -H 'Content-Type: application/json' --data "{\"release_id\":\"candidate-$REVISION\",\"ready\":true,\"reason\":\"candidate healthcheck passed\",\"observed_at_unix_millis\":$(date +%s%3N)}" || fail_rollback
control /v1/traffic/canary || fail_rollback
headers="$(mktemp)"
trap 'rm -f "$headers"' EXIT
if ! curl -fsS --max-time 10 -D "$headers" -H 'X-Assistant-Canary: true' "https://assistant.cin.su/login" -o /dev/null; then fail_rollback; fi
grep -qi '^X-Assistant-Traffic-Target: candidate' "$headers" || fail_rollback
control /v1/releases/promote || fail_rollback
echo "production canary promoted revision=$REVISION"
+1 -1
View File
@@ -1 +1 @@
{"task_file":"dev/tasks/GEN2-019-add-consequence-risk-assessment-without-artificial-capability-blacklist.json","task_id":"GEN2-019","task_revision":1}
{"task_file":"dev/tasks/GEN2-027-production-canary-and-controlled-gen2-traffic-transition.json","task_id":"GEN2-027","task_revision":1}
@@ -0,0 +1,42 @@
{
"task_id": "GEN2-DB-001",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Исправить provisioning Gen2 owner roles/schemas до применения migrations 046-050 без ослабления V1 DB isolation.",
"requirement_ids": ["REQ-GEN2-DB-001-ROLES", "REQ-GEN2-DB-001-OWNERSHIP", "REQ-GEN2-DB-001-IDEMPOTENCY", "REQ-GEN2-DB-001-RECOVERY"],
"allowed_change_scope": {
"modules": ["database bootstrap", "deployment migration runner"],
"paths": [
"database/bootstrap/002_gen2_roles_and_schemas.sql",
"deploy/apply-migrations.sh",
"deploy/images/postgres.Dockerfile",
"dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json"
],
},
"forbidden_change_scope": {
"paths": [
"GEN2-028",
"production-canary-before-provisioning",
"feature-migration-role-creation",
"cross-owner-grants",
"new canonical owner",
"production canary before provisioning"
]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["V1 database ownership/ACL", "GEN2 canonical owner registry", "GEN2 execution contract"], "may_change": ["Bootstrap provisioning only; feature migration semantics are unchanged."]},
"semantic_owner": "Dedicated PostgreSQL bootstrap/migration principal; semantic ownership remains World, Configuration, SELF and existing V1 owners.",
"canonical_state": ["world schema owned by world_role", "configuration schema owned by configuration_role", "self schema owned by self_role", "no cross-owner canonical writes"],
"state_transitions": ["missing Gen2 roles/schemas -> idempotently provisioned -> migrations 046-050 apply -> restart/recovery preserves schema and migration state"],
"side_effects_and_privileges": ["Create only the three declared LOGIN owner roles when absent.", "Revoke PUBLIC schema privileges and fail closed on an unexpected schema owner.", "Do not invent or print credentials; do not grant cross-owner access."],
"persistence_and_migrations": ["Provisioning runs in the dedicated bootstrap boundary before feature migrations.", "Migrations 046-050 remain unchanged and are applied by the existing migration history runner."],
"failure_retry_restart": ["Provisioning is safe to run repeatedly.", "Unexpected existing role/schema ownership fails closed and requires an architecture decision.", "PostgreSQL restart/recovery must retain owner roles, schemas, ACL and migration history."],
"assumptions": ["This is a user-authorized remediation task for the previously stopped GEN2-027 migration decision.", "No Gen2 runtime consumer currently requires a new database password; credentials are not invented by this task.", "GEN2-027 resumes only after provisioning and recovery evidence; GEN2-028 is not started."],
"acceptance_scenarios": ["GEN2-DB-001-SCN-01: all roles required by migrations 046-050 are identified", "GEN2-DB-001-SCN-02: provisioning succeeds twice without duplicate objects", "GEN2-DB-001-SCN-03: migrations 046-050 apply in the controlled deployment path", "GEN2-DB-001-SCN-04: schema owners and PUBLIC ACL are confirmed", "GEN2-DB-001-SCN-05: PostgreSQL restart/recovery preserves the result", "GEN2-DB-001-EXIT: migration blocker is removed without weakening V1 isolation"],
"required_tests": ["FAST governance: go run ./cmd/devctl preflight -task dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json", "FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "FAST whitespace: git diff --check gen2-integration --", "TARGETED: provisioning repeated twice, migrations 046-050, ACL/ownership verification and PostgreSQL restart/recovery"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,36 @@
{
"task_id": "GEN2-DEPLOY-001",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Развернуть независимый Supervisor control plane с durable release state, controlled traffic switching и доказуемым rollback перед GEN2-027.",
"requirement_ids": ["REQ-GEN2-DEPLOY-001-SUPERVISOR", "REQ-GEN2-DEPLOY-001-TRAFFIC", "REQ-GEN2-DEPLOY-001-ROLLBACK", "REQ-GEN2-DEPLOY-001-LIFEBOAT"],
"allowed_change_scope": {
"modules": ["deployment control", "runtime supervisor", "runtime health"],
"paths": [
"deploy/**",
"runtime/supervisor/**",
"runtime/health/**",
"cmd/supervisor/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-DEPLOY-001-supervisor-and-production-rollback.json",
"docs/evidence/**"
]
},
"forbidden_change_scope": {
"paths": ["GEN2-028", "database/**", "security/**", "architecture/**", "Authority/Commit bypass", "cognitive-lifeboat as Supervisor"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["GEN2-027 production canary contract", "runtime/supervisor durable state", "existing deployment update controller"], "may_change": ["deployment topology, Supervisor HTTP control contract and traffic routing only"]},
"semantic_owner": "Operations/Supervisor; not a semantic owner and not a cognitive provider.",
"canonical_state": ["durable release/current/last-known-good evidence owned by Supervisor control plane", "traffic target is Supervisor-owned operational state"],
"state_transitions": ["baseline -> candidate -> observed healthy -> header canary -> promoted OR independent rollback to last-known-good"],
"side_effects_and_privileges": ["Control API is bound only to the production-internal loopback port.", "Supervisor has no database, Authority or Commit credentials.", "Candidate traffic is explicit and rollback remains available."],
"persistence_and_migrations": ["No database changes; durable state is stored in a dedicated named volume outside candidate containers."],
"failure_retry_restart": ["Supervisor restart reloads release and traffic state from its volume.", "Candidate failure must not erase last-known-good or block stable traffic rollback.", "Lifeboat model failure remains a separately reported degraded provider condition."],
"assumptions": ["GEN2-027 resumes only after this task's production evidence; GEN2-028 is not started.", "The existing public ingress forwards to the configured creator web host port."],
"acceptance_scenarios": ["GEN2-DEPLOY-001-SCN-01: Supervisor is a separate healthy production container", "GEN2-DEPLOY-001-SCN-02: release state survives Supervisor restart", "GEN2-DEPLOY-001-SCN-03: canary header reaches candidate while default traffic stays stable", "GEN2-DEPLOY-001-SCN-04: candidate failure rolls traffic back to last-known-good", "GEN2-DEPLOY-001-SCN-05: cognitive lifeboat diagnosis and degraded role are explicit", "GEN2-DEPLOY-001-EXIT: GEN2-027 rollback prerequisites are proven"],
"required_tests": ["FAST: go test ./runtime/supervisor/... ./runtime/health/... ./cmd/supervisor/...", "FAST: go vet ./runtime/supervisor/... ./runtime/health/... ./cmd/supervisor/...", "FAST governance: preflight", "FAST scope: check-diff", "FAST independent: independent", "FAST whitespace: git diff --check", "TARGETED recovery: Supervisor restart, canary and rollback on docker.cin.su"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,44 @@
{
"evidence_id": "gen2-deploy-001-production-recovery-2026-08-29",
"task_id": "GEN2-DEPLOY-001",
"production_revision_before": "36fc8ec",
"production_revision_after": "2083096aedaec73191e0bc9e2566f596e1810d82",
"topology": {
"supervisor": "assistant-supervisor-1",
"public_traffic": "Supervisor reverse proxy on creator web host port",
"control_api": "Supervisor loopback-only host control port",
"durable_state": "assistant_assistant_supervisor_state:/var/lib/supervisor/release-state.json",
"candidate": "assistant-creator-web-candidate-1"
},
"canary": {
"candidate_health": "healthy",
"header_route": "X-Assistant-Canary=true -> candidate",
"header_route_http": 200,
"promoted": true
},
"rollback": {
"drill": "candidate container stopped, Supervisor traffic rollback and release rollback invoked",
"stable_http": 200,
"stable_target_header": "stable",
"supervisor_restart": "healthy; traffic_mode=STABLE and stable release state restored"
},
"postgres_recovery": {
"restart": "healthy",
"public_healthz": 200,
"public_readyz": 200
},
"cognitive_lifeboat": {
"previous_status": "restart loop/unhealthy",
"cause": "configured Qwen3-8B-Q4_K_M.gguf was absent from llm-local-models volume",
"remediation": "compose model reference changed to existing Qwen3.8-27B-UD-Q2_K_XL.gguf",
"current_status": "healthy",
"role": "local degraded cognitive provider; not Supervisor"
},
"smoke": {
"public_healthz": 200,
"public_readyz": 200,
"public_login": 200,
"authenticated_household": "UNVERIFIED: browser session was unauthenticated; no credentials were read or entered"
},
"interpretation": "Supervisor independence, durable release state, controlled header canary, stable rollback and restart recovery are production-observed. Authenticated household acceptance remains an explicit gap."
}
+34 -4
View File
@@ -2,10 +2,40 @@ package domain
import (
"fmt"
"sort"
"git.cin.su/m/Assistant/contracts/configuration"
"sort"
)
func Resolve(definition configuration.SettingDefinition, assignments []configuration.SettingAssignment, selector configuration.ScopeSelector) (configuration.EffectiveSetting,error){if err:=definition.Validate();err!=nil{return configuration.EffectiveSetting{},err}; var matches []configuration.SettingAssignment;for _,a:=range assignments{if err:=a.Validate();err!=nil{return configuration.EffectiveSetting{},err};if a.DefinitionRef==string(definition.ID)&&a.Lifecycle==configuration.SettingActive&&a.Selector.PrincipalRef==selector.PrincipalRef&&a.Selector.SkillRef==selector.SkillRef{matches=append(matches,a)}};if len(matches)==0{return configuration.EffectiveSetting{DefinitionRef:string(definition.ID),Value:definition.DefaultValue,ResolutionTrace:[]string{"system-default"}},nil};sort.SliceStable(matches,func(i,j int)bool{return specificity(matches[i].Selector)>specificity(matches[j].Selector)});if len(matches)>1&&specificity(matches[0].Selector)==specificity(matches[1].Selector){return configuration.EffectiveSetting{},fmt.Errorf("configuration conflict")};return configuration.EffectiveSetting{DefinitionRef:string(definition.ID),Value:matches[0].TypedValue,WinningAssignmentRef:string(matches[0].ID),ResolutionTrace:[]string{"specificity-selected"}},nil}
func specificity(s configuration.ScopeSelector)int{return len(s.RequiredContextBindings)+boolInt(s.SkillRef!="")+boolInt(s.TaskRef!="")+boolInt(s.SessionRef!="")+boolInt(s.TemporaryOverride)}
func boolInt(v bool)int{if v{return 1};return 0}
var ErrInvalidConfigurationCommand = fmt.Errorf("invalid conversational configuration command")
func Resolve(definition configuration.SettingDefinition, assignments []configuration.SettingAssignment, selector configuration.ScopeSelector) (configuration.EffectiveSetting, error) {
if err := definition.Validate(); err != nil {
return configuration.EffectiveSetting{}, err
}
var matches []configuration.SettingAssignment
for _, a := range assignments {
if err := a.Validate(); err != nil {
return configuration.EffectiveSetting{}, err
}
if a.DefinitionRef == string(definition.ID) && a.Lifecycle == configuration.SettingActive && a.Selector.PrincipalRef == selector.PrincipalRef && a.Selector.SkillRef == selector.SkillRef {
matches = append(matches, a)
}
}
if len(matches) == 0 {
return configuration.EffectiveSetting{DefinitionRef: string(definition.ID), Value: definition.DefaultValue, ResolutionTrace: []string{"system-default"}}, nil
}
sort.SliceStable(matches, func(i, j int) bool { return specificity(matches[i].Selector) > specificity(matches[j].Selector) })
if len(matches) > 1 && specificity(matches[0].Selector) == specificity(matches[1].Selector) {
return configuration.EffectiveSetting{}, fmt.Errorf("configuration conflict")
}
return configuration.EffectiveSetting{DefinitionRef: string(definition.ID), Value: matches[0].TypedValue, WinningAssignmentRef: string(matches[0].ID), ResolutionTrace: []string{"specificity-selected"}}, nil
}
func specificity(s configuration.ScopeSelector) int {
return len(s.RequiredContextBindings) + boolInt(s.SkillRef != "") + boolInt(s.TaskRef != "") + boolInt(s.SessionRef != "") + boolInt(s.TemporaryOverride)
}
func boolInt(v bool) int {
if v {
return 1
}
return 0
}
@@ -0,0 +1,32 @@
package domain
import (
"fmt"
"git.cin.su/m/Assistant/contracts/configuration"
"git.cin.su/m/Assistant/platform/ids"
"strings"
"time"
)
var ErrConfigurationAuthorityRequired = fmt.Errorf("configuration change requires Authority and Commit")
func AssignmentFromConversation(kind, principalID, definitionRef, typedValue, sessionRef, sourceRef string, temporaryOverride bool, definition configuration.SettingDefinition, now time.Time) (configuration.SettingAssignment, error) {
if kind != "SET" || principalID == "" || definitionRef != string(definition.ID) || strings.TrimSpace(typedValue) == "" || now.IsZero() || definition.ValueType == configuration.ValueSecretRef {
return configuration.SettingAssignment{}, ErrInvalidConfigurationCommand
}
if sourceRef == "" {
return configuration.SettingAssignment{}, ErrInvalidConfigurationCommand
}
id, err := ids.New()
if err != nil {
return configuration.SettingAssignment{}, err
}
return configuration.SettingAssignment{ID: configuration.SettingAssignmentID(id), PrincipalID: principalID, DefinitionRef: string(definition.ID), DefinitionRevision: definition.Revision, Revision: 1, Selector: configuration.ScopeSelector{PrincipalRef: principalID, SessionRef: sessionRef, TemporaryOverride: temporaryOverride}, TypedValue: strings.TrimSpace(typedValue), Source: configuration.SourceHumanDirective, SourceRef: sourceRef, Lifecycle: configuration.SettingActive, RecordedAtUnixMillis: now.UnixMilli()}, nil
}
func ExplainEffectiveSetting(definition configuration.SettingDefinition, effective configuration.EffectiveSetting) string {
if effective.WinningAssignmentRef == "" {
return fmt.Sprintf("Настройка %s сейчас имеет системное значение %q.", definition.StableKey, effective.Value)
}
return fmt.Sprintf("Настройка %s сейчас имеет значение %q; применено scoped-правило %s.", definition.StableKey, effective.Value, effective.WinningAssignmentRef)
}
@@ -0,0 +1,23 @@
package domain_test
import (
"git.cin.su/m/Assistant/contracts/configuration"
"git.cin.su/m/Assistant/modules/configuration/domain"
"testing"
"time"
)
func TestConversationCreatesScopedAssignmentWithoutBypassingAuthority(t *testing.T) {
d := configuration.SettingDefinition{ID: "setting:language", OwnerNamespace: "interaction", StableKey: "language", Revision: 2, ValueType: configuration.ValueString, Lifecycle: configuration.SettingActive, RecordedAtUnixMillis: 1}
a, err := domain.AssignmentFromConversation("SET", "principal-1", "setting:language", "ru", "session-1", "utterance-1", true, d, time.UnixMilli(2))
if err != nil || a.Selector.PrincipalRef != "principal-1" || !a.Selector.TemporaryOverride || a.Source != configuration.SourceHumanDirective {
t.Fatalf("assignment=%+v err=%v", a, err)
}
}
func TestSecretConfigurationCannotReceiveConversationalValue(t *testing.T) {
d := configuration.SettingDefinition{ID: "setting:key", OwnerNamespace: "capability", StableKey: "key", Revision: 1, ValueType: configuration.ValueSecretRef, Lifecycle: configuration.SettingActive, RecordedAtUnixMillis: 1}
if _, err := domain.AssignmentFromConversation("SET", "p", string(d.ID), "secret", "", "u", false, d, time.UnixMilli(2)); err == nil {
t.Fatal("secret value accepted")
}
}
+35
View File
@@ -0,0 +1,35 @@
package domain
import (
"errors"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/platform/ids"
)
var ErrInvalidOutcomeAssessment = errors.New("invalid world outcome assessment")
// AssessWorldOutcome keeps Commit's execution fact separate from Epistemics'
// observation. No observation means UNKNOWN; a transport receipt alone never
// becomes success.
func AssessWorldOutcome(principalID, attemptRef, expectedEffect string, receiptRefs []string, observationRefs []epistemics.ObservationID, claimRefs []epistemics.ClaimID, evidenceRefs []epistemics.EvidenceRef, observedState epistemics.WorldOutcomeState, confidence, assessedAt int64) (epistemics.WorldOutcomeAssessment, error) {
if principalID == "" || attemptRef == "" || expectedEffect == "" || assessedAt < 1 || confidence < 0 || confidence > 1000 {
return epistemics.WorldOutcomeAssessment{}, ErrInvalidOutcomeAssessment
}
state := epistemics.OutcomeUnknownState
if len(observationRefs) == 0 {
state = epistemics.OutcomeUnknownState
} else if observedState == epistemics.OutcomeVerifiedSuccess || observedState == epistemics.OutcomeVerifiedPartial || observedState == epistemics.OutcomeVerifiedFailure || observedState == epistemics.OutcomeConflicted {
state = observedState
} else {
state = epistemics.OutcomeVerifiedPartial
}
id, err := ids.New()
if err != nil {
return epistemics.WorldOutcomeAssessment{}, err
}
value := epistemics.WorldOutcomeAssessment{ID: string(id), PrincipalID: principalID, ActionAttemptRef: attemptRef, ExecutionReceiptRefs: append([]string(nil), receiptRefs...), ExpectedEffectRef: expectedEffect, ObservationRefs: append([]epistemics.ObservationID(nil), observationRefs...), ClaimRefs: append([]epistemics.ClaimID(nil), claimRefs...), State: state, ConfidenceMillis: confidence, EvidenceRefs: append([]epistemics.EvidenceRef(nil), evidenceRefs...), AssessedAt: assessedAt}
if !value.Valid() {
return epistemics.WorldOutcomeAssessment{}, ErrInvalidOutcomeAssessment
}
return value, nil
}
+21
View File
@@ -0,0 +1,21 @@
package domain_test
import (
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/modules/epistemics/domain"
"testing"
)
func TestExecutionReceiptWithoutObservationRemainsUnknown(t *testing.T) {
value, err := domain.AssessWorldOutcome("principal", "attempt", "effect", []string{"receipt"}, nil, nil, nil, epistemics.OutcomeVerifiedSuccess, 1000, 1)
if err != nil || value.State != epistemics.OutcomeUnknownState {
t.Fatalf("value=%+v err=%v", value, err)
}
}
func TestObservedOutcomeCanBeVerifiedSeparately(t *testing.T) {
value, err := domain.AssessWorldOutcome("principal", "attempt", "effect", []string{"receipt"}, []epistemics.ObservationID{"observation"}, []epistemics.ClaimID{"claim"}, []epistemics.EvidenceRef{"evidence"}, epistemics.OutcomeVerifiedSuccess, 900, 1)
if err != nil || value.State != epistemics.OutcomeVerifiedSuccess || len(value.EvidenceRefs) != 1 {
t.Fatalf("value=%+v err=%v", value, err)
}
}
+14
View File
@@ -0,0 +1,14 @@
package domain
import "git.cin.su/m/Assistant/contracts/epistemics"
func AssessSourceReliability(sourceRef, domainRef string, freshness epistemics.FreshnessState, historyScore, priorScore int64, evidence []string, at int64) epistemics.SourceReliabilityAssessment {
score := (historyScore + priorScore) / 2
state := epistemics.ReliabilityCurrent
if freshness == epistemics.FreshnessUnknown {
state = epistemics.ReliabilityUnknown
} else if freshness == epistemics.FreshnessStale {
state = epistemics.ReliabilityAging
}
return epistemics.SourceReliabilityAssessment{SourceRef: sourceRef, DomainRef: domainRef, Freshness: freshness, HistoryScoreMillis: historyScore, ReliabilityScoreMillis: score, State: state, EvidenceRefs: append([]string(nil), evidence...), AssessedAtUnixMillis: at}
}
@@ -0,0 +1,14 @@
package domain_test
import (
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/modules/epistemics/domain"
"testing"
)
func TestSourceReliabilityIncludesDomainFreshnessAndHistory(t *testing.T) {
a := domain.AssessSourceReliability("source", "weather", epistemics.FreshnessStale, 900, 700, []string{"history"}, 1)
if !a.Valid() || a.State != epistemics.ReliabilityAging || a.ReliabilityScoreMillis != 800 {
t.Fatalf("assessment=%+v", a)
}
}
+36
View File
@@ -0,0 +1,36 @@
package domain
import (
"errors"
"git.cin.su/m/Assistant/contracts/identity"
)
var ErrInvalidRealm = errors.New("invalid principal data realm")
func ValidatePrincipalDataRealm(value identity.PrincipalDataRealm) error {
if !value.Valid() {
return ErrInvalidRealm
}
if value.Kind == identity.DataRealmPersonal && len(value.MemberPrincipalIDs) != 0 {
return ErrInvalidRealm
}
return nil
}
func CanReadRealm(value identity.PrincipalDataRealm, principalID identity.PrincipalID) bool {
if ValidatePrincipalDataRealm(value) != nil || principalID == "" {
return false
}
if value.Kind == identity.DataRealmPublic {
return true
}
if principalID == value.OwnerPrincipalID {
return true
}
for _, member := range value.MemberPrincipalIDs {
if member == principalID {
return value.Kind == identity.DataRealmShared && value.Shared
}
}
return false
}
+14
View File
@@ -0,0 +1,14 @@
package domain_test
import (
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/modules/identity/domain"
"testing"
)
func TestRealmSeparatesOwnerSharedAndPublicAccess(t *testing.T) {
r := identity.PrincipalDataRealm{ID: "realm", OwnerPrincipalID: "owner", Kind: identity.DataRealmShared, Shared: true, MemberPrincipalIDs: []identity.PrincipalID{"member"}, Revision: 1, RecordedAtUnixMillis: 1}
if !domain.CanReadRealm(r, "owner") || !domain.CanReadRealm(r, "member") || domain.CanReadRealm(r, "guest") {
t.Fatalf("realm access leaked: %+v", r)
}
}
@@ -0,0 +1,31 @@
package domain
import (
"git.cin.su/m/Assistant/contracts/interaction"
"strings"
)
// ParseConfigurationCommand recognises bounded human intent and never writes
// configuration or invokes Authority/Commit.
func ParseConfigurationCommand(message, principalID, sourceRef string) interaction.ConfigurationCommand {
text := strings.TrimSpace(strings.ToLower(message))
if principalID == "" || sourceRef == "" {
return interaction.ConfigurationCommand{}
}
if strings.Contains(text, "какая настройка") || strings.HasPrefix(text, "какое значение") {
return interaction.ConfigurationCommand{Kind: interaction.ConfigurationCommandQuery, PrincipalID: principalID, SourceRef: sourceRef}
}
temporary := strings.Contains(text, "только сейчас") || strings.Contains(text, "на этот раз") || strings.Contains(text, "временно")
if strings.Contains(text, "настрой") || strings.Contains(text, "установи") || strings.Contains(text, "сделай") {
parts := strings.SplitN(message, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(strings.TrimPrefix(strings.ToLower(parts[0]), "настройка"))
value := strings.TrimSpace(parts[1])
for _, marker := range []string{"только сейчас", "на этот раз", "временно"} {
value = strings.TrimSpace(strings.ReplaceAll(strings.ToLower(value), marker, ""))
}
return interaction.ConfigurationCommand{Kind: interaction.ConfigurationCommandSet, PrincipalID: principalID, DefinitionRef: key, TypedValue: value, TemporaryOverride: temporary, SourceRef: sourceRef}
}
}
return interaction.ConfigurationCommand{}
}
@@ -0,0 +1,14 @@
package domain_test
import (
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
"testing"
)
func TestConversationParserProducesIntentOnly(t *testing.T) {
value := interactiondomain.ParseConfigurationCommand("Настройка language = ru только сейчас", "principal-1", "utterance-1")
if value.Kind != interaction.ConfigurationCommandSet || value.DefinitionRef != "language" || value.TypedValue != "ru" || !value.TemporaryOverride {
t.Fatalf("value=%+v", value)
}
}
@@ -11,6 +11,23 @@ type ContextProjection struct {
// BuildGen2Context is a read-side projection over canonical utterance history.
// PersonalMemory remains a compatibility fallback and is never written here.
func BuildGen2Context(history []interaction.HumanUtterance) ContextProjection {
return BuildScopedGen2Context(history, "")
}
// BuildScopedGen2Context is the Gen2 retrieval boundary for household
// context. The caller supplies already-authorized history; this defensive
// filter prevents accidental cross-principal projection when a mixed source
// is passed during degraded operation.
func BuildScopedGen2Context(history []interaction.HumanUtterance, principalID string) ContextProjection {
if principalID != "" {
scoped := make([]interaction.HumanUtterance, 0, len(history))
for _, item := range history {
if item.PrincipalID == "" || string(item.PrincipalID) == principalID {
scoped = append(scoped, item)
}
}
history = scoped
}
text := CompilePersonalMemory(history).ContextText()
if text != "" {
return ContextProjection{Text: text, Source: "retrieval:interaction-utterances"}
@@ -3,6 +3,7 @@ package domain_test
import (
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/modules/interaction/domain"
"strings"
"testing"
)
@@ -16,3 +17,22 @@ func TestGen2ContextUsesRetrievalProjectionAndKeepsV1Fallback(t *testing.T) {
t.Fatalf("fallback missing: %#v", empty)
}
}
func TestGen2ContextDoesNotProjectAnotherPrincipal(t *testing.T) {
got := domain.BuildScopedGen2Context([]interaction.HumanUtterance{
{ID: "mine", PrincipalID: "principal-1", Content: "Я сейчас в Москве", ReceivedAtUnixMillis: 2},
{ID: "other", PrincipalID: "principal-2", Content: "Я сейчас в Казани", ReceivedAtUnixMillis: 3},
}, "principal-1")
if got.Text == "" || strings.Contains(got.Text, "Казани") || !strings.Contains(got.Text, "Москве") {
t.Fatalf("scoped context=%#v", got)
}
}
func TestGen2ContextProjectsExplicitRememberedAssertion(t *testing.T) {
got := domain.BuildScopedGen2Context([]interaction.HumanUtterance{
{ID: "assertion", PrincipalID: "principal-1", Content: "Для этого разговора запомни: мой тестовый цвет — синий.", ReceivedAtUnixMillis: 1},
}, "principal-1")
if got.Source != "retrieval:interaction-utterances" || got.UsedPersonalMemoryFallback || !strings.Contains(got.Text, "тестовый цвет — синий") {
t.Fatalf("remembered assertion was not projected: %#v", got)
}
}
@@ -123,5 +123,31 @@ func personalPreference(message string) string {
return value
}
}
// Explicit remembered assertions are still human-provided context, not
// verified facts. Project the compact key/value statement without storing a
// second copy outside the canonical utterance history.
if index := strings.Index(lower, "запомни"); index >= 0 {
remembered := strings.TrimSpace(lower[index+len("запомни"):])
remembered = strings.TrimLeft(remembered, " :—-")
for _, prefix := range []string{"мой ", "моя ", "моё ", "мое "} {
if !strings.HasPrefix(remembered, prefix) {
continue
}
statement := strings.TrimSpace(remembered[len(prefix):])
separator := strings.IndexAny(statement, "—-:")
if separator <= 0 || separator >= len(statement)-1 {
continue
}
key := strings.TrimSpace(statement[:separator])
separatorWidth := 1
if strings.HasPrefix(statement[separator:], "—") {
separatorWidth = len("—")
}
value := strings.Trim(strings.TrimSpace(statement[separator+separatorWidth:]), " .,!?:;—-")
if key != "" && len([]rune(value)) >= 2 {
return key + " — " + value
}
}
}
return ""
}
+12
View File
@@ -0,0 +1,12 @@
package domain
import "git.cin.su/m/Assistant/contracts/interaction"
// AttributeSpeaker is an observational hint only. It deliberately cannot
// manufacture an authenticated principal binding from voice recognition.
func AttributeSpeaker(sourceRef, principalCandidate, observationRef string, confidence int64) interaction.SpeakerAttribution {
if sourceRef == "" || observationRef == "" || confidence < 0 || confidence > 1000 {
return interaction.SpeakerAttribution{}
}
return interaction.SpeakerAttribution{SourceRef: sourceRef, PrincipalCandidate: principalCandidate, ConfidenceMillis: confidence, ObservationRef: observationRef, Authenticated: false}
}
@@ -0,0 +1,13 @@
package domain_test
import (
"git.cin.su/m/Assistant/modules/interaction/domain"
"testing"
)
func TestSpeakerRecognitionRemainsObservationNotAuthentication(t *testing.T) {
a := domain.AttributeSpeaker("mic-1", "principal-member", "observation-1", 700)
if a.Authenticated || a.PrincipalCandidate == "" || a.ObservationRef == "" {
t.Fatalf("attribution=%+v", a)
}
}
+76
View File
@@ -0,0 +1,76 @@
package domain
import (
"errors"
"git.cin.su/m/Assistant/contracts/work"
)
var ErrGovernedEvolution = errors.New("governed evolution gate failed")
func RunGovernedEvolution(needRef string, ops work.GovernedEvolutionOps) (work.GovernedEvolutionResult, error) {
result := work.GovernedEvolutionResult{NeedRef: needRef, Stages: []work.EvolutionRunStage{work.EvolutionNeedRecorded}}
if needRef == "" || ops.CreateCandidate == nil || ops.Evaluate == nil || ops.SandboxBuild == nil || ops.Benchmark == nil || ops.Promote == nil || ops.Rollback == nil || ops.LearningLedgerOutcome == nil {
return result, ErrGovernedEvolution
}
candidate, err := ops.CreateCandidate()
if err != nil {
return result, err
}
if candidate.ID == "" || candidate.ProposalRef == "" {
return result, ErrGovernedEvolution
}
result.CandidateRef = candidate.ID
result.Stages = append(result.Stages, work.EvolutionCandidateBuilt)
evaluation, err := ops.Evaluate(candidate)
if err != nil || !evaluation.ValidFor(candidate) {
return result, ErrGovernedEvolution
}
result.EvaluationRef = evaluation.CandidateRef
result.Stages = append(result.Stages, work.EvolutionIndependentlyEvaluated)
sandboxEvidence, err := ops.SandboxBuild(candidate)
if err != nil || sandboxEvidence == "" {
return result, ErrGovernedEvolution
}
candidate.SandboxEvidenceRefs = append(candidate.SandboxEvidenceRefs, sandboxEvidence)
result.EvidenceRefs = append(result.EvidenceRefs, sandboxEvidence)
result.Stages = append(result.Stages, work.EvolutionSandboxed)
benchmarkEvidence, passed, err := ops.Benchmark(candidate)
if err != nil || !passed || benchmarkEvidence == "" {
result = rollbackEvolution(result, candidate, ops)
return result, ErrGovernedEvolution
}
candidate.BenchmarkEvidenceRefs = append(candidate.BenchmarkEvidenceRefs, benchmarkEvidence)
result.EvidenceRefs = append(result.EvidenceRefs, benchmarkEvidence)
result.Stages = append(result.Stages, work.EvolutionBenchmarked)
if ops.Canary != nil {
canaryEvidence, passed, canaryErr := ops.Canary(candidate)
if canaryErr != nil || !passed || canaryEvidence == "" {
result = rollbackEvolution(result, candidate, ops)
return result, ErrGovernedEvolution
}
result.EvidenceRefs = append(result.EvidenceRefs, canaryEvidence)
result.Stages = append(result.Stages, work.EvolutionCanaried)
}
promotionRef, err := ops.Promote(candidate, evaluation)
if err != nil || promotionRef == "" {
result = rollbackEvolution(result, candidate, ops)
return result, ErrGovernedEvolution
}
result.Promoted = true
result.Stages = append(result.Stages, work.EvolutionPromoted)
result.LearningLedgerOutcomeRef, err = ops.LearningLedgerOutcome(result)
if err != nil || result.LearningLedgerOutcomeRef == "" {
return result, ErrGovernedEvolution
}
return result, nil
}
func rollbackEvolution(result work.GovernedEvolutionResult, candidate work.EvolutionCandidate, ops work.GovernedEvolutionOps) work.GovernedEvolutionResult {
ref, err := ops.Rollback(candidate)
if err == nil && ref != "" {
result.RollbackRef = ref
result.Stages = append(result.Stages, work.EvolutionRolledBack)
result.LearningLedgerOutcomeRef, _ = ops.LearningLedgerOutcome(result)
}
return result
}
@@ -0,0 +1,77 @@
package domain_test
import (
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/domain"
"testing"
)
func TestGovernedEvolutionExecutesGatesAndLedgerOutcome(t *testing.T) {
calls := []string{}
result, err := domain.RunGovernedEvolution("need-1", work.GovernedEvolutionOps{
CreateCandidate: func() (work.EvolutionCandidate, error) {
calls = append(calls, "candidate")
return work.EvolutionCandidate{ID: "candidate-1", ProposalRef: "proposal-1", CreatorIdentity: "creator", RollbackRef: "rollback-plan"}, nil
},
Evaluate: func(c work.EvolutionCandidate) (work.EvolutionEvaluation, error) {
calls = append(calls, "evaluate")
return work.EvolutionEvaluation{CandidateRef: c.ID, EvaluatorIdentity: "independent", Approved: true, MustNotWorsenPassed: true, EvidenceRefs: []work.EvidenceRef{"eval"}, RecordedAtUnixMillis: 1}, nil
},
SandboxBuild: func(work.EvolutionCandidate) (work.EvidenceRef, error) {
calls = append(calls, "sandbox-build")
return "sandbox", nil
},
Benchmark: func(work.EvolutionCandidate) (work.EvidenceRef, bool, error) {
calls = append(calls, "benchmark")
return "benchmark", true, nil
},
Canary: func(work.EvolutionCandidate) (work.EvidenceRef, bool, error) {
calls = append(calls, "canary")
return "canary", true, nil
},
Promote: func(work.EvolutionCandidate, work.EvolutionEvaluation) (string, error) {
calls = append(calls, "promote")
return "promotion", nil
},
Rollback: func(work.EvolutionCandidate) (string, error) {
calls = append(calls, "rollback")
return "rollback", nil
},
LearningLedgerOutcome: func(work.GovernedEvolutionResult) (string, error) {
calls = append(calls, "ledger")
return "ledger-outcome", nil
},
})
if err != nil || !result.Promoted || result.LearningLedgerOutcomeRef != "ledger-outcome" {
t.Fatalf("result=%+v err=%v", result, err)
}
want := []string{"candidate", "evaluate", "sandbox-build", "benchmark", "canary", "promote", "ledger"}
if len(calls) != len(want) {
t.Fatalf("calls=%v", calls)
}
for i := range want {
if calls[i] != want[i] {
t.Fatalf("calls=%v", calls)
}
}
}
func TestGovernedEvolutionRollsBackOnMustNotWorsenFailure(t *testing.T) {
rolledBack := false
result, err := domain.RunGovernedEvolution("need-1", work.GovernedEvolutionOps{
CreateCandidate: func() (work.EvolutionCandidate, error) {
return work.EvolutionCandidate{ID: "candidate-1", ProposalRef: "proposal-1", CreatorIdentity: "creator", RollbackRef: "rollback-plan"}, nil
},
Evaluate: func(c work.EvolutionCandidate) (work.EvolutionEvaluation, error) {
return work.EvolutionEvaluation{CandidateRef: c.ID, EvaluatorIdentity: "independent", Approved: true, MustNotWorsenPassed: true, EvidenceRefs: []work.EvidenceRef{"eval"}, RecordedAtUnixMillis: 1}, nil
},
SandboxBuild: func(work.EvolutionCandidate) (work.EvidenceRef, error) { return "sandbox", nil }, Benchmark: func(work.EvolutionCandidate) (work.EvidenceRef, bool, error) { return "benchmark", false, nil },
Promote: func(work.EvolutionCandidate, work.EvolutionEvaluation) (string, error) {
t.Fatal("promotion bypassed failed benchmark")
return "", nil
}, Rollback: func(work.EvolutionCandidate) (string, error) { rolledBack = true; return "rollback-evidence", nil }, LearningLedgerOutcome: func(r work.GovernedEvolutionResult) (string, error) { return "ledger-rollback", nil },
})
if err == nil || !rolledBack || result.RollbackRef != "rollback-evidence" || result.Promoted {
t.Fatalf("result=%+v err=%v", result, err)
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ func AssembleSpecialists(p work.PerspectiveSpec, subtasks []string) work.TeamDec
}
assignments := make([]work.SpecialistAssignment, 0, len(subtasks))
for _, s := range subtasks {
assignments = append(assignments, work.SpecialistAssignment{Perspective: p, SubtaskRef: s})
assignments = append(assignments, work.SpecialistAssignment{Perspective: p, SubtaskRef: s, FocusRef: p.FocusRef, SkillRefs: append([]string(nil), p.SkillRefs...), ToolRefs: append([]string(nil), p.ToolRefs...)})
}
return work.TeamDecision{UseTeam: true, Reason: "independent subtasks justify bounded perspectives", Assignments: assignments}
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
func TestPerspectiveTeamIsBoundedByIndependentSubtasks(t *testing.T) {
p := work.PerspectiveSpec{ID: "architect", ObjectiveLens: "architecture", EvaluationCriteria: []string{"correctness"}, ExpectedOutputShape: "finding"}
p := work.PerspectiveSpec{ID: "architect", ObjectiveLens: "architecture", FocusRef: "focus-1", SkillRefs: []string{"skill-1"}, ToolRefs: []string{"tool-1"}, EvaluationCriteria: []string{"correctness"}, ExpectedOutputShape: "finding"}
if d := domain.AssembleSpecialists(p, []string{"a"}); d.UseTeam {
t.Fatal("team for one subtask")
}
+7
View File
@@ -0,0 +1,7 @@
package domain
import "git.cin.su/m/Assistant/contracts/work"
func PromoteEvolution(c work.EvolutionCandidate, e work.EvolutionEvaluation) bool {
return e.ValidFor(c) && c.ProposalRef != "" && c.RollbackRef != "" && len(c.SandboxEvidenceRefs) > 0 && len(c.BenchmarkEvidenceRefs) > 0
}
@@ -0,0 +1,19 @@
package domain_test
import (
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/domain"
"testing"
)
func TestEvolutionRequiresIndependentEvaluatorAndRollbackEvidence(t *testing.T) {
c := work.EvolutionCandidate{ID: "c", ProposalRef: "p", ChangeLevel: work.EvolutionBehavior, CreatorIdentity: "creator", RollbackRef: "rollback", SandboxEvidenceRefs: []work.EvidenceRef{"sandbox"}, BenchmarkEvidenceRefs: []work.EvidenceRef{"benchmark"}}
e := work.EvolutionEvaluation{CandidateRef: "c", EvaluatorIdentity: "creator", Approved: true, MustNotWorsenPassed: true, EvidenceRefs: []work.EvidenceRef{"eval"}, RecordedAtUnixMillis: 1}
if domain.PromoteEvolution(c, e) {
t.Fatal("creator promoted own change")
}
e.EvaluatorIdentity = "independent"
if !domain.PromoteEvolution(c, e) {
t.Fatal("independently evaluated candidate rejected")
}
}
+130
View File
@@ -0,0 +1,130 @@
package supervisor
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"sync"
)
// DurableStore is supervisor-owned external control-plane state. It is not a
// candidate Assistant process-local recovery cache.
type DurableStore struct{ path string }
func NewDurableStore(path string) (*DurableStore, error) {
if path == "" || !filepath.IsAbs(path) {
return nil, errors.New("supervisor state path must be absolute")
}
return &DurableStore{path: path}, nil
}
func (s *DurableStore) Save(value Lifeboat) error {
if s == nil || s.path == "" {
return errors.New("supervisor state store is unavailable")
}
payload, err := json.Marshal(value)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(s.path), 0700); err != nil {
return err
}
temporary, err := os.CreateTemp(filepath.Dir(s.path), ".lifeboat-*.tmp")
if err != nil {
return err
}
temporaryName := temporary.Name()
defer os.Remove(temporaryName)
if err = temporary.Chmod(0600); err == nil {
_, err = temporary.Write(payload)
}
if closeErr := temporary.Close(); err == nil {
err = closeErr
}
if err != nil {
return err
}
return os.Rename(temporaryName, s.path)
}
func (s *DurableStore) Load() (Lifeboat, error) {
if s == nil || s.path == "" {
return Lifeboat{}, errors.New("supervisor state store is unavailable")
}
payload, err := os.ReadFile(s.path)
if err != nil {
return Lifeboat{}, err
}
var value Lifeboat
if err := json.Unmarshal(payload, &value); err != nil {
return Lifeboat{}, err
}
return value, nil
}
// PersistentLifeboat reloads before every transition and commits after it.
type PersistentLifeboat struct {
store *DurableStore
mu sync.Mutex
}
func NewPersistentLifeboat(store *DurableStore) *PersistentLifeboat {
return &PersistentLifeboat{store: store}
}
func (p *PersistentLifeboat) transition(fn func(*Lifeboat) bool) (bool, error) {
p.mu.Lock()
defer p.mu.Unlock()
value, err := p.store.Load()
if err != nil && !os.IsNotExist(err) {
return false, err
}
ok := fn(&value)
if !ok {
return false, nil
}
if err := p.store.Save(value); err != nil {
return false, err
}
return true, nil
}
func (p *PersistentLifeboat) Start(candidate Release) (bool, error) {
return p.transition(func(value *Lifeboat) bool { return value.Start(candidate) })
}
func (p *PersistentLifeboat) EstablishBaseline(release Release) (bool, error) {
return p.transition(func(value *Lifeboat) bool { return value.EstablishBaseline(release) })
}
func (p *PersistentLifeboat) Record(observation Observation) (bool, error) {
return p.transition(func(value *Lifeboat) bool { return value.Record(observation) })
}
func (p *PersistentLifeboat) Rollback() (bool, error) {
return p.transition(func(value *Lifeboat) bool { return value.Rollback() })
}
func (p *PersistentLifeboat) SetTraffic(mode TrafficMode, stableUpstream, candidateUpstream string) (bool, error) {
return p.transition(func(value *Lifeboat) bool { return value.SetTraffic(mode, stableUpstream, candidateUpstream) })
}
func (p *PersistentLifeboat) Promote() (bool, error) {
return p.transition(func(value *Lifeboat) bool {
if value.Current.State != ReleaseHealthy || value.Current.ID == "" {
return false
}
return value.SetTraffic(TrafficCandidate, value.StableUpstream, value.CandidateUpstream)
})
}
func (p *PersistentLifeboat) RollbackTraffic() (bool, error) {
return p.transition(func(value *Lifeboat) bool {
if !value.Rollback() {
return false
}
return value.SetTraffic(TrafficStable, value.StableUpstream, value.CandidateUpstream)
})
}
func (p *PersistentLifeboat) Snapshot() (Lifeboat, error) {
p.mu.Lock()
defer p.mu.Unlock()
return p.store.Load()
}
+110
View File
@@ -0,0 +1,110 @@
package supervisor
type ReleaseState string
const (
ReleaseCandidate ReleaseState = "CANDIDATE"
ReleaseHealthy ReleaseState = "HEALTHY"
ReleaseFailed ReleaseState = "FAILED"
ReleaseRolledBack ReleaseState = "ROLLED_BACK"
)
type TrafficMode string
const (
TrafficStable TrafficMode = "STABLE"
TrafficCanaryHeader TrafficMode = "CANARY_HEADER"
TrafficCandidate TrafficMode = "CANDIDATE"
)
type Release struct {
ID string `json:"id"`
Version string `json:"version"`
State ReleaseState `json:"state"`
LastKnownGood bool `json:"last_known_good"`
}
type Observation struct {
ReleaseID string `json:"release_id"`
Ready bool `json:"ready"`
Reason string `json:"reason"`
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
}
type Lifeboat struct {
Current Release `json:"current"`
LastKnownGood Release `json:"last_known_good"`
RollbackTarget Release `json:"rollback_target"`
Observations []Observation `json:"observations"`
TrafficMode TrafficMode `json:"traffic_mode"`
StableUpstream string `json:"stable_upstream"`
CandidateUpstream string `json:"candidate_upstream"`
}
func (l *Lifeboat) Start(candidate Release) bool {
if candidate.ID == "" || candidate.Version == "" {
return false
}
candidate.State = ReleaseCandidate
if l.LastKnownGood.ID != "" {
l.RollbackTarget = l.LastKnownGood
}
l.Current = candidate
return true
}
func (l *Lifeboat) EstablishBaseline(release Release) bool {
if release.ID == "" || release.Version == "" {
return false
}
release.State = ReleaseHealthy
release.LastKnownGood = true
l.Current = release
l.LastKnownGood = release
l.RollbackTarget = Release{}
l.TrafficMode = TrafficStable
return true
}
func (l *Lifeboat) Record(o Observation) bool {
if o.ReleaseID == "" {
return false
}
l.Observations = append(l.Observations, o)
if o.ReleaseID != l.Current.ID {
return true
}
if o.Ready {
l.Current.State = ReleaseHealthy
l.Current.LastKnownGood = true
l.LastKnownGood = l.Current
} else {
l.Current.State = ReleaseFailed
}
return true
}
func (l *Lifeboat) Rollback() bool {
target := l.RollbackTarget
if target.ID == "" {
target = l.LastKnownGood
}
if target.ID == "" {
return false
}
l.Current = target
l.Current.State = ReleaseRolledBack
l.Current.LastKnownGood = true
l.LastKnownGood = l.Current
l.RollbackTarget = Release{}
return true
}
func (l *Lifeboat) SetTraffic(mode TrafficMode, stableUpstream, candidateUpstream string) bool {
if mode != TrafficStable && mode != TrafficCanaryHeader && mode != TrafficCandidate {
return false
}
if stableUpstream == "" || (mode != TrafficStable && candidateUpstream == "") {
return false
}
l.TrafficMode = mode
l.StableUpstream = stableUpstream
l.CandidateUpstream = candidateUpstream
return true
}
+84
View File
@@ -0,0 +1,84 @@
package supervisor_test
import (
"git.cin.su/m/Assistant/runtime/supervisor"
"path/filepath"
"testing"
)
func TestLifeboatRollsBackCandidateToLastKnownGood(t *testing.T) {
l := supervisor.Lifeboat{}
l.Start(supervisor.Release{ID: "good", Version: "1"})
l.Record(supervisor.Observation{ReleaseID: "good", Ready: true})
l.Start(supervisor.Release{ID: "candidate", Version: "2"})
l.Record(supervisor.Observation{ReleaseID: "candidate", Ready: false})
if !l.Rollback() || l.Current.ID != "good" || l.Current.State != supervisor.ReleaseRolledBack {
t.Fatalf("%#v", l)
}
}
func TestPersistentLifeboatRestoresLastKnownGoodAfterCandidateFailure(t *testing.T) {
store, err := supervisor.NewDurableStore(filepath.Join(t.TempDir(), "control", "lifeboat.json"))
if err != nil {
t.Fatal(err)
}
first := supervisor.NewPersistentLifeboat(store)
if ok, err := first.Start(supervisor.Release{ID: "good", Version: "1"}); err != nil || !ok {
t.Fatalf("start good: %v %v", ok, err)
}
if ok, err := first.Record(supervisor.Observation{ReleaseID: "good", Ready: true, ObservedAtUnixMillis: 1}); err != nil || !ok {
t.Fatalf("record good: %v %v", ok, err)
}
if ok, err := first.Start(supervisor.Release{ID: "candidate", Version: "2"}); err != nil || !ok {
t.Fatalf("start candidate: %v %v", ok, err)
}
if ok, err := first.Record(supervisor.Observation{ReleaseID: "candidate", Ready: false, Reason: "candidate crashed", ObservedAtUnixMillis: 2}); err != nil || !ok {
t.Fatalf("record failure: %v %v", ok, err)
}
second := supervisor.NewPersistentLifeboat(store)
if ok, err := second.Rollback(); err != nil || !ok {
t.Fatalf("rollback after restart: %v %v", ok, err)
}
state, err := second.Snapshot()
if err != nil {
t.Fatal(err)
}
if state.Current.ID != "good" || state.Current.State != supervisor.ReleaseRolledBack || state.LastKnownGood.ID != "good" {
t.Fatalf("state=%+v", state)
}
}
func TestPersistentSupervisorRestoresTrafficModeAfterRestart(t *testing.T) {
store, err := supervisor.NewDurableStore(filepath.Join(t.TempDir(), "control", "release-state.json"))
if err != nil {
t.Fatal(err)
}
first := supervisor.NewPersistentLifeboat(store)
if ok, err := first.Start(supervisor.Release{ID: "candidate", Version: "2"}); err != nil || !ok {
t.Fatalf("start: %v %v", ok, err)
}
if ok, err := first.Record(supervisor.Observation{ReleaseID: "candidate", Ready: true, ObservedAtUnixMillis: 1}); err != nil || !ok {
t.Fatalf("observe: %v %v", ok, err)
}
if ok, err := first.SetTraffic(supervisor.TrafficCanaryHeader, "http://stable:8084", "http://candidate:8084"); err != nil || !ok {
t.Fatalf("canary: %v %v", ok, err)
}
second := supervisor.NewPersistentLifeboat(store)
state, err := second.Snapshot()
if err != nil {
t.Fatal(err)
}
if state.TrafficMode != supervisor.TrafficCanaryHeader || state.CandidateUpstream != "http://candidate:8084" || state.LastKnownGood.ID != "candidate" {
t.Fatalf("state=%+v", state)
}
if ok, err := second.RollbackTraffic(); err != nil || !ok {
t.Fatalf("rollback traffic: %v %v", ok, err)
}
state, err = second.Snapshot()
if err != nil {
t.Fatal(err)
}
if state.TrafficMode != supervisor.TrafficStable || state.Current.State != supervisor.ReleaseRolledBack {
t.Fatalf("rollback state=%+v", state)
}
}
+18
View File
@@ -0,0 +1,18 @@
package execution_outcome_test
import (
"git.cin.su/m/Assistant/contracts/commit"
"git.cin.su/m/Assistant/contracts/epistemics"
"testing"
)
func TestReportedExecutionSuccessIsNotWorldVerification(t *testing.T) {
r := commit.ExecutionReceipt{ID: "r", ActionAttemptRef: "a", ExecutorImplementationRef: "impl", RequestDigest: "d", ExecutorResult: commit.ExecutorReportedSuccess, ReconciliationKey: "k", DispatchAtUnixMillis: 1}
if !r.Valid() {
t.Fatal("receipt invalid")
}
o := epistemics.WorldOutcomeAssessment{ID: "o", PrincipalID: "p", ActionAttemptRef: "a", State: epistemics.OutcomeUnverified, ConfidenceMillis: 0, AssessedAt: 1}
if !o.Valid() {
t.Fatal("unverified assessment invalid")
}
}