diff --git a/docs/runbooks/opax-live-deploy.md b/docs/runbooks/opax-live-deploy.md
new file mode 100644
index 0000000..6687c9f
--- /dev/null
+++ b/docs/runbooks/opax-live-deploy.md
@@ -0,0 +1,100 @@
+# OPAX Live Deployment Runbook
+
+## Purpose
+
+Use these scripts to deploy the two production components required for the
+Emma/OPAX experience:
+
+- OPAX MCP: the conversational MCP backend and `run_emma` tool service.
+- OPAX Web: the live web application served at `https://opax.vauco.no`.
+
+## Fixed Production Targets
+
+| Item | Value |
+|---|---|
+| Google Cloud project | `propane-will-491900-m5` |
+| Region | `us-central1` |
+| Artifact Registry repository | `us-central1-docker.pkg.dev/propane-will-491900-m5/osvauco-repo` |
+| MCP Cloud Run service | `opax-mcp` |
+| OPAX Web Cloud Run service | `opax-web` |
+| Live operator URL | `https://opax.vauco.no` |
+
+## Normal Release Sequence
+
+Run commands from the repository root:
+
+```bash
+./scripts/deploy-mcp.sh
+./scripts/deploy-opax-web.sh
+./scripts/check-live-services.sh
+```
+
+The scripts build an image, resolve its immutable digest, and deploy that digest
+to the existing Cloud Run service.
+
+## MCP Deployment Behavior
+
+The existing `cloudbuild.deploy.yaml` workflow builds and pushes an MCP image.
+Its internal Cloud Build deploy step can fail because the Cloud Build service
+account is blocked by VPC Service Controls.
+
+`deploy-mcp.sh` handles that condition by resolving the image that was pushed
+during the build and then deploying its immutable digest directly through the
+authenticated local `gcloud` session.
+
+A Cloud Build failure does not automatically mean the image build failed. The
+script stops if it cannot resolve an immutable pushed image digest.
+
+## Live Validation
+
+After deployment:
+
+1. Open `https://opax.vauco.no`.
+2. Hard-refresh the browser with `Ctrl+Shift+R`.
+3. Log in normally.
+4. Confirm that the new Emma workspace UI is visible.
+5. In a new conversation, send:
+
+ ```text
+ Jeg heter Chris.
+ ```
+
+6. In the same conversation, send:
+
+ ```text
+ Hva heter jeg?
+ ```
+
+7. Confirm that Emma uses the previous message as conversation history.
+8. Start a second conversation and ask:
+
+ ```text
+ Hva heter jeg?
+ ```
+
+9. Confirm that the second conversation does not inherit context from the first
+ conversation.
+
+## Guardrails
+
+- Use `us-central1` only.
+- Never deploy to `europe-west1`.
+- Never deploy a service named `opax`.
+- Deploy only the existing `opax-mcp` and `opax-web` services.
+- Final Cloud Run deployment must use an immutable image digest.
+- Never use a mutable tag for the final deploy.
+- Never print or place secret values in scripts, logs, documentation, or Git.
+- Do not deploy from Gemini without explicit human approval.
+- Do not change IAM, service accounts, secrets, VPC settings, DNS, OAuth, or
+ Cloud Run networking as part of a normal application release.
+
+## Rollback
+
+Use one of these existing rollback methods:
+
+1. In Cloud Run, route traffic back to the prior ready revision.
+2. Re-run the relevant deployment script after replacing the image digest with a
+ previously known good immutable digest.
+
+Use `./scripts/check-live-services.sh` to record the currently active revisions
+and images before a release.
\ No newline at end of file
diff --git a/opax-mcp/emma_adapter.py b/opax-mcp/emma_adapter.py
index 2db740c..b3a6437 100644
--- a/opax-mcp/emma_adapter.py
+++ b/opax-mcp/emma_adapter.py
@@ -47,4 +47,5 @@ class CanonicalEmma:
self._model,
prompt,
composed_system_prompt,
+ history=history or [],
)
diff --git a/opax-mcp/manifests/tools/run_emma.md b/opax-mcp/manifests/tools/run_emma.md
new file mode 100644
index 0000000..dcb0a91
--- /dev/null
+++ b/opax-mcp/manifests/tools/run_emma.md
@@ -0,0 +1,163 @@
+# run_emma / emma.chat Component Manifest
+
+**Status:** Draft
+**Runtime effect:** None
+**Governing standard:** [`INCU Master Constitution`](../../../docs/INCU_Master_Constitution.md)
+**Template:** [`INCU Component Manifest Template`](../../../docs/templates/component_manifest.md)
+**Review date:** 2026-10-18
+
+> This is a documentation-only draft manifest for an existing runtime
+> capability. It does not activate, disable, expand, or otherwise modify the
+> `run_emma` tool, the `emma.chat` capability, Emma's prompt, Emma's authority,
+> A2H2A behavior, MCP behavior, or any external-system permission.
+
+## Identity
+
+- Component ID: emma.chat
+- Component name: run_emma
+- Class: Orchestrator
+- Version: 0.1.0-draft
+- Owner: Platform Engineering
+- Status: draft
+- Review date: 2026-10-18
+
+Current runtime capability status:
+
+ACTIVE in the existing capability registry.
+
+Current manifest governance status:
+
+draft.
+
+Reason:
+
+The manifest is documentation-only. No manifest validator or runtime
+manifest-enforcement mechanism exists yet.
+
+## Purpose
+
+- Intended outcome: Provide conversational reasoning through the canonical Emma
+ runtime for a user-supplied prompt, while retaining the canonical system
+ prompt and appending only the read-only capability-status system context.
+- Value to system: Provides a single canonical entry point for conversational
+ AI reasoning, decoupled from specific models or backends, and supplies
+ governance documentation for future manifest-driven orchestration.
+- Explicit non-goals:
+ - No tool execution.
+ - No repository or file access.
+ - No deployment or infrastructure action.
+ - No external-system action, integration operation, or state-changing service
+ call. The existing model-inference request remains part of the current
+ canonical conversational runtime path only.
+ - No memory or document retrieval.
+ - No ticket creation, ticket approval, or A2H2A execution.
+ - No write, deletion, permission, scheduling, or communication action.
+ - No delegation to additional agents.
+ - No independent completion claim beyond the returned conversational response.
+
+## Scope
+
+- Permitted tasks:
+ - Accept a user-provided prompt through the existing canonical run_emma path.
+ - Compose the canonical Emma system prompt with read-only capability context.
+ - Return the model's conversational response.
+- Prohibited tasks: Any task corresponding to a planned or forbidden
+ capability. This manifest does not activate any capability.
+- Supported systems/environments: Existing opax-mcp runtime path.
+- Time/cost/retry limits: Not currently declared in the runtime capability
+ registry. No additional retry, time, or cost authority is granted by this
+ manifest.
+
+## Authority
+
+- Read permissions: User-provided prompt and existing canonical
+ prompt/capability-context inputs within the current request path only.
+- Write permissions: None.
+- Approval requirement: No external or state-changing action is within scope.
+ Any future external or irreversible action requires a separate in-scope
+ manifest, exact explicit human approval, and platform authorization.
+- Delegation rules: No delegation to subordinate agents, tools, connectors,
+ workflows, or operators is implemented or authorized.
+- Revocation method: Disable or remove the existing runtime capability through
+ the authoritative runtime capability/tool configuration and platform
+ controls; this draft document alone does not revoke runtime access.
+
+## Inputs and Outputs
+
+- Required inputs: A user prompt supplied through the canonical run_emma
+ request path.
+- Optional inputs: None declared for this manifest.
+- Outputs: The existing raw conversational response returned by the canonical
+ Emma adapter/runtime path.
+- Source of truth: Current capability registry for capability status; current
+ source code and tests for runtime contract; platform runtime for actual
+ service state.
+- Evidence format: Existing unit-test output, capability-registry state,
+ adapter call contract, and runtime health/response evidence where separately
+ verified.
+
+## INCU Mandate
+
+- Outcome statement format: Return a bounded conversational response to a
+ user-supplied prompt without claiming unavailable authority.
+- Startability rule: Begin only with the current user-provided prompt and the
+ existing read-only runtime path.
+- Applicable levers: Interest, Novelty, Challenge, Urgency, Purpose — future
+ governance reference only; no runtime behavior change is introduced by this
+ manifest.
+- Boundedness rule: One user prompt, current canonical runtime path, no
+ external-system action, no tool delegation, no state mutation, and no
+ authority expansion.
+- Restart artifact: Not currently implemented for single-request conversational
+ handling.
+- Blocker behavior: If a request requires a missing capability, tool, external
+ action, write, deployment, repository operation, memory/document access, or
+ unapproved delegation, state that it is unavailable and require a separate
+ approved manifest and explicit human authorization before any future
+ implementation.
+
+## Safety and Governance
+
+- Data classification: User-provided request content; classification and
+ retention are governed by existing platform policy and are not changed by
+ this manifest.
+- Security constraints: Secrets must not be intentionally included in prompts
+ or outputs. This draft manifest does not implement secret detection or
+ redaction. No tool execution or external-system action is authorized, and
+ existing platform authentication and authorization remain controlling.
+- Privacy constraints: Do not claim retention, deletion, or consent guarantees
+ not established by current implementation.
+- Stop conditions: Missing capability, external or state-changing request,
+ request outside current conversational scope, missing approval for a future
+ consequential action, or evidence conflict.
+- Escalation path: Accountable human owner / Platform Engineering.
+- Audit events: No new INCU audit event is created by this draft manifest.
+ Existing runtime and A2H2A audit behavior remain unchanged.
+
+## A2HA Contract
+
+- Ticket fields read: None.
+- Ticket fields written: None.
+- Allowed state transitions: None.
+- Required evidence before transition: Not applicable, because this component
+ has no A2HA write authority.
+- Comment/update policy: No A2HA comment or update authority is granted.
+
+## Evaluation
+
+- Acceptance tests:
+ - Capability registry retains emma.chat as ACTIVE.
+ - Capability bridge remains deterministic and read-only.
+ - Canonical Emma adapter retains the original user prompt.
+ - Capability context is appended only to the system prompt.
+ - No tool, connector, external request, repository, deployment, memory,
+ document-retrieval, or state-changing capability is activated.
+- Reliability metrics: Existing test success/failure and runtime health
+ evidence; no new metrics system is introduced.
+- Safety metrics:
+ - Unauthorized-write rate must remain zero.
+ - Unauthorized tool/delegation rate must remain zero.
+- Review/rollback procedure: Review this draft on or before 2026-10-18. To
+ pause or change runtime behavior, use the existing capability/tool
+ configuration and human-approved change process. This draft document has no
+ runtime rollback effect.
diff --git a/opax-mcp/server.py b/opax-mcp/server.py
index a7d87d3..0c2c5a3 100644
--- a/opax-mcp/server.py
+++ b/opax-mcp/server.py
@@ -726,13 +726,19 @@ async def trigger_build(p: dict) -> dict:
# Ollama helpers — direkte mot emma-gpu-vm
# ---------------------------------------------------------------------------
-async def _ollama_chat(model: str, prompt: str, system: str = "") -> dict:
+async def _ollama_chat(model: str, prompt: str, system: str = "", history: Optional[List[Dict[str, str]]] = None) -> dict:
# ... (beholdt uendret)
messages = []
if system:
messages.append({"role": "system", "content": system})
+ if history:
+ messages.extend(history)
messages.append({"role": "user", "content": prompt})
- payload = { "model": model, "messages": messages, "stream": False }
+ payload = {
+ "model": model,
+ "messages": messages,
+ "stream": False
+ }
try:
async with httpx.AsyncClient(timeout=120) as c:
r = await c.post(f"{OLLAMA_BASE_URL}/api/chat", json=payload)
@@ -873,12 +879,38 @@ async def tui_command(p): return await _agent_post("/tui-comman
async def run_jason(p):
"""Kaller /run på osvauco-agent, som nå har sin egen JASON_BACKEND-logikk."""
return await _agent_post("/run", {"message": p.get("prompt", p.get("message", "")), "user_id": p.get("user_id", "opax"), "session_id": p.get("session_id", "mcp"), "mode": p.get("mode", "light")})
+def _normalize_emma_history(value):
+ if not isinstance(value, list):
+ return []
+
+ normalized = []
+ for entry in value:
+ if not isinstance(entry, dict):
+ continue
+
+ role = entry.get("role")
+ content = entry.get("content")
+
+ if role not in {"user", "assistant"}:
+ continue
+
+ if not isinstance(content, str):
+ continue
+
+ content = content.strip()
+ if not content:
+ continue
+
+ normalized.append({"role": role, "content": content})
+
+ return normalized[-40:]
+
async def run_emma(p: dict) -> dict:
"""Kaller den kanoniske Emma-agenten med en prompt."""
capability_system_context = build_capability_system_context()
return await canonical_emma.run(
prompt=p.get("prompt", p.get("message", "")),
- history=[],
+ history=_normalize_emma_history(p.get("history")),
system_context=capability_system_context,
)
async def run_emma_fast(p): return await _ollama_chat(EMMA_FAST_MODEL, p.get("prompt", p.get("message", "")), p.get("system", "Du er en rask og konsis AI-assistent..."))
@@ -1164,7 +1196,7 @@ TOOLS = {
),
# AI Agents
"run_jason": (run_jason, "Kjør Jason-agenten med en prompt", {"type":"object","properties":{"prompt":{"type":"string"},"mode":{"type":"string"}},"required":["prompt"]}),
- "run_emma": (run_emma, "Emma Vauger (gemma3:4b) — primær lokal AI", {"type":"object","properties":{"prompt":{"type":"string"}}}),
+ "run_emma": (run_emma, "Emma Vauger (gemma3:4b) — primær lokal AI", {"type": "object", "properties": {"prompt": {"type": "string"}, "history": {"type": "array", "items": {"type": "object", "properties": {"role": {"type": "string", "enum": ["user", "assistant"]}, "content": {"type": "string"}}}}}}),
# Local models — read-only discovery
"list_emma_models": (
diff --git a/opax-web/backend/routes/consoleApi.js b/opax-web/backend/routes/consoleApi.js
index c18ae50..df92456 100644
--- a/opax-web/backend/routes/consoleApi.js
+++ b/opax-web/backend/routes/consoleApi.js
@@ -67,8 +67,24 @@ router.get('/approvals', (req, res) => {
});
});
+function normalizeHistory(value) {
+ if (!Array.isArray(value)) return [];
+
+ return value
+ .filter(
+ (entry) =>
+ entry &&
+ typeof entry === 'object' &&
+ (entry.role === 'user' || entry.role === 'assistant') &&
+ typeof entry.content === 'string' &&
+ entry.content.trim().length > 0
+ )
+ .slice(-40)
+ .map(({ role, content }) => ({ role, content: content.trim() }));
+}
+
router.post('/emma/chat', async (req, res) => {
- const { message, session_id, ticket_number } = req.body;
+ const { message, session_id, ticket_number, history } = req.body;
if (!message || typeof message !== 'string' || message.length > 4096) {
return res.status(400).json({ status: 'error', message: 'Invalid message.'});
@@ -79,7 +95,10 @@ router.post('/emma/chat', async (req, res) => {
}
try {
- const emmaData = await callAgent('run_emma', { prompt: message, history: [] });
+ const emmaData = await callAgent('run_emma', {
+ prompt: message,
+ history: normalizeHistory(history),
+ });
res.json({
status: 'success',
diff --git a/opax-web/frontend/src/App.tsx b/opax-web/frontend/src/App.tsx
index 76d6eb0..623819c 100644
--- a/opax-web/frontend/src/App.tsx
+++ b/opax-web/frontend/src/App.tsx
@@ -8,30 +8,32 @@ interface User {
function App() {
const [user, setUser] = useState(null);
+ const [authChecked, setAuthChecked] = useState(false);
useEffect(() => {
fetch('/auth/me', { credentials: 'include' })
.then((r) => (r.ok ? r.json() : null))
- .then(setUser);
+ .then(setUser)
+ .finally(() => setAuthChecked(true));
}, []);
+ if (!authChecked) {
+ return null; // or a loading spinner
+ }
+
if (!user) {
return (
-
+