Reconstitute the exact runtime and executable proof from Core 4651beb4
without importing its broad unlanded history. The four affected source
files still match that fix parent byte-for-byte. Preserve current
subsystem contracts while inserting the existing typed-filter obligations.
Boolean true and false survive MCP HTTP projection; path and private
metadata, supported value types, write bodies and authentication stay bounded.
Original-contribution: https://github.com/rcourtman/Pulse/pull/3097
Reported-by: X1pheR (Pulse PR #3097)
Source-Commit: 4651beb4c8
Change-source: pulse-maintainer
With the web chat window and its backend routes retired (#3126,
#3137), the pulse_assistant surface only serves Pulse Mobile's chat.
Its manifest description still called it the explanation and handoff
surface for Patrol findings, and that text is public: the docs/AI.md
overview, the MCP README surface block, and the External agents
settings panel all show it. It now names the Pulse Mobile role and says
the web chat is retired.
The browser's fallback affordances for an Assistant surface with none
declared still offered interactive questions. They now match the
backend default of tools only, so no client path advertises a question
tool nobody can answer.
## Why
The in-app Assistant chat window was retired in #3126. Several backend
routes existed only for that window, and Pulse Mobile, the remaining
chat client, never called them. This removes them and the code that only
they reached.
## What changes
- **Routes removed:**
- `GET /api/ai/assistant/surface-tools`
- `POST /api/ai/workflow-prompts/render`
- `POST /api/ai/question/{id}/answer`
- the `/summarize`, `/fork`, `/undo`, `/redo`, `/steer`, `/diff`,
`/revert` and `/unrevert` sub-resources under `/api/ai/sessions/{id}`.
These now return `404` after the existing `ai:chat` scope check.
- **Code removed behind them:**
- the handlers and their `AIService` methods;
- the chat service's fork, undo, redo, steer, summarize, answer-question
and surface-tool methods;
- the session store's fork and turn-undo operations and the persisted
redo stack (`can_redo` leaves session summaries);
- session compaction, the agent loop's steering inbox and
question-answer wait, the `steered` message flag, and the `question` and
`steer_applied` stream events.
- **The question tool is no longer offered.** Interactive chat still
offered `pulse_question`, and Pulse Mobile has no way to answer it, so a
Mobile turn that called it waited until the request ended. Now:
- no profile offers the tool;
- the agent loop answers any call with a refusal telling the model to
ask in its reply text, the same path Patrol runs already took;
- the chat system prompt no longer mentions a clarification tool;
- the agent capabilities manifest's Pulse Assistant surface no longer
declares the `interactiveQuestions` affordance, so the prompt's
affordance text, the fallback governance names, and the generated
`docs/AI.md` and `cmd/pulse-mcp/README.md` surface blocks drop it. The
External agents settings panel stops showing an "Interactive questions"
affordance for Pulse Assistant. The shared `pulse_question` declaration
stays.
- **Docs and contracts:**
- `docs/API.md` lists the remaining routes under "Chat Sessions (Pulse
Mobile)", including the rename route Mobile already used.
- The ai-runtime and api-contracts rules for the removed workflows are
rewritten, and the dependent contracts carry dated notes.
## Unchanged
- `/api/ai/chat` and the session list, create, rename, delete, messages
and abort routes, which are the Pulse Mobile relay route list.
- `/api/ai/status`, which the dev health check uses.
- `/api/ai/workflow-prompts/activity`, which Patrol uses.
- Patrol investigations, provider routing, model-boundary sanitizing and
approvals.
- Old session files: they load as before and drop the redo stack and
`steered` flags on their next write.
## Not in this PR
- The Pulse Assistant surface description in the manifest, which still
calls it a handoff surface for Patrol findings. It now describes Pulse
Mobile chat and can be reworded with the surface contract when Mobile
retires.
- The browser's legacy fallback in
`frontend-modern/src/api/agentCapabilities.ts`. It still gives Pulse
Assistant `interactiveQuestions` when a manifest declares no affordances
at all. A same-version server always declares them, so that path is
unused; it changes with the surface description.
- The dead `highlight.js` chunk rule in `vite.config.ts`.
## Release note
> Removed API routes the retired in-app chat window used: Assistant
surface tools, workflow-prompt rendering, question answers, and the
session summarize, fork, undo, redo, steer, diff, revert and unrevert
routes. Pulse Mobile's chat is unaffected.
The in-app Assistant chat window was retired in #3126. Several backend
routes existed only for it, and Pulse Mobile, the remaining chat client,
never called them: assistant surface-tools, workflow-prompt render,
question answers, and the session summarize, fork, undo, redo, steer,
diff, revert and unrevert sub-resources. This removes them along with
the handlers, service and session-store methods, the persisted redo
stack, session compaction, mid-turn steering and the question-answer
wait behind them.
The question tool needed more than a route removal. Interactive chat
still offered pulse_question, and Pulse Mobile has no way to answer it,
so a Mobile turn that called it waited until the request ended. No
profile offers it now. The agentic loop answers any call with a refusal
that tells the model to ask in its reply text, the path Patrol runs
already took, and the chat prompt drops its clarification-tool advice.
The capabilities manifest's Assistant surface stops declaring
interactive questions, so the generated docs and fallback governance
names follow it.
/api/ai/chat and the session list, create, rename, delete, messages and
abort routes Pulse Mobile uses are unchanged, as is /api/ai/status. Old
session files load as before and drop the redo stack and steered flag
on their next write. docs/API.md lists the remaining chat routes, and
the subsystem contracts record the removal.
t.TempDir creates its directory through the process umask. Under 0002
the directory is group-writable, the installer correctly refuses to
trust the reader inside it, and every valid case failed on hosts with
that umask while passing on CI runners.
Contract-Neutral: test fixture hardening; no contract change
The ingress test came from a line whose installer also refused NUL bytes
in token files. Main's installer does not, so those cases failed on
every runner. The remaining cases cover what this branch fixes: both
token options read through the real installer and token reader.
Contract-Neutral: test scope correction for the portability fix; no contract change
The agent installer checked an --action-token-file's permissions with
GNU-only stat -c. BSD stat on macOS rejects it, the mode read came back
empty, and every action token file was refused as group- or
world-accessible. It now uses the existing portable_path_mode helper,
which the collector token path already relies on.
The ingress test runs both token options through the real installer
functions and the compiled token reader; on the macOS runner it is the
proof for this fix.
Contract-Neutral: portability fix restoring the documented --action-token-file option; no contract change
Require a blank destination instead of using nodes.enc as a first-run sentinel. Recheck before writes, retain unknown or unreadable state, and leave deliberate authenticated transfer unchanged. Keep deployment .env provisioning and encrypted file precedence; explain the separate archive path in canonical and served help.
Change-source: pulse-maintainer
Keep PULSE_TOKEN out of the direct-token flag default. Refuse an unreadable or empty selected file instead of silently borrowing other credentials, while preserving enrolled runtime-token restarts and implicit token-optional startup.
Cover actual flag loading and a synthetic host issuer, private installer-writer, compiled agent and trusted HTTPS lookup/report journey. This repairs an independent source-selection defect; it does not establish the cause or recovery of Discussion #2769.
Change-source: pulse-maintainer
Replace the credential-argument probe recommendation with private, bounded reads. Explain least-scope client setup, private configuration and preserved proxy access; distinguish scope-only writes, terminal events and partial verification from recovered workloads. Keep the adapter entry point and shipped guide consistent without changing runtime behaviour.
Contract-Neutral: Existing external-agent documentation and its copied/rendered regression checks only; no endpoint, scope, policy, adapter or runtime contract changes.
Change-source: pulse-maintainer
The refreshed native Windows job passes its runtime tests, then correctly refuses lifecycle admission because canonical ProgramData state already exists. Fourteen startup fixtures omit a private state directory, and another writes a fixed NAS path. Give each test temporary state, including allocation before runtime goroutines, and reject recurrence with an AST fixture guard and adverse controls. Preserve real startup, platform defaults and every native lifecycle/installer safety check.
Contract-Neutral: Test-only fixture state ownership and regression checks; agent configuration, persistent service state, installer and native lifecycle contracts are unchanged.
Change-source: pulse-maintainer
Replace the rejected global host-network proposal with the unchanged private namespace on every platform, including native linux-386. Carry successful rows and omitted VMIDs through operation v2 without clearing degraded health or renewing omitted measurements; retain strict complete-only v1 compatibility. Restart the installer-owned helper only after isolated-unit and socket checks, and document the unresolved pmxcfs IPC limitation rather than claiming PVE recovery.
Change-source: pulse-maintainer
A deployed agent's first run gave the remote config client, the updater, and
the Docker / Podman and Kubernetes modules the bootstrap token, which only the
host module exchanged. Pulse revokes that token on exchange, so those clients
failed with 401 until the service restarted. Enroll once at startup and build
every module from the runtime token and the server-assigned agent ID.
The supervisor sends its commit call before returning, but both channels can be ready when the test wakes. Treat a completed successful supervisor with the recorded commit as success, and bound the post-commit wait.
Change-source: pulse-maintainer
Wait for the agent supervisor to check false readiness before asserting its commit gate, and give the race-instrumented fixture a non-expiring window. Hold the monitoring request at target resolution after its history snapshot so replacement is deterministic. Keep production paths and assertions unchanged.
Change-source: pulse-maintainer
A provider-hosted control plane refused to start once its licence was past
expiry and the 7-day grace, because startup validated it with
ValidateLicense. So at the next restart after a 60-day evaluation ran out,
or after a paying provider's subscription lapsed, the portal went down, and
the portal's Plan tab is the only place a provider can buy or renew.
Reproduced on the walkthrough lab.
Startup now accepts an authentic, key-bound licence even when lapsed
(ValidateLicenseAllowingLapse), preferring a current licence and otherwise
the one that expired later, and logs the lapse. It unlocks nothing: client
runtimes still verify the provider licence in every lease with
ValidateLicense and drop MSP capabilities, new clients are refused with
provider_msp_license_lapsed, and the refresher still refuses a lapsed
licence. The Plan panel says the evaluation or plan ended and offers the
plans, and provider-msp status reports license_lapsed without failing so a
lapsed install can still upgrade.
The same run showed the portal printing control plane error codes such as
provider_msp_license_lapsed instead of their message. The portal now
prefers an error's message over its code, which also fixes
already_subscribed and checkout_unavailable in the Plan tab.
Setup's summary tells every new provider to run the install proof before
their first real client. Its temporary workspaces are created on the
provider's own account, so they count against the client limit. On a
two-client evaluation that already had a client, the proof created its
first workspace, was refused the second, and returned no report, so
install-proof's cleanup had nothing to remove and "Provider MSP Proof
01" stayed behind in one of the evaluation's two slots. Found on the
walkthrough lab.
The proof now checks for room before creating anything and says why it
cannot run, and a proof that fails part-way returns the IDs it created
so install-proof removes them. On the lab, with 3 of 3 slots in use,
install-proof now refuses up front and the client count is unchanged.
A provider could stand the MSP bundle up and evaluate it without asking
anyone, but paying still meant emailing its public key to Pulse and
copying a licence file onto the host by hand. With the licence server's
self-serve purchase path (pulse-pro license-server/provider_msp_purchase),
the control plane can do the whole thing itself.
The control plane now signs a challenge with its lease signing key to
fetch the licence its subscription entitles, validates it exactly as
startup does, requires it to bind this platform's key, and keeps it in
the data directory, because the host licence is a read-only secret
mount. Startup prefers that renewed licence while it validates, so a
paying provider keeps starting after the self-issued evaluation on the
host expires. A changed licence restarts the control plane through the
existing graceful shutdown, since the plan version is read at load by
every workspace-limit, backup and status path; client workspaces keep
running. Preflight, proof, recovery and backup accept either signed
source.
The portal gains owner and admin routes for plan state, checkout, the
Stripe billing portal and an immediate licence refresh, and the plan list
comes from the licence server so it shows only what can be bought, at
Stripe's price. Adds the msp_solo plan: 3 client workspaces, the first
paid step above the 2-workspace evaluation.
Return persisted planning acceptance or refusal inside the investigation turn.
Keep model judgment separate from action authority and preserve accepted action
identity across provider failures. Enforce actor/request idempotency atomically
and retain complete approval and independent verification context.
Preserve unknown disk evidence, stream whitespace and historical resolution
timestamps. Keep conversation scrolling inside its own panel. Record real-model,
disposable-lab and browser qualification with explicit population limits.
Refs #1782
The Docker and Kubernetes retry tests used a whole-runtime timeout as both
their synchronization point and shutdown trigger. Under suite load, startup
could consume that deadline before the second module initialization, producing
a false failure unrelated to retry behavior.
Wait for the successful retry explicitly, then cancel and verify clean
shutdown so the tests measure the lifecycle contract deterministically.
Change-source: pulse-maintainer
go test places t.TempDir under GOTMPDIR, and the preflight worker nested that
under its per-run directory, so unix socket fixtures in cmd/pulse-agent-helper,
internal/agenthelper, and internal/dockeragent exceeded the 108-byte sun_path
limit and failed with "bind: invalid argument". GitHub runners leave GOTMPDIR
unset, and the longest rootless Podman fixture sits exactly at the limit under
/tmp, so the worker now leaves GOTMPDIR unset by default and only honours an
explicit PULSE_RELEASE_PREFLIGHT_GO_TMP_DIR override.
Two cmd/pulse-agent helper tests ran without a state directory, so the agent
consulted the platform default /var/lib/pulse-agent. On a host where that
path exists as a private directory the pending-update handoff lookup fails
with permission denied. Give those tests an isolated state directory.
Harden release workflow input transport and make native lifecycle proof honor each platform security boundary. Refresh stale telemetry and delivery qualifications.
Contract-Neutral: Formatting-only catalog updates and browser-verified alert copy; release compiler and native lifecycle changes update their owning contracts.
Container update detection only ever negotiated anonymous pull tokens, so
containers from registries that reject anonymous digest HEADs pinned a
permanent "authentication required" badge (#1706). The agent already runs
on the Docker host, so the checker now resolves the same credential store
docker pull uses - config.json auths entries, credsStore/credHelpers
credential helpers (docker-credential-<name> get), and Podman's auth.json -
and presents the stored login: Basic auth on Bearer token negotiation and
on the hardcoded Docker Hub / ghcr.io token endpoints, direct answers to
Basic challenges, and the refresh-token grant for identity-token logins
such as Azure ACR.
Credentials never leave the host: they are only presented to the registry
or its token endpoint, helper output stays out of reported check errors,
and lookups are cached in memory for five minutes. Helper names are
validated before exec, and a stale login falls back to the anonymous path
so checks that used to work keep working. Set
PULSE_DISABLE_REGISTRY_CREDENTIALS=true (--disable-registry-credentials)
to keep detection anonymous-only. The agent-lifecycle and security-privacy
subsystem contracts pin the host-local credential boundary.
The server now echoes its version on unified-agent report acks, and the
agent nudges its auto-updater the moment an ack carries a newer version.
After a server upgrade, agents converge within one report interval
instead of waiting out the hourly update check, so the "older Pulse
agent" notice self-resolves in seconds once upgraded agents report in.
The hourly loop stays as the retry and backstop path. Nudges dedupe per
server version, refuse downgrades, skip disabled and development-mode
updaters, and never fire from observer destination acks — only the
authoritative server may steer an agent's updater, and a nudged check
re-validates against the server and runs the existing checksum and
self-test pipeline before swapping binaries. Agents deployed before
this change still converge on their old hourly cadence once; every
upgrade after that lands within a report cycle.
Contract deltas recorded in agent-lifecycle and api-contracts, with
boundary notes in security-privacy (no update authority in the echo),
performance-and-scalability (no steady-state work), and
storage-recovery (nothing persisted).
The evaluation mode added earlier today was hollow. An unlicensed control
plane starts, but release-build client runtimes only trust entitlement
leases chained to a Pulse-signed licence, so its client workspaces ran
without the capabilities the provider was evaluating. Standing the stack
up proved the portal and the isolation boundary and nothing else.
setup.sh now requests a capped evaluation licence from the licence server
when no licence path is set, sending only the public half of the key it
generated locally. It degrades rather than blocks: a missing key, an
unreachable server, or a licence-free response leaves the install
unlicensed with an explicit warning, an existing licence on disk is
reused, and PULSE_PROVIDER_MSP_SKIP_EVAL_LICENSE skips it for air-gapped
hosts. Guarded with an if-test rather than a trailing true inside the
command substitution, because the derive helper calls die and exit in a
subshell is not a status that can be caught, so setup.sh aborted under
set -e.
Second fix. The lease capability ceiling was selected by licence
presence, so an unlicensed provider control plane fell through to the
Pulse-hosted branch and minted leases claiming relay, mobile and push,
which a provider deployment cannot serve and which previously caused
repeating relay registration failures in client runtimes. The ceiling now
follows hosting via SetProviderHosted. providerChained keeps its narrower
meaning of having a licence available to embed.
Third. Corrects a sentence I wrote into cloud-paid.md this afternoon
claiming msp_eval carries the same MSP capabilities. A plan version
selects the workspace cap; it does not entitle a workspace.
The regression test was negative-tested by reverting the ceiling selector
and confirming it catches relay. All three setup.sh degradation paths
were exercised directly. Licence server side is pulse-pro 7f6a319 and is
not live until the next deploy-license-server run.