diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
index 9d8cf25939..a1f77fcb71 100644
--- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
+++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
@@ -166,7 +166,11 @@ management, and fleet control surfaces.
infrastructure and install commands are available, the same lifecycle path
must auto-watch the canonical `/api/state` projection for the first
reporting host rather than requiring a brand-new operator to know and type
- a hostname or agent ID just to see the first success handoff.
+ a hostname or agent ID just to see the first success handoff. When that
+ workspace is entered through first-run setup handoff, the same lifecycle
+ path must also auto-create the scoped first-host install token so the
+ operator lands on ready-to-copy commands instead of being asked to perform a
+ second manual token-generation step immediately after securing the server.
9. Keep `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`
ordered around the actual first-run operator sequence: credentials that must
be saved now should be visible before the operator leaves the screen, and
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index 2e81599203..9c5c4497cd 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -216,6 +216,13 @@ Own canonical runtime payload shapes between backend and frontend.
detecting the first active reporting system during install so brand-new
operators can receive the first success handoff without typing a hostname
or agent ID.
+20. Keep the shared first-run install-token transport explicit on
+ `/api/security/tokens` as used by
+ `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`:
+ once quick setup has produced the setup handoff credentials, the canonical
+ token-creation contract must remain usable immediately from the install
+ workspace so the first-host flow can auto-create the scoped install token
+ without forcing the operator through a second manual token-generation step.
## Current State
diff --git a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx
index 5e12a84313..c90be5f33a 100644
--- a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx
+++ b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx
@@ -43,7 +43,32 @@ export const InfrastructureInstallerSection: Component = () => {
Security configured. Save these first-run credentials now.
This is the canonical handoff from first-run setup into Infrastructure Install.
- Generate a scoped install token below before copying agent commands.
+
+ {' '}
+ Pulse could not prepare the first scoped install token automatically, so
+ use the token step below and continue from there.
+
+ }
+ >
+ {' '}
+ Pulse already prepared the first scoped install token for this handoff, so
+ you can move straight to copying the install command for the first host.
+
+ }
+ >
+ {' '}
+ Pulse is preparing the first scoped install token now so the install commands
+ unlock without another setup step.
+
@@ -153,7 +178,13 @@ export const InfrastructureInstallerSection: Component = () => {
{state.requiresToken()
- ? 'Create a fresh token for the generated install commands and host reporting.'
+ ? state.setupHandoffAutoTokenPending()
+ ? 'Pulse is generating the first scoped install token for this setup handoff now.'
+ : state.latestTokenSource() === 'setup_handoff'
+ ? 'Pulse generated the first scoped install token automatically. The install commands below are ready for the first host.'
+ : state.setupHandoffAutoTokenFailed()
+ ? 'Pulse could not generate the first scoped install token automatically. Generate one here to unlock the install commands.'
+ : 'Create a fresh token for the generated install commands and host reporting.'
: 'Tokens are optional on this Pulse instance. Generate one if you want copied commands to preserve explicit credentialed transport.'}
@@ -191,8 +222,17 @@ export const InfrastructureInstallerSection: Component = () => {
- Install token {state.latestRecord()?.name} created. Commands below
- now include this credential.
+ {state.latestTokenSource() === 'setup_handoff' ? (
+ <>
+ First-host install token {state.latestRecord()?.name} prepared
+ automatically. Commands below already include this credential.
+ >
+ ) : (
+ <>
+ Install token {state.latestRecord()?.name} created. Commands
+ below now include this credential.
+ >
+ )}
diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsController.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsController.test.tsx
index 265efc8f35..6f21eaa82d 100644
--- a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsController.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsController.test.tsx
@@ -24,6 +24,7 @@ import type {
KubernetesCluster,
State,
} from '@/types/api';
+import { STORAGE_KEYS } from '@/utils/localStorage';
const { navigateMock } = vi.hoisted(() => ({
navigateMock: vi.fn(),
@@ -624,6 +625,7 @@ const getTargetProfileSelect = (): HTMLElement => {
beforeEach(() => {
securityStatusResponse = { requiresAuth: true, apiTokenConfigured: false };
+ sessionStorage.clear();
navigateMock.mockReset();
lookupMock.mockReset();
getStateMock.mockReset();
@@ -663,6 +665,7 @@ beforeEach(() => {
afterEach(() => {
cleanup();
+ sessionStorage.clear();
vi.useRealTimers();
vi.unstubAllGlobals();
});
@@ -677,6 +680,47 @@ describe('InfrastructureOperationsController token generation', () => {
expect(screen.getByRole('button', { name: /Generate token/i })).toBeInTheDocument();
});
+ it('auto-generates the first-host install token from setup handoff', async () => {
+ createTokenMock.mockResolvedValue({
+ token: 'handoff-install-token',
+ record: {
+ id: 'token-record-handoff',
+ name: 'Agent 2026-03-27 10-56',
+ prefix: 'agent',
+ suffix: '1234',
+ createdAt: new Date().toISOString(),
+ },
+ });
+
+ sessionStorage.setItem(
+ STORAGE_KEYS.SETUP_HANDOFF,
+ JSON.stringify({
+ username: 'admin',
+ password: 'generated-password',
+ apiToken: 'admin-token',
+ createdAt: new Date().toISOString(),
+ }),
+ );
+
+ setupComponent();
+
+ await waitFor(() => expect(createTokenMock).toHaveBeenCalled(), { interval: 0 });
+ await waitFor(() => expect(screen.getByText(/First-host install token/i)).toBeInTheDocument(), {
+ interval: 0,
+ });
+
+ expect(trackAgentInstallTokenGeneratedMock).toHaveBeenCalledWith(
+ 'settings_unified_agents',
+ 'setup_handoff',
+ );
+ expect(notificationSuccessMock).not.toHaveBeenCalled();
+ expect(screen.getByText(/Security configured\. Save these first-run credentials now\./i)).toBeInTheDocument();
+ expect(
+ screen.getByText(/Pulse already prepared the first scoped install token for this handoff/i),
+ ).toBeInTheDocument();
+ expect(screen.getByText('Installation commands')).toBeInTheDocument();
+ });
+
it('generates a token and shows confirmation', async () => {
createTokenMock.mockResolvedValue({
token: 'test-token-123',
diff --git a/frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx b/frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx
index 773c5bfd03..add77c20a1 100644
--- a/frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx
+++ b/frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx
@@ -97,6 +97,12 @@ export const useInfrastructureInstallState = (
const [customAgentUrl, setCustomAgentUrl] = createSignal('');
const [customCaPath, setCustomCaPath] = createSignal('');
const [setupHandoff, setSetupHandoff] = createSignal(null);
+ const [latestTokenSource, setLatestTokenSource] = createSignal<'manual' | 'setup_handoff' | null>(
+ null,
+ );
+ const [setupHandoffAutoTokenPending, setSetupHandoffAutoTokenPending] = createSignal(false);
+ const [setupHandoffAutoTokenFailed, setSetupHandoffAutoTokenFailed] = createSignal(false);
+ const [setupHandoffAutoTokenAttempted, setSetupHandoffAutoTokenAttempted] = createSignal(false);
createEffect(() => {
if (requiresToken()) {
@@ -225,9 +231,10 @@ Generate a scoped install token below before copying Unified Agent install comma
notificationStore.success('Confirmed install commands without an API token.', 3500);
};
- const handleGenerateToken = async () => {
- if (isGeneratingToken()) return;
-
+ const generateInstallToken = async (
+ source: 'manual' | 'setup_handoff',
+ options: { notifySuccess?: boolean } = {},
+ ) => {
setIsGeneratingToken(true);
try {
const desiredName = tokenName().trim() || buildDefaultTokenName();
@@ -242,24 +249,41 @@ Generate a scoped install token below before copying Unified Agent install comma
setCurrentToken(token);
setLatestRecord(record);
+ setLatestTokenSource(source);
setTokenName('');
setConfirmedNoToken(false);
- trackAgentInstallTokenGenerated(UNIFIED_AGENT_TELEMETRY_SURFACE, 'manual');
- notificationStore.success(
- 'Token generated with Agent config + reporting, Docker, and Kubernetes permissions.',
- 4000,
- );
+ setSetupHandoffAutoTokenFailed(false);
+ trackAgentInstallTokenGenerated(UNIFIED_AGENT_TELEMETRY_SURFACE, source);
+ if (options.notifySuccess) {
+ notificationStore.success(
+ 'Token generated with Agent config + reporting, Docker, and Kubernetes permissions.',
+ 4000,
+ );
+ }
} catch (err) {
logger.error('Failed to generate agent token', err);
- notificationStore.error(
- 'Failed to generate agent token. Confirm you are signed in as an administrator.',
- 6000,
- );
+ if (source === 'setup_handoff') {
+ setSetupHandoffAutoTokenFailed(true);
+ } else {
+ notificationStore.error(
+ 'Failed to generate agent token. Confirm you are signed in as an administrator.',
+ 6000,
+ );
+ }
} finally {
setIsGeneratingToken(false);
+ if (source === 'setup_handoff') {
+ setSetupHandoffAutoTokenPending(false);
+ }
}
};
+ const handleGenerateToken = async () => {
+ if (isGeneratingToken()) return;
+
+ await generateInstallToken('manual', { notifySuccess: true });
+ };
+
const resolvedCommandToken = () => {
if (requiresToken()) {
return currentToken() || TOKEN_PLACEHOLDER;
@@ -267,6 +291,25 @@ Generate a scoped install token below before copying Unified Agent install comma
return currentToken();
};
+ createEffect(() => {
+ const shouldAutoCreateInstallToken =
+ Boolean(setupHandoff()) &&
+ securityStatus() !== null &&
+ requiresToken() &&
+ !currentToken() &&
+ !latestRecord() &&
+ !isGeneratingToken() &&
+ !setupHandoffAutoTokenAttempted();
+
+ if (!shouldAutoCreateInstallToken) {
+ return;
+ }
+
+ setSetupHandoffAutoTokenAttempted(true);
+ setSetupHandoffAutoTokenPending(true);
+ void generateInstallToken('setup_handoff');
+ });
+
createEffect(() => {
const shouldAutoDetectFirstHost =
commandsUnlocked() && !lookupValue().trim() && !lookupResult()?.agent?.connected;
@@ -467,6 +510,7 @@ Generate a scoped install token below before copying Unified Agent install comma
isEmbedded,
isGeneratingToken,
latestRecord,
+ latestTokenSource,
lookupError,
lookupLoading,
lookupResult,
@@ -484,6 +528,8 @@ Generate a scoped install token below before copying Unified Agent install comma
setLookupValue,
setTokenName,
setupHandoff,
+ setupHandoffAutoTokenFailed,
+ setupHandoffAutoTokenPending,
tokenName,
};
};