feat: decommission nomos agent runtime (plan §5)
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Replace nomos with dsh (DeepSeek Harness) as the agent runtime per
plans/2026-08-16-dsh-as-agent-replace-nomos.md section 5.

Deleted:
- cmd/nomos/ (agent loop, :8092 chat gateway, task tools, eval runner)
- nomos/ (SOUL.md, config.yaml, skills/)
- internal/nomos/{session,messagequeue,retrycap,turngate,assent}/
- compose/nomos/ Dockerfile + docker-compose nomos service
- httpapi /agent reverse-proxy mount (NOMOS_PROXY_URL)
- evals/*.yaml (nomos golden-conversation manifests)
- compose/caddy/Caddyfile.oikos nomos.hubris.network block + /agent path
- .golangci.yml nomos-isolation rules
- go.mod openai-go dependency (go mod tidy)

Kept:
- internal/adapters/postgres assent-window logic (governance port)
- OIKOS_NOMOS_AGENT_SLUG config (MCP agent entity attribution)
- Full REST API, MCP tools, policy, scheduler, secrets

Cleaned: deploy.sh image prune, verify-phase6.sh checks,
seed-secrets.sh key source, README/CONTRIBUTING/AGENTS.md docs.

Build, vet, test, compose config all pass.
This commit is contained in:
2026-08-17 00:15:34 +02:00
parent 22fe1526ca
commit 7712e957ed
129 changed files with 36 additions and 59398 deletions

View File

@@ -1,143 +0,0 @@
package assent
import (
"regexp"
"strings"
)
// Chat-assent approval: the operator authorizes a proposed action by
// replying normally in chat ("go ahead", "yes", "do it") instead of clicking
// a separate Approve button. This is deterministic (not LLM-judged) so it
// can't be talked around by a model that misreads intent, and it only ever
// looks at the assistant turn immediately preceding the operator's reply —
// an old "yes" from three messages ago can never retroactively approve
// something new. Destructive-risk actions are excluded: they always need the
// explicit typed-confirmation flow, never loose assent.
// PendingApproval is one gated action proposed in the immediately-preceding
// assistant turn, extracted from its tool_result text.
type PendingApproval struct {
ExecID string
Destructive bool
}
// executionQueuedRE matches the "execution <uuid> queued" phrasing shared by
// the run and request_execution/pct_create tool result messages.
var executionQueuedRE = regexp.MustCompile(`(?i)execution\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s+queued`)
// extractPendingApprovals scans the tool results of one assistant turn for
// gated actions that are still awaiting a decision.
func ExtractPendingApprovals(resultTexts []string) []PendingApproval {
var out []PendingApproval
for _, text := range resultTexts {
m := executionQueuedRE.FindStringSubmatch(text)
if m == nil {
continue
}
out = append(out, PendingApproval{
ExecID: m[1],
Destructive: strings.Contains(strings.ToUpper(text), "DESTRUCTIVE"),
})
}
return out
}
// negationWords, checked first: any of these anywhere in the message means
// the reply is NOT assent, even if a positive word also appears (e.g. "no,
// don't restart it yet" contains neither "yes" nor "go ahead", but "wait"
// alone should also block a stray "yes" a sentence later — checking negation
// first and returning false errs toward re-confirming rather than assuming
// consent, per "when in doubt, escalate"). Includes contracted negatives
// ("haven't", "isn't", ...) alongside "don't"/"do not" — found live: "I
// haven't confirmed anything yet" was reading as an explicit confirmation
// because none of the contracted forms were covered, only "don't"/"do not".
// Deliberately does NOT include a bare "not": that's broad enough to false-
// negative ordinary assent ("go ahead, this is not risky") — the specific
// contracted-verb forms below are unambiguous negation on their own.
var negationWords = []string{
"no", "nope", "don't", "do not", "stop", "wait", "hold on", "hold off",
"not yet", "cancel", "nevermind", "never mind", "actually don't", "skip that",
"haven't", "hasn't", "isn't", "wasn't", "aren't", "can't", "cannot",
"won't", "wouldn't", "shouldn't", "didn't", "doesn't",
}
// assentWords, checked only if no negation matched.
var assentWords = []string{
"go ahead", "goahead", "yes", "yep", "yeah", "yup", "do it", "proceed",
"approve", "approved", "confirm", "confirmed", "ship it", "sounds good",
"lgtm", "run it", "execute", "ok go", "okay go", "please do",
}
// wordTokenRe splits a message into lowercase word tokens. Apostrophes
// (straight ' and curly ) stay attached to their word so "don't"/"haven't"
// tokenize as one token, not two.
var wordTokenRe = regexp.MustCompile(`[a-z0-9']+`)
func tokenize(msg string) []string {
return wordTokenRe.FindAllString(strings.ToLower(strings.ReplaceAll(msg, "", "'")), -1)
}
// containsPhrase reports whether phrase (one or more words) appears as a
// consecutive run of WHOLE tokens in tokens — never a mid-word substring
// match. This is the fix for a real false positive found live: the old
// substring check (`strings.Contains(m, "yes")`) matched "yes" inside
// "yesterday", and "confirm" inside "confirmed"/"unconfirmed" without regard
// for word boundaries. Negation already used a word-boundary check
// (space-padded); assent/confirm words didn't — this brings both onto the
// same, more robust tokenized comparison instead of ad-hoc string padding.
func containsPhrase(tokens []string, phrase string) bool {
words := strings.Fields(phrase)
if len(words) == 0 || len(words) > len(tokens) {
return false
}
for i := 0; i+len(words) <= len(tokens); i++ {
match := true
for j, w := range words {
if tokens[i+j] != w {
match = false
break
}
}
if match {
return true
}
}
return false
}
// isAssent reports whether msg is a plain-language authorization of a
// pending proposal. Deliberately simple and auditable: a fixed word list,
// not a model judgment call, so behavior is predictable and can't be
// prompt-injected via the pending action's own content.
func IsAssent(msg string) bool {
tokens := tokenize(msg)
for _, w := range negationWords {
if containsPhrase(tokens, w) {
return false
}
}
for _, w := range assentWords {
if containsPhrase(tokens, w) {
return true
}
}
return false
}
// isTypedConfirmation reports whether msg is an explicit confirmation strong
// enough to grant a DESTRUCTIVE pending action. Deliberately a separate,
// stricter check from isAssent: a bare "yes"/"go ahead"/"proceed" must never
// grant something destructive, only an explicit "confirm" statement does —
// this is the typed-confirmation phrase SOUL.md tells the operator to use
// ("I confirm destroy 135"). Still negation-aware for the same reason as
// isAssent: "don't confirm yet" must not accidentally match.
func IsTypedConfirmation(msg string) bool {
tokens := tokenize(msg)
for _, w := range negationWords {
if containsPhrase(tokens, w) {
return false
}
}
return containsPhrase(tokens, "confirm") || containsPhrase(tokens, "confirmed")
}

View File

@@ -1,128 +0,0 @@
package assent
import (
"testing"
)
func TestIsAssent_Positive(t *testing.T) {
cases := []string{
"go ahead", "Go ahead.", "yes", "Yes!", "yeah", "yep", "do it",
"proceed", "approve", "ship it", "sounds good", "lgtm", "please do",
"ok go ahead and run it",
}
for _, c := range cases {
if !IsAssent(c) {
t.Errorf("IsAssent(%q) = false, want true", c)
}
}
}
func TestIsAssent_Negative(t *testing.T) {
cases := []string{
"no", "no, don't", "wait", "hold on", "not yet", "cancel that",
"nevermind", "what's the plan for tomorrow?", "how many CPUs does strong have?",
"maybe later", "",
}
for _, c := range cases {
if IsAssent(c) {
t.Errorf("IsAssent(%q) = true, want false", c)
}
}
}
func TestIsAssent_NegationBeatsAssentWord(t *testing.T) {
// Contains "yes" as a substring pattern risk word but is clearly not
// assent — negation must win.
cases := []string{
"no, don't do it yet",
"wait, not yet please",
}
for _, c := range cases {
if IsAssent(c) {
t.Errorf("IsAssent(%q) = true, want false (negation should block)", c)
}
}
}
// TestIsAssent_WholeWordBoundary regression-tests a real false positive found
// live: the old substring check matched "yes" inside "yesterday" (and would
// equally match "confirm" inside "confirmed"/"unconfirmed" for
// IsTypedConfirmation below) because only negation used a word-boundary
// check — assent/confirm words used a bare strings.Contains. Confirmed via a
// throwaway probe before being fixed; kept here permanently so a future
// change can't silently reintroduce it.
func TestIsAssent_WholeWordBoundary(t *testing.T) {
cases := []string{
"not sure, maybe yesterday's logs show something useful",
"my eyesight isn't great, what does that say",
}
for _, c := range cases {
if IsAssent(c) {
t.Errorf("IsAssent(%q) = true, want false (word-boundary: 'yes' must not match inside 'yesterday'/'eyesight')", c)
}
}
}
// TestIsTypedConfirmation_ContractedNegation regression-tests the other real
// false positive: IsTypedConfirmation gates DESTRUCTIVE actions, and
// "confirm" matching inside "confirmed" combined with contracted negatives
// ("haven't") not being in negationWords meant a message that explicitly
// says the operator has NOT confirmed something could read as confirming it.
func TestIsTypedConfirmation_ContractedNegation(t *testing.T) {
cases := []string{
"I haven't confirmed anything yet, let me think",
"that isn't confirmed on my end",
"we can't confirm that until tomorrow",
}
for _, c := range cases {
if IsTypedConfirmation(c) {
t.Errorf("IsTypedConfirmation(%q) = true, want false (contracted negation should block)", c)
}
}
}
func TestIsTypedConfirmation(t *testing.T) {
positive := []string{
"I confirm destroy 135 in strong",
"confirm",
"Confirmed.",
"yes I confirm",
}
for _, c := range positive {
if !IsTypedConfirmation(c) {
t.Errorf("IsTypedConfirmation(%q) = false, want true", c)
}
}
negative := []string{
"yes", "go ahead", "do it", "proceed", "lgtm", // loose assent must NOT satisfy this
"no, don't confirm yet", "wait", "",
}
for _, c := range negative {
if IsTypedConfirmation(c) {
t.Errorf("IsTypedConfirmation(%q) = true, want false (only explicit confirm should pass)", c)
}
}
}
func TestExtractPendingApprovals(t *testing.T) {
got := ExtractPendingApprovals([]string{
"run on host:strong requires approval (risk: config_mutation) - execution 019f4930-e22b-7c47-8c6e-715dcd59df19 queued. Present the command...",
"some unrelated read-only result, no approval here",
"run on lxc:caddy requires approval (risk: destructive) - execution 019f4931-aaaa-7c47-8c6e-715dcd59df20 queued. This is classified DESTRUCTIVE - flag that clearly.",
})
if len(got) != 2 {
t.Fatalf("expected 2 pending approvals, got %d", len(got))
}
if got[0].ExecID != "019f4930-e22b-7c47-8c6e-715dcd59df19" || got[0].Destructive {
t.Errorf("first approval: ExecID=%s Destructive=%v", got[0].ExecID, got[0].Destructive)
}
if got[1].ExecID != "019f4931-aaaa-7c47-8c6e-715dcd59df20" || !got[1].Destructive {
t.Errorf("second approval should be flagged destructive: %+v", got[1])
}
}
func TestExtractPendingApprovals_NoneWhenNoneQueued(t *testing.T) {
if got := ExtractPendingApprovals(nil); len(got) != 0 {
t.Errorf("expected 0, got %d", len(got))
}
}