Skip to content

Release: open-source-readiness pass + CVE clear + captcha primitive#283

Open
djl11 wants to merge 124 commits into
mainfrom
staging
Open

Release: open-source-readiness pass + CVE clear + captcha primitive#283
djl11 wants to merge 124 commits into
mainfrom
staging

Conversation

@djl11
Copy link
Copy Markdown
Member

@djl11 djl11 commented May 26, 2026

Promotes 6 commits from staging to main. Two themes plus one feature.

Open-source-readiness pass (3 commits)

  • aaabf3d46 chore(repo): tighten .gitignore for build artifacts and add AGENTS.md

    • .gitignore now covers build/, dist/, *.egg-info/, Local/
    • Removed ~12MB of build artifacts from the working tree
    • New AGENTS.md distilled from .cursor/rules/ so Claude Code, Codex, Aider, Cline, etc. pick up the same conventions Cursor does
  • bfe44c46f chore(github): add CODEOWNERS, PR/issue templates, dependabot, OSV scanner

    • CODEOWNERS@unifyai/Engineers as catch-all + explicit ownership of security-sensitive paths
    • PULL_REQUEST_TEMPLATE.md — references the .cursor/rules invariants
    • ISSUE_TEMPLATE/{config,bug_report,feature_request}.yml — routes bugs by surface; steers "please add this skill" feature requests toward GuidanceManager/FunctionManager
    • dependabot.yml — github-actions weekly (grouped) + agent-service/ npm weekly; deliberately skips scheduled pip per the editable-sibling install model
    • workflows/osv-scanner.yml — Google's reusable workflow pinned by SHA, SARIF to Security tab

Dependabot CVE triage (1 commit + 5 dismissals)

Captcha primitive + docs (2 commits)

  • c9ba90982 feat(computer): add solve_captcha primitive for reCAPTCHA v2 via AntiCaptcha
  • 39fe85099 docs(env): document ANTICAPTCHA_KEY placeholder in .env.example

Other in-flight work picked up incidentally

  • bd001c346 test(task_scheduler): pin Communication env-builder equivalence in shared contract tests — landed on staging before this session.

Test plan

The full test suite auto-runs on staging→main PRs (tests.yml line 130). No tags needed. Auto-merge on green.

djl11 added 7 commits May 26, 2026 00:37
…ared contract tests

Adds 4 new tests in test_offline_runner_contract.py that prove
field-for-field that Communication's NEW _build_offline_runner_env
composition (shared Unity contract + hosted-only assistant-identity
layer) produces dicts identical to the OLD monolithic Communication
builder, across the scheduled, triggered, entrypoint-override, and
sparse-assistant-data scenarios.

The golden reference function is a verbatim copy of Communication's
pre-refactor builder inlined into the test file. If anything in the
shared contract drifts from the old behaviour, these tests fail
loudly here, in Unity's test suite, before reaching Communication's
deployment.

Brings total contract-module test count to 35 (up from 31).
…Captcha

Exposes a deterministic, Python-callable primitive
`WebSessionHandle.solve_captcha()` on every web session created via
`cp.web.new_session(...)`. The primitive delegates the visible
reCAPTCHA v2 challenge to the AntiCaptcha worker pool and injects the
returned Google-signed token back into the live page so the page's
own submit flow accepts the verification.

Layers wired:
- agent-service: new `POST /captcha/solve` handler (sitekey extraction
  + createTask/getTaskResult polling + page.evaluate injection).
  Reads `ANTICAPTCHA_KEY` only from `process.env`; token is never
  logged or echoed in the response.
- Python: `ComputerSession.solve_captcha` (+ matching mock-backend
  and `_MockSession` stubs) with rich docstring on
  `_LowLevelActionsMixin`. `ComputerSession._request` gains a keyword-
  only `timeout` parameter (default preserves existing behaviour).
- Runtime exposure: `"solve_captcha"` appended to `_COMPUTER_METHODS`
  and `ComputerPrimitives._LOW_LEVEL_METHODS`; excluded from
  `_DESKTOP_METHODS` (desktop sessions have no DOM target).
- Config: optional `ANTICAPTCHA_KEY` documented in
  `agent-service/README.md`; missing key surfaces as 503
  `anticaptcha_key_missing`.
- Tests: mock-backend coverage in `test_computer_multimode.py`
  guarding the auto-wiring and the default/invisible variant paths.

Magnitude-core is intentionally untouched: the primitive is not in
the LLM action vocabulary. Callers reach for it from their own
orchestration code after a prior `observe()` has confirmed a CAPTCHA
is on screen.

Out of scope: v3/Enterprise reCAPTCHA, hCaptcha, Turnstile,
FunCaptcha, GeeTest, desktop-mode equivalents, and wiring into
specific actor/extractor flows.
Clean up the open-source-ready repo surface:
- .gitignore now covers build/, dist/, *.egg-info/ (any name), and Local/
  so setuptools/uv build output and personal workspace dirs stay out of
  git status. Deleted ~12MB of build/, dist/, unity.egg-info/,
  unify_agent.egg-info/, Local/, __pycache__/, .cache.ndjson from disk.
- AGENTS.md distilled from .cursor/rules/ so Claude Code, Codex, Aider,
  Cline, and other assistants pick up the same conventions Cursor does
  (testing philosophy, no-defensive-coding, explicit-path commits,
  state-manager design rules, repo map).

No code changes.
…anner

Brings .github/ in line with peer open-source AI-assistant repos
(NousResearch/hermes-agent, openclaw/openclaw) so contributors land
on a familiar surface and supply-chain hygiene is visible.

Added:
- CODEOWNERS — @unifyai/engineers as catch-all + explicit ownership
  of security-sensitive paths (CODEOWNERS itself, dependabot.yml,
  workflows/, SECURITY.md, AGENTS.md, ARCHITECTURE.md, secret_manager/).
- PULL_REQUEST_TEMPLATE.md — Summary / type / areas / test plan /
  migration / checklist. References the .cursor/rules invariants
  (no-defensive-coding, no-temporal-comments, zero-backcompat target).
- ISSUE_TEMPLATE/{config,bug_report,feature_request}.yml — bug template
  routes by surface (CLI / voice / installer / specific manager /
  ConversationManager / etc.) and asks for `unity doctor` output; feature
  template explicitly steers users toward GuidanceManager/FunctionManager
  for runtime-extension requests so the issue queue isn't drowned in
  "please add this skill" tickets.
- dependabot.yml — github-actions weekly (grouped minor/patch) +
  agent-service npm weekly. Deliberately skips scheduled pip updates
  per the editable-sibling install model (unify/unillm/orchestra-core);
  CVE-driven pip security updates remain enabled at the repo-settings
  level. Comment explains the rationale.
- workflows/osv-scanner.yml — Google's reusable workflow pinned by SHA.
  Scans uv.lock + agent-service/package-lock.json on lockfile changes,
  push to main/staging, and weekly. SARIF results land in the Security
  tab; fail-on-vuln disabled so pre-existing CVEs don't block merges.
Lockfile bumps only — no pyproject.toml / package.json changes.
Triggered by the 15 open Dependabot alerts on the default branch
(see https://github.com/unifyai/unity/security/dependabot).

uv.lock (7 bumps):
- urllib3   2.6.3  -> 2.7.0    CVE-2026-44431 (high)  cross-origin header
                                  leak in proxied redirects
- urllib3   2.6.3  -> 2.7.0    CVE-2026-44432 (high)  decompression-bomb
                                  bypass in streaming API
- langchain-core 1.3.0 -> 1.4.0  CVE-2026-44843 (high)  unsafe deserialization
                                  via overly broad load() allowlists
                                  (pulls in new transitive langchain-protocol 0.0.15)
- python-multipart 0.0.26 -> 0.0.29  CVE-2026-42561 (high)  DoS via unbounded
                                       multipart part headers
- lxml      6.0.3  -> 6.1.1    CVE-2026-41066 (high)  XXE in default config of
                                  iterparse() and ETCompatXMLParser()
- langsmith 0.7.33 -> 0.8.5    CVE-2026-45134 (high)  public prompt pull
                                  deserializes untrusted manifests
- authlib   1.7.0  -> 1.7.2    CVE-2026-44681 (medium)  OIDC implicit/hybrid
                                  open redirect (not reachable — we don't run
                                  an OIDC provider — but bumped for hygiene)
- idna      3.11   -> 3.16     CVE-2026-45409 (medium)  IDNA encode() bypass
                                  of CVE-2024-3651 fix

agent-service/package-lock.json (2 bumps, via npm audit fix):
- qs        6.15.0 -> 6.15.2   CVE-2026-8723  (medium)  qs.stringify DoS on
                                  null/undefined entries in comma-format arrays
- ws        8.18.3 -> 8.21.0   CVE-2026-45736 (medium)  uninitialized memory
                                  disclosure

Not addressed in this commit (blocked on sibling repos):
- litellm 1.83.4 -> 1.83.10  (clears 4 alerts: 1 critical SQLi in proxy,
   3 high — sandbox escape, RCE via MCP stdio, SSTI in /prompts/test).
   All four CVEs are in the LiteLLM *proxy server* surface, which Unity
   does not run; reachability is effectively zero, but the bump should land
   for defense in depth. BLOCKED: unillm pins litellm==1.83.4 exactly.
   The unillm Dependabot PR is already open at unifyai/unillm#54.
- python-dotenv 1.0.1 -> 1.2.2  (CVE-2026-28684, medium — symlink-following
   in set_key; Unity only reads .env so not reachable). BLOCKED: litellm
   1.83.4 ships an unusual pin (python-dotenv>=1.0.1,<1.0.1+) that effectively
   freezes python-dotenv at 1.0.1. Will unblock once unillm#54 lands and
   `uv sync` brings litellm 1.83.10 in.
The agent-service /captcha/solve handler (added in c9ba909) reads
process.env.ANTICAPTCHA_KEY at request time and returns 503
anticaptcha_key_missing if it's unset. Document the env var alongside
the other optional integration keys so operators know where to put it
without having to read the agent-service README.

The actual key value lives in GCP Secret Manager under
projects/responsive-city-458413-a2/secrets/ANTICAPTCHA_KEY, alongside
the other runtime API keys (ANTHROPIC_API_KEY, DEEPGRAM_API_KEY,
LIVEKIT_API_KEY, etc.). The companion unity-deploy commit adds
ANTICAPTCHA_KEY to setup_k8s_config.py's required_secrets list so
the unity-secrets K8s Secret picks it up automatically on cluster
setup.
@djl11 djl11 temporarily deployed to unity-testing May 26, 2026 13:04 — with GitHub Actions Inactive
@djl11 djl11 temporarily deployed to unity-testing May 26, 2026 13:04 — with GitHub Actions Inactive
@github-advanced-security
Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

The script's `discover_all()` was only recursing into top-level tests/
sub-directories whose names start with `test` — but Unity's convention
is to name per-manager test directories after the manager itself
(contact_manager/, knowledge_manager/, actor/, task_scheduler/,
conversation_manager/, etc.) without the `test_` prefix.

Effect: the staging→main CI matrix was silently collapsing to just 2
entries (tests/test_integration_status/ and tests/test_session_details.py
— the only top-level paths starting with `test`) instead of the ~67
leaf paths that actually exist. Every prior release went green on a
hollow signal exercising none of the manager test suites.

Fix: replace `item.name.startswith("test")` with
`item.name not in EXCLUDE_DIRS`. Safe because `collect_paths()` is
itself gated by `has_test_files`/`has_test_subdirs`, so recursing into
a non-test directory is a no-op. EXCLUDE_DIRS already covers
__pycache__, .pytest_cache, .venv, etc.

Verified locally: `python3 .github/scripts/discover_test_paths.py | wc -l`
returns 67 (was 2), and the output now includes tests/contact_manager,
tests/task_scheduler, tests/actor/*, tests/conversation_manager/*, etc.
@djl11 djl11 temporarily deployed to unity-testing May 26, 2026 13:21 — with GitHub Actions Inactive
@djl11 djl11 temporarily deployed to unity-testing May 26, 2026 13:21 — with GitHub Actions Inactive
djl11 and others added 20 commits May 28, 2026 19:23
… walk, cwd, HOME, pid)

The earlier diagnostic (6c04f36) confirmed venv_dir.exists()=
False between prepare_venv() return and create_subprocess_exec()
on Linux CI. Local repro on macOS doesn't reproduce the
disappearance, and no codepath in unity/, unify/, or unillm/
rmtrees the .unity/venvs/ tree. To narrow which tree level is
being wiped, expand the diagnostic to dump:

  - PID + cwd + HOME at the failure point (in case some sibling
    test changed cwd / HOME after the prepare_venv chdir)
  - Existence of every Path ancestor from python_path up to "/"
    (deepest first), so we can tell whether the wipe is
    leaf-only (just the venv_id dir) or full-tree (.unity/venvs/)
  - Grandparent directory listing (the safe_ctx-keyed dir
    holding all venv_id subdirs for this test context); if it
    exists with OTHER venv-id subdirs, suspect per-venv-id
    cleanup; if missing, suspect higher-level rmtree

The structured error message lets the next matrix run reveal
the actual rmtree scope without further code changes, narrowing
the search to the right code path. No behavior change otherwise
— the structured RuntimeError is still raised in place of the
generic FileNotFoundError.
…introduced third-parties

Two related LLM-eval failures on the conv_mgr/voice fast-brain cluster:

1. test_redundant_checking_guidance_avoids_same_deferral_phrase:
   Scenario: assistant has already said "Let me check on that."
   then a `[notification]` confirms it's checking. On the next
   turn, the LLM said "Let me check on that." AGAIN — exact
   verbatim repeat. The existing "Do NOT over-acknowledge or
   send multiple confirmations" line in Communication
   guidelines is too generic; the LLM took it to mean "don't
   send multiple replies", not "don't reuse the same phrase".

   Fix: add an explicit bullet right under the existing one:
   "Never repeat the same deferral / filler phrase verbatim
   across consecutive turns" — with concrete varied
   alternatives ("Still looking…", "Almost there", "One moment
   more", or stay silent via `wait`). Naming the verbatim-
   repeat case directly avoids the LLM rationalising the
   repeat as a fresh ack.

2. test_demo_introduction_without_name_in_greeting:
   Scenario: boss says "I'm here with Maria — Maria, go ahead
   and ask Alex anything", then Maria (as the next user
   message) asks "can you pronounce my name?" The LLM replied
   "Sure — how do you spell it?" — completely failing to
   carry "Maria" forward from the introduction.

   The voice prompt had no guidance about WHO is currently
   speaking when multiple parties share the line. The model
   sees both messages as role=user and didn't infer the
   speaker switch.

   Fix: add a "Tracking who's currently speaking" sub-section
   to the voice prompt's pacing block. Explicitly states that
   after a "here with X" / "I'll hand you over to Y"
   introduction, the next turn IS X/Y, and self-referential
   language ("my name", "I'm thinking…") refers to the
   introduced person. Names the exact failure case as the
   wrong-answer example: asking "how do you spell it?" after
   "this is Maria" sounds inattentive.

Both are prompt-engineering nudges; no code path changes.
Caches will invalidate naturally since both nudges change the
system prompt text.
…nterjected questions

test_interjection (in tests/transcript_manager/test_ask.py) was
failing:

  1) tm.ask(Q1)  # "When did Dan last speak with Julia on the phone?"
  2) handle.interject(Q2)  # "Did Jimmy ever tell us when he's on holiday?"
  3) handle.result()  # expects answers to BOTH Q1 and Q2

The LLM-judge assertion (_llm_assert_correct with
multiple_answers=True) checks the final reply contains both the
Dan-Julia date AND the Jimmy holiday date. Production LLM
behavior was to abandon Q1 entirely once Q2 came in and reply
with just Jimmy's date — natural conversational behavior but
not what the test (or a power-user querying transcripts)
expects.

The TM ask prompt previously had no guidance about how to
handle interjections. Add a global directive: "treat
interjections as ADDITIVE — final reply must cover BOTH the
original question AND the interjected one, not just the
latest." This makes the test's expectation explicit in the
prompt rather than relying on the LLM to infer the right
behavior.

This is the right semantic for a transcript-query loop
specifically — when someone interjects an extra question while
you're querying transcripts, they typically want both questions
answered, not the first abandoned. The same nudge wouldn't
apply uniformly to all loops (e.g. ConversationManager actions
where interjection often means "course correct the current
action", not "do both") so it lives only in the TM ask prompt.
… when caller asks "what is this about?"

test_triggered_wake_explains_topic_naturally was failing:

  System prompt: voice agent (Alex calling Alice)
  Conversation:
    1) assistant [notification]: "this phone call from Alice
       may relate to the task 'Invoice follow-up'. ... Do not
       mention the task unless it naturally helps."
    2) user (Alice): "Sure, what is this about?"
  Expected response: mentions "invoice", "follow-up", or "alice"
  Actual response: "Hi, how can I help?"

The LLM was being overly conservative about the "do not
mention unless it naturally helps" hedge in the wake
notification. "What is this about?" is the canonical scenario
where mentioning the topic naturally helps — but the fast
brain was treating the hedge as "stay silent about it".

The existing "Notification authority" block only covered
completion notifications. It said nothing about wake-context
(why the assistant is awake / why a call is happening). Add a
new sub-section "Wake-context notifications" that:

  - Explicitly identifies the pattern: "Background context:
    this call may relate to X" / "<task X> is due now"
  - Names "what's this about?" / "why did you call?" as the
    canonical "should mention the topic" trigger
  - Reinterprets the hedge phrasing ("may relate", "still
    deciding", "do not mention unless it naturally helps") as
    "lead with the topic but stay open to redirection", NOT as
    "stay silent"
  - Gives the exact failing example as the wrong-answer
    ("Hi, how can I help?" — ignores the context I was given)
  - Gives concrete right-answer phrasings ("Wanted to follow
    up on the invoice — is now a good time?")
  - Reiterates: never quote internal phrasing aloud (already
    enforced elsewhere, but adjacent to this block for clarity)

Prompt-engineering nudge only; no code paths changed.
…stant to skip CI-broken wake-up

The per-test ephemeral-assistant fixture (added earlier in this
session) was failing in CI with 500 Internal Server Error on
every POST /v0/assistant call. Orchestra-side stack trace shows:

  views.py:788 create_assistant
    -> assistant_infra.py:1652 wake_up_assistant
      -> httpcore.UnsupportedProtocol: Request URL is missing an
         'http://' or 'https://' protocol.

`wake_up_assistant` builds
`_adapters_url_for(deploy_env) + "/assistant/wakeup"`. In CI
there is no Communication / adapters service running, so
`_adapters_url_for("")` returns "" and httpx rightly refuses
to POST to a protocol-less URL.

The orchestra view (Phase 3 in create_assistant) only calls
wake-up when `not assistant_in.is_local`. The intent of
`is_local=True` is "unity runs locally, no remote infra
needed" — exactly what every CI test wants. The fixture
already passes `create_infra=False` (skips pubsub/VM
provisioning); adding `is_local=True` skips the adapters
wakeup webhook too, completing the "no external services
required" picture.

Long-term, orchestra's `wake_up_assistant` should fail
gracefully (or no-op) when `_adapters_url_for` returns empty,
since a 500 here masks the real configuration issue.
Cross-repo fix can land separately; the test-side flag is the
correct caller behavior anyway (CI assistants are local
fixtures by definition).
…s.agent_id

Two unrelated function_manager/python failures from the CI matrix
that were blocking the whole cluster:

1. `uv sync` failing with "Distribution not found at:
   file:///tmp/.../<venv_dir>" on every test that creates a venv:

   The synthetic pyproject.toml generated for each user venv
   only declares `[project]` + `dependencies`. It is NOT an
   installable package. Without `--no-install-project`, uv tries
   to install the project itself (in editable mode), fails to
   find a build backend / sdist for the empty venv_dir, and
   surfaces "Distribution not found" — which then masquerades as
   a venv-creation failure.

   Adding `--no-install-project` to the `uv sync` invocation
   tells uv "install dependencies into .venv, do NOT install the
   project itself". The project manifest is just a dependency
   declaration for us, never a real package. Verified the flag
   exists in current uv. The CI failure was reliably reproducible
   across every test in the `test_venv_*` family.

2. `AttributeError: AssistantDetails ... has no attribute 'id'`
   in test_remote_windows fixtures:

   `mock_session_details_windows` and `mock_session_details_ubuntu`
   were calling `monkeypatch.setattr(SESSION_DETAILS.assistant,
   "id", "test-assistant")`. Same legacy `.id` → `.agent_id`
   rename that bit contact_manager tests earlier. AssistantDetails
   is a Pydantic model with the new field name `agent_id` (int);
   setting an arbitrary attribute fails because the model rejects
   unknown fields.

   Switched the fixtures to set `agent_id` to a unique int per
   fixture (999_001 / 999_002) to keep the two windows / ubuntu
   variants distinguishable in any downstream test assertions.
…gger

The earlier fix (f9d2289) called caplog.set_level(INFO, logger="unity"),
on the assumption that this would also subscribe caplog to the named
logger. That assumption was wrong:

  - pytest's caplog.handler is registered with the ROOT logger only,
    via the `propagate=True` chain.
  - `caplog.set_level(level, logger=name)` sets the LEVEL on the named
    logger but does NOT attach caplog.handler to it.
  - unity/logger.py sets `LOGGER.propagate = False` (since 5ed695f,
    2026-02-20 "Consolidate logging into unity.logger as single
    authority"), so unity log records never reach the root handler.

Net effect: caplog.records stays empty for unity LOGGER output even
after set_level(..., logger="unity"). Verified locally with a minimal
test (`logging.getLogger("unity")` + `LOGGER.info(...)`) — without
the explicit `addHandler(caplog.handler)`, the captured records list
is empty; with the explicit add, the message appears immediately.

Fix: explicitly `logging.getLogger("unity").addHandler(caplog.handler)`
at the top of the test body, wrap the assertion block in try/finally,
and `removeHandler(caplog.handler)` in finally so the handler doesn't
leak across tests (each test gets a fresh caplog with a fresh handler,
and a stale handler would write to a disposed buffer).

The existing `set_level(..., logger="unity")` call is left as-is so the
unity logger's effective level still includes INFO during the test
(otherwise INFO records would be filtered before reaching any handler).
…NGS picks them up

The env vars in `pytest_configure(config)` (USER_DESKTOP_CONTROL_ENABLED,
ASSISTANT_EMAIL, ASSISTANT_NUMBER, ASSISTANT_WHATSAPP_NUMBER) were
landing too late.

Order of operations:
  1. pytest collects this conftest.py → runs `from tests.helpers import ...`
     at module top
  2. tests/helpers.py transitively imports unity modules
  3. unity.settings instantiates `SETTINGS = ProductionSettings()` —
     pydantic-settings reads env vars **once**, at this point.
  4. pytest_configure() runs — by now SETTINGS is frozen, so the env
     overrides are silently ignored.

Symptom (caught in conv_mgr/flows + conv_mgr/voice + conv_mgr/core CI):
  - test_can_you_use_my_computer: LLM answered "Not directly" instead
    of "Yes — install a quick remote-access tool from unify.ai",
    because SETTINGS.conversation.USER_DESKTOP_CONTROL_ENABLED was
    False at SETTINGS-instantiation time → the desktop_access_faq
    prompt branched the wrong way.
  - test_reply_adds_re_prefix_to_subject: LLM didn't emit EmailSent
    because `send_email` wasn't surfaced as a tool (gated on non-
    empty assistant.email; SESSION_DETAILS.assistant.email was still
    "" because ASSISTANT_EMAIL env var hadn't been read at populate
    time).

Fix: hoist the env-var setdefaults to the very top of conftest.py,
BEFORE the `from tests.helpers import ...` line. Add a header comment
documenting the timing requirement so future hands don't move them
back into pytest_configure() "for tidiness". The redundant copies in
pytest_configure() stay as a defense-in-depth (in case a downstream
test reimports SETTINGS) but the authoritative point of truth is
now the module top.
…exist, restore _sync_required_contacts

In commit 2b07266 I renamed the SyncContacts handler call from
contact_manager._sync_required_contacts → _provision_system_overlays
in both event_handlers.py and the matching test. The rename was
wrong: _provision_system_overlays is NOT defined anywhere on
ContactManager / BaseContactManager / SimulatedContactManager —
only _sync_required_contacts is.

Symptoms:
  - Production: every SyncContacts event raised AttributeError
    on cm.contact_manager._provision_system_overlays (caught and
    logged by the try/except in event_handlers.py:_sync_contacts,
    silently dropping the sync — no boom in logs, just no-op).
  - Tests: test_queue_operation_waits_for_initialization patched
    the same wrong name; production handler now AttributeError'd
    inside the try/except, never reached the (non-existent)
    overlay call, mock_sync.called stayed False, assert_called_once()
    failed with "Expected '_sync_required_contacts' to have been
    called once" (mock display name inherited from the wraps=
    target, which itself failed to resolve).

Fix is purely the revert. Restore the actual production method name
in BOTH the handler and the test. Verified locally
(`pytest test_queue_operation_waits_for_initialization -xvs` → 1
passed).

Lesson for future renames: confirm the target method exists on
every concrete implementation (BaseContactManager,
ContactManager, SimulatedContactManager) before committing the
rename — `git grep "def _provision_system_overlays"` would have
shown zero matches and flagged this immediately.
…n't depend on cwd

test_screenshot_crop_via_act was failing with:
  FileNotFoundError: [Errno 2] No such file or directory: 'Screenshots'

`generate_screenshot_path(entry)` returns a relative path
(`Screenshots/User/<ts>.jpg`). Production code calls it from
inside a worker that runs in `cwd=local_root` — see
`conversation_manager/main.py:os.chdir(_local_root)` early in
startup. Test fixtures spin up CM in-process via CMStepDriver
without that chdir, so when the test calls
`write_screenshot_to_disk` directly, the relative path resolves
against whatever cwd pytest is in (usually the repo root), and
`p.parent.mkdir(parents=True)` raises because `./Screenshots/`
isn't there.

Earlier in the test we already mkdir'd
`local_root / "Screenshots" / "User"` (line 61) and the test
later globs `local_root / "Screenshots" / "User"` for the
written file (line 108), so the test already KNOWS where the
screenshot should live — it just wasn't telling
write_screenshot_to_disk explicitly.

Fix: absolutise the path before the disk write:
  screenshot_path = str(local_root / generate_screenshot_path(entry))

This keeps the production codepath (relative + cwd chdir)
untouched and makes the test independent of cwd. No prod change.
…on exit (parallel race)

ROOT CAUSE found for the function_manager/python "venv python
disappeared between prepare_venv() and create_subprocess_exec()"
RuntimeError that has been failing reliably on every CI matrix
run for weeks.

The pytest_unconfigure hook was calling
`shutil.rmtree("/tmp/unity_test_home")` at the end of EVERY pytest
session. parallel_run.sh launches one pytest session per test in
parallel tmux panes, all of them sharing the same deterministic HOME
(by design — LLM cache keys embed ~/Unity/Local and must stay
stable). When session A finishes, its pytest_unconfigure wipes the
entire tree — including the venvs that session B/C/D's
FunctionManager just created under
`$HOME/Unity/Local/.unity/venvs/<ctx>/<id>/`. The next
`execute_in_venv` in B/C/D then sees:

  venv_dir=/tmp/unity_test_home/Unity/Local/.unity/venvs/<ctx>/0
  exists=False
  ancestor existence: False all the way up to /tmp/

The earlier diagnostic dump (commit 1e9a5f4) confirmed the wipe
scope was full-tree, not leaf-only, ruling out per-venv-id cleanup.

Fix: drop the rmtree. The shared HOME stays, but:
  - CI runners are ephemeral — the dir is reclaimed when the
    runner shuts down.
  - Local dev: users can `rm -rf /tmp/unity_test_home` manually.
  - Test isolation is already enforced by per-test Unify contexts
    (each FunctionManager venv lives under a context-keyed
    subdirectory), so leftover files from one test do not affect
    another.

This single-line change unblocks every test in
function_manager/python and likely fixes a long tail of
intermittent "vanishing fixture file" failures elsewhere too.
… doesn't time out

Five test_remote_windows tests were failing in CI with:
  RuntimeError: Managed VM did not become ready within 5 minutes

The production helper
`_execute_python_function_on_remote_windows` waits on the
`_vm_ready` threading.Event (defined at
`unity/function_manager/primitives/runtime.py:56`). That event is
set in production by either:
  - ConversationManager startup
    (`unity/conversation_manager/main.py:278`)
  - ComputerPrimitives mock path
    (`unity/function_manager/primitives/runtime.py:643`)

In a pure FunctionManager unit test neither codepath runs, so the
wait blocks until its 300s timeout fires.

Fix: pre-set the event in both `mock_session_details_windows` and
`mock_session_details_ubuntu` fixtures (the two fixtures any
remote_windows test ever uses). Capture the prior state so we
clear it again on teardown if WE flipped it on, leaving the
global event in its previous state for any sibling test that
happens to share the process.

This is the smallest surgical fix — alternatives like patching
the wait helper or refactoring `_execute_python_function_on_remote_windows`
to accept an injectable readiness signal would have a much wider
blast radius for no real benefit on the test side.
resolve_bot_token queried Orchestra with team_id and without include_token, so /slack/send could never fetch a token (400/503). Key on slack_team_id and request include_token. Add POST /slack/user-info (users.info) returning email + real/display name so the inbound pipeline can resolve an unknown Slack sender to a contact.
…name

On first inbound from an unmapped slack_user_id, look up the sender via the gateway /slack/user-info and match an existing contact by email then by real/display name (ambiguity-refuse), persisting slack_user_id so later messages match directly. Addressed (@app) senders with no match get a respondable contact; others keep the gated unknown-contact policy. Breaks the bootstrap deadlock that dropped every first Slack message.
…dempotent

A channel @mention is delivered twice (app_mention + message) with distinct event_ids but the same client_msg_id; dedup now keys on the stable message id so the pair collapses to one processed event. _create_slack_contact pre-checks and, on a lost unique-slack_user_id race, resolves to the existing contact instead of dropping the message.
)
logger.info(
"sent Slack message to %s on team %s (ts=%s)",
channel_id,
logger.info(
"sent Slack message to %s on team %s (ts=%s)",
channel_id,
team_id,
juliagsy and others added 9 commits May 29, 2026 16:43
…tact orphans

Slack bot_user_id is workspace-scoped (on the install), so the per-assistant assistant_slack_bot_user_id is never set at bootstrap and the brain never gets the Slack send tools (always waits). Adopt the bot id from the inbound event when handling SlackMessageReceived/SlackChannelMessageReceived so the triggered brain run exposes send_slack_message/send_slack_channel_message. Also stop _create_slack_contact from minting a nameless, email-less contact that captures the slack_user_id and shadows the real contact; require a name or email and otherwise leave the sender for email/name resolution.
Surface the inbound message's event_ts as the effective thread_ts for
Slack channel messages so a top-level @mention reply starts a thread
instead of posting at the channel root. DMs keep prior behaviour and
only thread when already threaded.
…ility demos

Adds a collapsible README section illustrating six interactions the nested
steerable-handle model enables (live deep redirect, live introspection,
pause/inspect/resume, concurrent independent steering, clarification bubbling,
and single-branch stop), each with its own generated diagram. Allow-lists the
new demo images in .gitignore alongside the existing repo-shipped diagrams.
…e drifted)

The container entrypoint only execed the headless offline runner when
UNITY_OFFLINE_TASK_MODE == "function", but offline_runner_contract sets it to
"actor" (pinned by tests/task_scheduler/test_offline_runner_contract.test_mode_is_actor).
So the gate silently stopped matching: every offline scheduled/triggered task
fell through to the live ConversationManager, which booted, idled on a startup
reply, and exited WITHOUT executing its function — leaving the run row stale at
"running" and never cloning the next recurrence. Match on any non-empty
UNITY_OFFLINE_TASK_MODE so it's rename-proof. This entrypoint is baked into the
unity base image; the unity-deploy/base/entrypoint.sh mirror is kept in sync.
OpenClaw and Hermes Agent both store skills as agentskills.io SKILL.md
files (YAML frontmatter + markdown body + optional bundled scripts),
which map near one-to-one onto GuidanceManager entries. This adds a
reusable parsing/mapping core plus convenient openclaw_to_guidance and
hermes_to_guidance CLIs so either library (~247 skills combined) can be
imported off-the-shelf as guidance.

The transfer is deliberately faithful: a skill's description + body
become the guidance content and bundled scripts are inlined verbatim
as textual reference (promoting them into runnable FunctionManager
functions is left as a separate step). CLIs default to a dry run, are
title-namespaced to avoid cross-repo collisions, and support
skip/overwrite conflict handling on re-runs. Includes pure parsing
tests and end-to-end GuidanceManager import tests, plus README docs.
Provide a reusable common helper for exact semantic top-k merging across multiple contexts by fetching offset+limit candidates per source, sorting by exposed score, and applying the final window once. This keeps future built-in-library reads from hand-rolling pagination and tie-breaking logic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants