v0.10.5 cycle - #188
Conversation
…on a duplicate tag
The dsh row rendered 'Register DSH plugin' unconditionally because mcp_registered() had no dsh arm — it answered false for every vendor without a native MCP config file, so an operator whose ~/.dsh web profile already carries our bundle and configured ccteam-client row still saw the CTA and could not tell registered from unregistered. Answer it at the layer that owns the fact: a read-only detector next to the writer (both halves required — bundle alone leaves the plugin with no ACP listener), surfaced through the same mcp_registered field the five config-file vendors use, so the row renders the ✓ state and the CTA only appears when there is something to do.
…st/client stubs) and backlog cards
…actory build
The browser cannot reach ccteam directly: ccteam-web installs no CORS layer, so
the panel speaks only to this plugin's own /ccteam/api prefix route and the host
proxies upstream. That also keeps the REST token out of the browser.
BFF (src/bff.ts, src/sse.ts)
- One prefix route registered exactly once; the disposer goes to ctx.effect.
DSH's webServer throws on a duplicate (kind, path), so registration is
single-shot by construction.
- POST /ccteam/api/<method> for the five contract methods, GET /events for SSE,
anything else 404 JSON.
- Upstream mapping verified against crates/ccteam-web rather than guessed:
Authorization: Bearer ccteam:<hex> (a bare hex paste is normalized; the
daemon rejects bare hex in that header), GET /api/v1/capabilities as the
cheap vendor probe, GET /api/v1/agents/{graph,events},
GET /api/v1/sessions/{sid}[/events], POST /api/v1/sessions/{sid}/turn, and
the project-scoped POST /api/v1/projects/{slug}/sessions.
- Honest receipts: the queued turn (202 + queued_behind) surfaces as
{ok,queued,queuedBehind}; failures carry errorKind, preferring the daemon's
stable error_code. A 401 is text/plain, so parsing is content-type guarded.
- SSE fan-out keeps at most one upstream per stream, refcounted by downstream
subscribers; the last disconnect tears it down, a drop retries with capped
backoff while any subscriber remains, and 25s heartbeats keep proxies off.
turn_done is emitted only from the global stream so a sid watcher (subscribed
to both) cannot double-count.
Credential hygiene: the token lives in closure scope, is never written to
process.env, never logged, and never appears in a response or frame.
Build (tsdown.config.ts, build/)
- Host -> lib/index.js plain ESM; client -> lib/client.js in DSH's
closure-factory format (CJS + banner/intro/footer) so the bundle calls
window.__ModuleLoader__.load({id, factory}) with id byte-equal to the package
name and the cordis.patch.yml row.
- Externals are the eight platform seed keys, matched by exact string; a purity
gate fails the build on any other @deepseek-ai/* value import.
- Minimal CSS Modules support (hashed class map + tagged style injection at
factory execution) in place of lightningcss.
Tests: 56, zero real network. fetch is injected, the fake webServer reproduces
the duplicate-route throw, and the build test executes the artifact against a
stand-in loader instead of asserting bytes.
Deviation: contract.ts gains an optional SpawnRequest.project (strictly
additive). ccteam's create-session endpoint is project-scoped and never infers
a project, so without it a spawn has nowhere to land; the host falls back to a
new defaultProject setting and otherwise refuses with an actionable error.
…he file stays text
…sion-gated mount The @ccteam/dsh-team client half: a hand-rolled reducer store (view stack, badge, recents, transcripts, receipts), a contract-typed BFF client with injectable transports, and the panel tree composed exclusively from @deepseek-ai/dsh-client-ui-primitives + semantic --dsw tokens (CSS modules, zero globals, zero ccteam-web imports). Mounting is version-gated: entry button into sidebar.footer.action and panel into shell.overlay via slots.inject, degrading whole to a body portal when either slot is missing or rejects. Copy flows through the DSH locale service (zh/en, locale: NS register option). Store and api unit tests.
…al/shape tests SpawnForm gains the project seat (slugs from team.graph; exactly one = auto-picked and hidden; last choice persisted under ccteam.console.project) and spawn responses flow through a pure planSpawnOutcome: a sid present even on ok:false still navigates into the session with the error stated in its chat, no sid keeps the form up with the host's actionable error. Stream errors now re-probe status instead of declaring unreachable, the checking phase is initial-only (no flicker on SSE retries), SessionChat is keyed by sid (no draft bleed across sessions), and the drag handle handles pointercancel. New suites: version gate (slot mode, whole-mount fallback on throw/absence/deadline, contained fallback crash), registration shape via recording ctx, material guards (semantic-token families, no hex/rgb/static, import allowlist, zh/en parity).
…s, 16-glyph send icon, drop dead locale key
Materializing `@ccteam/dsh-client` was a one-plugin code path; the panel `@ccteam/dsh-team` needs the same treatment, so make the path plural once instead of adding a second copy of it. - `CCTEAM_PLUGINS` table (bundle / package / row id / cache namespace / embedded bytes) drives extraction, node_modules links, `dsh.profile.bundles` and the patch rows. `ProfileSpec.vendor_bundles` now lists only vendor bundles: ccteam's own are implicit, so a third plugin is one table row and reaches the tenant and operator paths at once. - Each plugin caches under `runtime/dsh/<ns>/<sha>/`, so bumping one never invalidates the other. - The panel row is an OVERRIDE, never an `insert`: `@ccteam/dsh-team` ships a bundle patch that inserts `ccteam-team` itself, and a second insert aborts the whole Cordis boot on `duplicate loader entry id` — the v0.10.0 failure, identical in shape. Config keys stay flat (`daemonUrl`, `restToken`). - Tenant runtimes get their own REST bearer wired in on start: reuse the token the identity already signs in with, mint only where the store itself would (`DshRestTokenResolver`, injected from ccteam-web because the token stores sit above this crate). An unknown user is refused, never created. The token is written to that identity's own profile and nowhere else — never logged, never in a response. A resolver failure degrades to an unconfigured panel rather than failing the DSH runtime. - The operator's `~/.dsh` gets both plugins merge-only with NO credential, mirroring what `enrollment: None` already means there: ccteam does not put secrets in a home it does not own, and Settings covers it. Hosts-page detection now answers for every plugin, so a client-only profile reads as not registered and the button keeps offering. - `plugins/pack-assets.sh` rebuilds both embedded tarballs and refuses to publish one whose `bundledDependencies` did not land — packing without installing first yields an archive that extracts fine and then cannot resolve `@deepseek-ai/schemastery` at runtime, invisible to every Rust test. It reproduces the committed dsh-client.tgz byte for byte. - `plugins/dsh-team/package.json` gains the `bundledDependencies` block dsh-client already had; without it the packed panel cannot boot. Pain point: requirements.md "跨 vendor 身份/路由" — mode 1 gets the team panel with zero user steps. tech-design: DSH plugin materialization (K25).
…e the panel The rc.7 scanner resolves <pkg>/package.json and silently treats any resolve failure as 'not a client package'; an exports map without ./package.json made the panel vanish from the web console with no error anywhere. Found on metal, reproduced outside the app, locked with two regression tests (109/109).
…ion wakeups
`gateway::tests` shared the literal `/tmp/alpha` project dir across dozens of
tests. Under full-parallel `cargo test` they shared both a filesystem path and
the `s1` sid namespace, so unrelated tests drained each other's pending-turn
files and resume state — the near-deterministic red registered in
`.loop/verify/README.md`. Every construction site now takes its own
`tempfile::TempDir`, matching the tempdir pattern already used elsewhere in the
file. The `/tmp/alpha-<suffix>` sites go too: unique per test within a run, but
they persisted BETWEEN runs, so a previous run's residue leaked into the next.
Three timing defects that the shared path was masking, all test-side:
- `FakeAdapter::events()` handed every session's pump a clone of ONE `Notify`.
`notify_one` gives its single permit to an arbitrary waiter, so a pump for a
different session could wake, find nothing matching its identity, consume the
permit and park again — stalling the pump the event was for. Wakeups are now
keyed by thread identity, which is the fix the verify README itself named.
- `concurrent_replacement_discards_stale_resume...` sampled `fake.closes` the
instant the conflict error arrived, but `apply_resume_dead_session` closes the
orphan thread in a detached `tokio::spawn` BY DESIGN. Poll it to the same
expected value, bounded so a real regression still fails.
- Both `turn_answer_*context_echo*` tests asserted `ends_with("→ alpha/s1 ...")`.
`context_echo_line` also renders the session title, and the first message's
auto-title is written to meta.json asynchronously — so the bare form silently
asserted "and the title write has not landed yet". They now match the trailing
block's head, which is what the tests are actually about.
Tests only: every change is inside `#[cfg(test)] mod tests`, no production code
and no assertion intent altered. `cargo test -p ccteam-im --lib` 627/0 on three
consecutive full-parallel runs; the two echo tests 30/30.
Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
… binaries
Four tests mutated process-wide `HOME`/`CCTEAM_HOME`/`DEEPSEEK_API_KEY` from
inside `#[cfg(test)] mod tests`, so they ran in the SAME process as every other
lib test at full parallelism — the AGENTS.md §六 red line. The two groups used
DIFFERENT `serial_test` keys (`#[serial]` vs `#[serial(dsh_config_env)]`), so
they did not serialize against each other: that is precisely the registered
3-in-10 flaky pair. An integration binary is its own process, which closes it.
- `model_catalog::env_resolution_prefers_ccteam_home_and_falls_back_to_home`
→ new `tests/model_catalog_env_test.rs`. It exists to cover the
`ccteam_root_from_env` wiring, so injecting a root would have deleted the
very thing it tests; a separate process is the only fix that keeps it.
- `spawn_spec::{tenant_web_seed_refreshes_unmodified_files_from_operator_home,
identity_home_maps_tenants_managed_and_everyone_else_to_the_operator_home,
config_source_prefers_tenant_home_with_credentials}`
→ existing `tests/dsh_acp_test.rs`, which already covers this surface and
already has `isolate()` pinning BOTH homes with RAII restore. All three only
pinned `HOME`, never `CCTEAM_HOME` — which outranks it in the root resolver,
so an exported `CCTEAM_HOME` could have sent "isolated" writes to the real
`~/.ccteam`. The seed test reaches the private
`seed_or_refresh_tenant_web_config_home` through the public
`build_web_spawn_spec` door, exactly as its six new neighbours do; no
visibility was widened.
Manual env restore is replaced by RAII guards throughout: a panic mid-test used
to leak a tempdir-pinned HOME into the rest of the binary.
Tests only: every change is inside `#[cfg(test)]` or a `tests/` file, no
production code and no assertion dropped. Lib count 536→532 (the 4 relocated);
`dsh_acp_test` 22→25 passing with the same 4 registered default-model failures;
`model_catalog_env_test` 1/1. `cargo test -p ccteam-harness --lib` 532/0 on
three consecutive full-parallel runs.
Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
…ncovered Narrow write-back on the card this dev session took: status line + verification and deviation sections. Notes the newly characterized (and deliberately unfixed) `delegation_reconcile_folds_missed_backlog_into_one_notification` race for the planning session to adjudicate — it reproduces at the same 1-in-20 rate on pristine dev, so it is not this card's regression. Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
… (not fixed, needs adjudication)
…g harness tests to integration bins Verified independently by a codex checker (session s422): 7/8 claims bit-for-bit confirmed (line numbers, test counts, fn-name diffs all reproduced); item 8 (an unrelated pre-existing reconcile flake at ~1/20) diverged only on sample-size noise, not a regression — checker's own conclusion.
…n, tech-design pointer)
…served for the daemon
CLI-ENVTEST-1. CI's deterministic-baseline job went red twice in one day on commits that only touched a backlog file: `ccteam-cli`'s `web_chat_newproject_scaffolds_registers_and_cd_works` read a `config.yaml` that was not where it left it. `ccteam-cli` has no lib target, so every `#[cfg(test)]` module under `src/` runs in ONE process; `web_chat_bridge`'s tests `set_var`'d HOME/CCTEAM_HOME and restored them, `commands.rs`'s peek tests did the same with CCTEAM_HOME/CCTEAM_MUX_BACKEND under a different lock, and the loser of that race wrote its files into a stranger's tempdir. Fixed at the layer that reads the environment, not at the symptom: - `DaemonArgs::ccteam_root` — the sibling `registry` never had. `registry` already injected `projects_root`; the root half was still read from `CCTEAM_HOME` at each use site. One resolver (`daemon_ccteam_root`) now decides it once per daemon run and every root-relative path takes it as an argument: credentials, the bot registry, `config.yaml`, the gateway's project-creation + persistence roots, and the durable outbound ledger. `None` keeps the env-derived production root, so `ccteam start` is unchanged. - `peek_session_by_name_with(backend, name)` — the mux backend is handed in; the `CCTEAM_MUX_BACKEND` read stays at the CLI boundary in `ccteam_harness::from_env`. `resolve_peek_session_name` splits the pure name-resolution rule out so it can be asserted directly. The three web-chat tests and both peek tests now touch no environment at all and keep their assertions (the peek pair gained strength: exact session-name equality, and the capture contract asserted through a recording double rather than inferred from an error string). Nothing had to move to `tests/*.rs`. `/newproject`'s `pre_trust_project` is the one step that still resolves off `$HOME`; it is disabled once per process via the helper AGENTS.md §六 prescribes — monotone, never restored, so it cannot race a reader. `test_hygiene.rs` makes the rule mechanical: it scans `#[cfg(test)]` regions under `crates/ccteam-cli/src/**` for `set_var`/`remove_var` and fails on any hit. Its own scanner is tested against production code, a comment and a string literal, and the guard was confirmed to fail on a temporarily injected `set_var`. Refs AGENTS.md §五 (治病根 — one layer, once), §六 (env-mutating tests). Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
…eek backend, env-mutation guard Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
ccteam rebuilds the operator's `package.json` and `cordis.patch.yml` from a parsed model on every daemon start and every `register-mcp` call, so serializing normalizes formatting they never asked to change — `serde_json` sorts object keys, `serde_yaml` renders a flow mapping as a block one and prefers single quotes. Comparing the result to disk as BYTES therefore calls a file "changed" that says exactly what it said before, and rewrites their profile every run. Ask the question by meaning instead: parse both sides and compare the documents. Equal ⇒ return without touching the file, so a second registration leaves every byte and the mtime alone (and `set_private_file` skips the chmod when the file is already 0600). Semantic preservation of the operator's rows is the requirement; byte-identical formatting of a file ccteam legitimately edits is not — the first ccteam write does normalize that file, and `serde_yaml` drops YAML comments, which the write site now says out loud. A comment-preserving round-trip needs a CST-level YAML parser and nothing in this workspace's dependency graph has one; no crate was added for it. When a write IS needed it now goes through a sibling temp file and a rename (the previous path truncated in place), with the destination's mode carried onto the temp file so the credential-carrying patch file is never briefly world-readable. The ownership gate is untouched. Maps to: requirements.md "ccteam does not rewrite the user's files"; tech-design.md §2.2 (DSH profile materialization).
…mments; later registrations are zero-write
CLI-ENVTEST-1 addendum. Two things: the peek test that fixing this card broke on CI, and the same defect everywhere else in the workspace. CI first. `run_peek_uses_state_tmux_session_for_meta_project` failed on dev@be368490 after 5s in `RmuxBackend connect_or_start`. The claim in the previous commit that name resolution is pure was wrong: `resolve_chat_session_name` enumerates live sessions through a backend built from `CCTEAM_MUX_BACKEND`, so dropping the old `=tmux` pin moved it onto the default rmux backend and it went looking for a daemon on the runner. Fixed where the env is read, not in the test: `resolve_chat_session_name_with` takes the backend, `run_peek_with_backend` threads ONE backend through both the lookup and the capture (they used to build two), and the test hands in the recording double with a session list. It now asserts the rule structurally — live panes exist but none is this slug's, so peek falls back to `ProjectState::tmux_session` — plus a new case for the two branches the old test never reached: a live chat session wins, and an explicit sid answers without listing at all. Then the sweep. The guard now walks every `crates/*/src/**` rather than only ccteam-cli's, and found 17 call sites in 6 files. Each fixed at the layer that reads the env: - `ccteam-core` `write_project_settings_in(paths, …)` — `bootstrap_project` was handed `paths` and then let `effective_hook_sh_path` re-read the root from `CCTEAM_HOME` two frames down, so a caller whose `paths` disagreed with the env stamped a hook path from the wrong home. - `ccteam-harness` `resolve_socket_path_from(override, codex_home, home)` — pure; its test now covers all three tiers instead of only the override. - `ccteam-im` `Gateway::reply_wait` + `set_reply_wait` — the paneless first-event wait is resolved once at construction and carried on the gateway (and on `UnlockedTurnPlan`) instead of re-read from env on every turn by three separate call sites. - `ccteam-im` `ask_permission_with_ttl(…, ttl_secs)` and `permission_prompt_timeout_secs_from(raw)` — the TTL is a parameter; the env read stays in the wrapper. - `ccteam-im` `hub_base_from(raw)` — pure; the test now covers both the default and the override without unsetting anything. - `ccteam-im` `daemon_boots_and_exits_on_max_runtime` — uses the `ccteam_root` + `registry` injections instead of pinning HOME/CCTEAM_HOME. No test moved to `tests/*.rs`: every one of them was made env-free, so the baseline rises rather than falls. Three `env_lock` helpers are gone with the mutations they guarded. The scanner also had a real bug: it sliced sources by byte index, which panics the moment it meets a multi-byte character — and this workspace is full of CJK comments. It is byte-only now. Refs AGENTS.md §五 总纲 (同形扫一遍), §六 (env-mutating tests). Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
…ixed, CI green Co-Authored-By: firstintent <239836187+firstintent@users.noreply.github.com>
…efix incident recorded
…y an unsealed update-ladder repro)
…ldown's native binding
…hip to be confirmed by owner
…/ccteam-ui@0.10.4-alpha.0 on next)
…ll, AGENTS/state/history/backlog distillation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6dad1c2a31
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (enrollment() === undefined && isLoopbackUrl(daemonUrl())) { | ||
| void enrollmentBootstrap.ensure() |
There was a problem hiding this comment.
Bootstrap credentials only after the daemon is ready
On a fresh DSH installation where the host services are already available but no daemon or token file exists, this fire-and-forget enrollment attempt runs before the later web-face callback installs/starts the engine. restToken() therefore calls the read-once token bootstrap while secrets/web-token is absent and permanently caches undefined; enrollment fails, and subsequent workbench requests and MCP tools remain unauthenticated even after the daemon becomes ready, until the plugin process is restarted. Chain enrollment/token bootstrap after supervisor.ensure() succeeds, or allow a failed pre-start token read to be retried.
AGENTS.md reference: AGENTS.md:L29-L29
Useful? React with 👍 / 👎.
| let existing = list_in(root) | ||
| .into_iter() | ||
| .find(|c| c.owner == owner && c.scope == scope && c.label.as_deref() == label); |
There was a problem hiding this comment.
Serialize enrollment get-or-mint operations
When two POST /api/v1/enroll {ensure:true} requests for the same owner/scope/label run concurrently, both can observe no existing record here and each mint a different credential file. Both callers receive valid bearers, while later ensures arbitrarily select one of the duplicate records, violating the advertised one-slot/idempotent contract and leaking credentials across repeated concurrent boots. Protect the lookup-plus-mint/revoke sequence with a lock scoped to the enrollment store.
AGENTS.md reference: AGENTS.md:L29-L29
Useful? React with 👍 / 👎.
| const port = parsed.port !== '' ? parsed.port : parsed.protocol === 'https:' ? '443' : '80' | ||
| return ['start', '--web-bind', `${parsed.hostname}:${port}`, '--json'] |
There was a problem hiding this comment.
Resolve hostnames before constructing the daemon bind
If a user configures a local URL such as http://localhost:7333, isLoopbackUrl classifies it as locally supervised, but this produces --web-bind localhost:7333. The Rust launcher later parses that value as SocketAddr, which accepts numeric IP addresses rather than hostnames, so both auto-start and the Start button fail instead of launching the bundled engine. Resolve the hostname to a bindable address or reject/normalize such URLs before building the CLI arguments.
AGENTS.md reference: AGENTS.md:L20-L20
Useful? React with 👍 / 👎.
Second DSH plugin
@ccteam/dsh-team: the ccteam team panel inside the DSH native web console — cross-vendor session tree, embedded chat, one-click spawn, mounted via DSH's ownsidebar.footer.action+shell.overlayslots and rendered exclusively with DSH-native primitives and design tokens.Maps to requirements.md pain points 1/7 (one entry point to the whole team; progress visibility) and tech-design §6 extension surfaces. Spec: docs-local/versions/v0-10-4/README.md (local).
🤖 Generated with Claude Code