Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
dd189e3
fix(node-ui): replace token bootstrap with dashboard sessions
Jul 3, 2026
847b160
test(node-ui): stabilize dashboard session auth tests
Jul 3, 2026
8d1013b
fix(node-ui): address dashboard session review
Jul 3, 2026
a8e7b1f
fix(node-ui): refresh stale dashboard sessions
Jul 3, 2026
264a775
Address node UI auth review follow-ups
Jul 3, 2026
a550721
Address dashboard session review follow-ups
Jul 3, 2026
70a688f
Address review round 4 dashboard session feedback
Jul 3, 2026
d5efb31
Address review round 5 and CI failure
Jul 3, 2026
1973575
Address node-ui session review and e2e failures
Jul 4, 2026
8832b9a
Address delayed PR review comments
Jul 4, 2026
03813b2
Address dashboard session review feedback
Jul 4, 2026
8f3e55c
Tighten dashboard logout session safety
Jul 4, 2026
022d17a
Fix auth-session logout CSRF e2e
Jul 4, 2026
622e8c6
Avoid SSE near-expiry reconnect loop
Jul 4, 2026
949a1af
Tighten dashboard auth policy coverage
Jul 4, 2026
e60ef12
Cover dashboard CSRF session endpoint
Jul 4, 2026
69f0df8
Honor Forwarded proto for dashboard cookies
Jul 4, 2026
087a7dc
Cover SSE session expiry closure
Jul 4, 2026
ee51a4e
Handle explicit empty auth and stale CSRF
Jul 4, 2026
a83ef23
Cover CSRF on unsafe dashboard methods
Jul 4, 2026
afb358d
Narrow stale CSRF retry handling
Jul 4, 2026
6e1a664
Trust forwarded proto only from loopback
Jul 4, 2026
90ec4f9
Cover incorrect CSRF token rejection
Jul 4, 2026
e797a82
Resolve dashboard principals at auth time
Jul 4, 2026
1ffc988
Cover UI asset and SSE review gaps
Jul 4, 2026
d8834d9
Cover explicit SSE and referer auth gaps
Jul 4, 2026
dc6ccfc
Require token for loopback dashboard sessions
Jul 4, 2026
7fd09f2
Cover dashboard cors and smoke auth gaps
Jul 4, 2026
3c1d75c
Authenticate raw mock fallback e2e spec
Jul 4, 2026
f217dae
Handle dashboard origin metadata and gate ownership
Jul 4, 2026
8dd4d81
Block cross-site dashboard session exchange
Jul 4, 2026
0a644ed
Guard dashboard session refresh races
Jul 4, 2026
955fbfe
Support cross-origin dashboard session review gaps
Jul 4, 2026
5bbb47a
Close stale dashboard SSE and code CSRF retries
Jul 4, 2026
104be8f
Cover dashboard exchange and unlock failures
Jul 4, 2026
f7ed67c
Make daemon fetch path contract explicit
Jul 4, 2026
7a6700a
Update node-ui compatibility assertion for daemon paths
Jul 4, 2026
1aec8f5
Share auth CORS header policy
Jul 4, 2026
7680931
Cover dashboard status and connect session paths
Jul 4, 2026
a9a1cea
Allow blob-backed UI previews in CSP
Jul 4, 2026
e8847c8
Allow UI font origins in CSP
Jul 4, 2026
066cd95
Add credentialed CORS to node-ui API responses
Jul 4, 2026
c47979e
Handle proxied HTTPS dashboard origins
Jul 4, 2026
d01aecd
Keep node-ui daemon paths typed
Jul 4, 2026
0da257a
Add dashboard username password login
Jul 4, 2026
a2f725b
Create dashboard credentials in setup flows
Jul 4, 2026
8f878fc
Harden dashboard credential setup review gaps
Jul 4, 2026
b73da51
Address dashboard login review gaps
Jul 4, 2026
e652ed6
Address dashboard login follow-up review
Jul 4, 2026
6793e7c
Require fingerprints for login sessions
Jul 4, 2026
58ec3ce
Tighten dashboard login review gaps
Jul 5, 2026
a5b65e3
Bound dashboard login attempts
Jul 5, 2026
40e9c3b
Cover dashboard login CSRF contract
Jul 5, 2026
965dba6
Assert password-login cookie attributes
Jul 5, 2026
1a94b4d
Harden dashboard login review gaps
Jul 5, 2026
5cad89f
Revalidate dashboard login event streams
Jul 5, 2026
160958d
Cover dashboard login setup review gaps
Jul 5, 2026
acdebb1
Harden dashboard credential verification
Jul 5, 2026
2928480
Merge pull request #1439 from OriginTrail/codex/node-ui-dashboard-login
Jurij89 Jul 5, 2026
b6f58d0
Refine dashboard login follow-up boundaries
Jul 5, 2026
55d2a64
Address dashboard follow-up review gaps
Jul 5, 2026
bc13750
Collapse dashboard SSE auth metadata
Jul 5, 2026
a9da311
Revalidate dashboard SSE sessions
Jul 5, 2026
6daa067
Cover resolved dashboard SSE comments
Jul 5, 2026
1badd52
Preserve dashboard auth compatibility
Jul 5, 2026
2b191a8
Keep fingerprints out of request auth
Jul 5, 2026
c3b387f
Reuse dashboard credential result types
Jul 5, 2026
3cab575
Cover MCP credential setup wiring
Jul 5, 2026
064cabe
Merge pull request #1451 from OriginTrail/codex/dashboard-login-follo…
Jurij89 Jul 5, 2026
9d535ea
Harden dashboard auth follow-up coverage
Jul 5, 2026
343929e
Address dashboard auth follow-up review
Jul 5, 2026
921d5aa
Tighten dashboard login limiter API
Jul 5, 2026
f7e8737
Split dashboard auth and static UI modules
Jul 5, 2026
7b77e85
Centralize dashboard principal and limiter state
Jul 5, 2026
8aef2a5
Merge pull request #1460 from OriginTrail/codex/dashboard-auth-bounda…
Jurij89 Jul 5, 2026
86455bf
Close dashboard auth boundary follow-up
Jul 5, 2026
c923a03
Address dashboard auth review comments
Jul 5, 2026
c06881a
Collapse dashboard auth context handoff
Jul 5, 2026
65bc558
Tighten dashboard auth review coverage
Jul 5, 2026
1b25ac9
Preserve plugin and sign-join contracts
Jul 6, 2026
4def731
Enforce login session fingerprint at runtime
Jul 6, 2026
fe5eadb
Merge pull request #1465 from OriginTrail/codex/dashboard-auth-1433-c…
Jurij89 Jul 6, 2026
77318af
Tighten daemon route auth identity handoff
Jul 6, 2026
4680aa1
Clarify route auth context handoff
Jul 6, 2026
e8bd4dc
Keep route dispatch identity boundary strict
Jul 6, 2026
0f74f77
Pin public plugin context surface
Jul 6, 2026
bf03d77
Cover plugin and route identity review gaps
Jul 6, 2026
4667af2
Use checked plugin type fixture
Jul 6, 2026
0e47c7e
Clarify auth resolver and plugin boundaries
Jul 6, 2026
3eeed32
Tighten plugin boundary review followups
Jul 6, 2026
c753511
Anchor plugin context compatibility keys
Jul 6, 2026
96e4115
Assert plugin context compatibility key diffs
Jul 6, 2026
a171eb1
Merge pull request #1471 from OriginTrail/codex/dashboard-auth-route-…
Jurij89 Jul 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,37 @@ jobs:
run: tar -xzf /tmp/build-outputs.tgz
- name: "node-ui tests"
run: pnpm --filter @origintrail-official/dkg-node-ui test
- name: "node-ui static bundle contract"
shell: bash
run: |
pnpm --filter @origintrail-official/dkg-node-ui build:ui
test -f packages/node-ui/dist-ui/index.html
if grep -R "__DKG_TOKEN__" packages/node-ui/dist-ui; then
echo "dist-ui must not contain legacy browser bearer-token bootstrap"
exit 1
fi
node - <<'NODE'
const { readFileSync, existsSync } = require('node:fs');
const { join, dirname } = require('node:path');
const indexPath = join('packages', 'node-ui', 'dist-ui', 'index.html');
const html = readFileSync(indexPath, 'utf8');
const refs = [...html.matchAll(/(?:src|href)="([^"]+\.(?:js|css))"/g)].map((m) => m[1]);
if (refs.length === 0) {
throw new Error('dist-ui/index.html did not reference any JS/CSS assets');
}
for (const ref of refs) {
if (ref.startsWith('/') && !ref.startsWith('/ui/')) {
throw new Error(`UI asset reference must be relative or rooted under /ui/: ${ref}`);
}
const normalized = ref.startsWith('/ui/') ? ref.slice('/ui/'.length) : ref.replace(/^\.\//, '');
Comment thread
Jurij89 marked this conversation as resolved.
const assetPath = join(dirname(indexPath), normalized);
if (!existsSync(assetPath)) throw new Error(`Missing referenced UI asset: ${ref}`);
}
NODE
- name: Install Playwright Chromium for production-static smoke
run: pnpm --filter @origintrail-official/dkg-node-ui exec playwright install chromium --with-deps
- name: "node-ui production-static Playwright smoke"
run: pnpm --filter @origintrail-official/dkg-node-ui test:e2e:static

# node-ui Playwright e2e — REAL NODE, NO MOCKS. The suite boots a live
# 4-node devnet (Hardhat + four DKG daemons) via Playwright's `webServer`
Expand Down
29 changes: 29 additions & 0 deletions packages/adapter-hermes/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,23 @@ function rewriteActiveProviderLine(raw: string, newProvider: string): string | n
* request so the adapter stays free of a `@origintrail-official/dkg-agent`
* dependency: the cli layer (which has dkg-agent) provides `loadOpWallets`.
*/
type PostConfigSetupHook = (dkgHome: string) => Promise<unknown>;

export interface RunHermesSetupDeps {
/**
* Optional setup lifecycle hook invoked after DKG node config bootstrap and
* before wallet creation / daemon start. CLI setup entry points use this for
* CLI-owned best-effort side effects while daemon/UI-driven adapter setup
* omits it. Required setup work should stay explicit instead of being hidden
* behind this optional hook.
*/
afterConfigBootstrap?: PostConfigSetupHook;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: A generic post-config hook turns setup into a side-effect injection point

What's wrong
The hook is intentionally broad, optional, and best-effort, while the actual behavior is specific dashboard credential provisioning. This leaks CLI-owned side effects into adapter setup through an abstraction that does not describe what it does. It also duplicates the same fallback and warning pattern in multiple setup orchestrators, which is the kind of hook-based growth that tends to become permanent setup debt.

Example
Dashboard credential provisioning is the only new use case, but the adapter API now exposes a generic post-config lifecycle hook plus a deprecated alias. The same hook fallback and catch/log behavior is repeated across Hermes, OpenClaw, and MCP setup.

Suggested direction
Make the dependency explicit to the behavior being added, or centralize a typed setup-step runner reused by Hermes/OpenClaw/MCP. Avoid a generic lifecycle hook that can hide arbitrary work and duplicated error handling.

Confidence note
This is a structural concern from the diff shape; the exact public compatibility constraints for the adapter setup dependency objects may need maintainer confirmation.

For Agents
Review the setup dependency surfaces in Hermes, OpenClaw, and MCP. Preserve the ordering: config bootstrap before wallet creation/daemon start, dry-run skip, and best-effort credential provisioning. Replace the generic hook with an explicit named setup concern or a shared post-config step runner so future setup side effects do not accumulate behind an untyped lifecycle slot.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Avoid spreading a generic post-config hook across setup flows

What's wrong
The new afterConfigBootstrap abstraction is generic, optional, duplicated, and currently only exists to run dashboard credential provisioning. It hides a feature-specific side effect behind a lifecycle hook and repeats the same control-flow policy in multiple setup implementations.

Example
Adding another CLI-owned setup side effect after config bootstrap would require deciding whether it goes through the generic hook, then auditing Hermes, OpenClaw, and MCP to keep the same ordering/dry-run/best-effort behavior.

Suggested direction
Prefer an explicit, shared setup lifecycle helper or an explicit dashboardCredentialSetup dependency with one runner for dry-run/deprecated-alias/best-effort handling. That would keep adapter setup order readable without giving every setup path an open-ended side-effect slot.

For Agents
Inspect packages/adapter-hermes/src/setup.ts, packages/adapter-openclaw/src/setup.ts, packages/cli/src/mcp-setup.ts, and the CLI setup wrappers. Preserve dry-run skipping, wallet-before-daemon ordering, and best-effort dashboard credential creation. Replace the repeated generic hook with a shared setup lifecycle helper or an explicit dashboard-credential dependency composed in the CLI-owned setup layer.

/**
* @deprecated Use `afterConfigBootstrap`. Kept as a runtime fallback so
* JavaScript callers on the previous dependency shape do not silently lose
* dashboard credential provisioning.
*/
ensureDashboardCredentials?: PostConfigSetupHook;
/**
* Eagerly create the node's operational wallets (generate-if-absent) after
* the config bootstrap and before the daemon starts, so faucet funding and
Expand Down Expand Up @@ -704,6 +720,7 @@ export async function runHermesSetup(
// one they chose. Gating on `config.json` alone (the pre-#960 behavior) misses
// the YAML-only install.
const dkgConfigHome = resolveDkgConfigHome({ startDir: __dirname });
const dashboardCredentialHome = shouldStart ? dkgConfigHome : dkgDir(setupOptions.daemonUrl);
const dkgConfigExists =
existsSync(join(dkgConfigHome, 'config.json')) ||
existsSync(join(dkgConfigHome, 'config.yaml'));
Expand Down Expand Up @@ -750,6 +767,18 @@ export async function runHermesSetup(
console.log('[hermes-setup] [dry-run] Would bootstrap ~/.dkg/config.json if missing');
}

// Run CLI-owned setup side effects after node config bootstrap and before
// wallets / daemon startup. Best-effort so adapter setup remains recoverable
// and daemon/UI-driven setup can omit terminal-only side effects entirely.
const afterConfigBootstrap = deps.afterConfigBootstrap ?? deps.ensureDashboardCredentials;
if (!dryRun && afterConfigBootstrap) {
try {
await afterConfigBootstrap(dashboardCredentialHome);
} catch (err: any) {
console.warn(`[hermes-setup] Post-config setup hook failed (${err?.message ?? String(err)}); continuing.`);
}
}

// Eagerly ensure the node's wallets exist BEFORE the daemon starts (issue
// #1306) — matching `dkg init` — so faucet funding (testnet) and manual
// mainnet funding have wallets even if the daemon never fully boots. Runs
Expand Down
183 changes: 182 additions & 1 deletion packages/adapter-hermes/test/hermes-adapter.part-07.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ vi.mock('@origintrail-official/dkg-core', async () => {
...actual,
resolveDkgConfigHome: vi.fn((opts) => actual.resolveDkgConfigHome(opts)),
resolveDkgHome: vi.fn((opts) => actual.resolveDkgHome(opts)),
startDaemon: vi.fn(async () => {}),
};
});
import { resolveDkgHome } from '@origintrail-official/dkg-core';
import { resolveDkgConfigHome, resolveDkgHome, startDaemon } from '@origintrail-official/dkg-core';
import { HermesAdapterPlugin } from '../src/HermesAdapterPlugin.js';
import { registerHermesRoutes } from '../src/hermes-routes.js';
import { HermesDkgClient, redact } from '../src/dkg-client.js';
Expand Down Expand Up @@ -115,6 +116,186 @@ describe('Hermes profile setup helpers', () => {
}
});

it('#1443: invokes the post-config setup hook, skipped on --dry-run', async () => {
const hermesHome = mkdtempSync(join(tmpdir(), 'hermes-profile-'));
const dkgHome = mkdtempSync(join(tmpdir(), 'dkg-home-1439-'));
vi.stubGlobal('fetch', async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const oldDkgHome = process.env.DKG_HOME;
process.env.DKG_HOME = dkgHome;
try {
const afterConfigBootstrap = vi.fn(async () => {});

await runSetup(
{ hermesHome, verify: false, start: false, fund: false },
{ afterConfigBootstrap },
);
expect(afterConfigBootstrap).toHaveBeenCalledTimes(1);
expect(afterConfigBootstrap.mock.calls[0][0]).toBe(dkgHome);

afterConfigBootstrap.mockClear();
await runSetup(
{ hermesHome, verify: false, start: false, fund: false, dryRun: true },
{ afterConfigBootstrap },
);
expect(afterConfigBootstrap).not.toHaveBeenCalled();
} finally {
if (oldDkgHome === undefined) delete process.env.DKG_HOME;
else process.env.DKG_HOME = oldDkgHome;
rmSync(hermesHome, { recursive: true, force: true });
rmSync(dkgHome, { recursive: true, force: true });
}
});

it('#1451: honors deprecated ensureDashboardCredentials hook as a runtime fallback', async () => {
const hermesHome = mkdtempSync(join(tmpdir(), 'hermes-profile-'));
const dkgHome = mkdtempSync(join(tmpdir(), 'dkg-home-legacy-hook-'));
vi.stubGlobal('fetch', async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const oldDkgHome = process.env.DKG_HOME;
process.env.DKG_HOME = dkgHome;
try {
const ensureDashboardCredentials = vi.fn(async () => {});

await runSetup(
{ hermesHome, verify: false, start: false, fund: false },
{ ensureDashboardCredentials },
);
expect(ensureDashboardCredentials).toHaveBeenCalledTimes(1);
expect(ensureDashboardCredentials.mock.calls[0][0]).toBe(dkgHome);
} finally {
if (oldDkgHome === undefined) delete process.env.DKG_HOME;
else process.env.DKG_HOME = oldDkgHome;
rmSync(hermesHome, { recursive: true, force: true });
rmSync(dkgHome, { recursive: true, force: true });
}
});

it('#1439: creates dashboard credentials in the config DKG home when setup starts the daemon', async () => {
const hermesHome = mkdtempSync(join(tmpdir(), 'hermes-profile-'));
const configDkgHome = mkdtempSync(join(tmpdir(), 'dkg-home-config-'));
const daemonDkgHome = mkdtempSync(join(tmpdir(), 'dkg-home-daemon-url-'));
vi.stubGlobal('fetch', async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const oldDkgHome = process.env.DKG_HOME;
delete process.env.DKG_HOME;
const configResolver = vi.mocked(resolveDkgConfigHome);
const homeResolver = vi.mocked(resolveDkgHome);
const originalResolveDkgConfigHome = configResolver.getMockImplementation();
const originalResolveDkgHome = homeResolver.getMockImplementation();
configResolver.mockReturnValue(configDkgHome);
homeResolver.mockReturnValue(daemonDkgHome);
vi.mocked(startDaemon).mockClear();
try {
const afterConfigBootstrap = vi.fn(async () => {});

await runSetup(
{
hermesHome,
daemonUrl: 'http://127.0.0.1:9300',
verify: false,
fund: false,
},
{ afterConfigBootstrap },
);

expect(startDaemon).toHaveBeenCalledWith(9300);
expect(resolveDkgHome).toHaveBeenCalledWith({ daemonUrl: 'http://127.0.0.1:9300' });
expect(afterConfigBootstrap).toHaveBeenCalledTimes(1);
expect(afterConfigBootstrap.mock.calls[0][0]).toBe(configDkgHome);
expect(afterConfigBootstrap.mock.calls[0][0]).not.toBe(daemonDkgHome);
} finally {
if (originalResolveDkgConfigHome) configResolver.mockImplementation(originalResolveDkgConfigHome);
if (originalResolveDkgHome) homeResolver.mockImplementation(originalResolveDkgHome);
vi.mocked(startDaemon).mockClear();
if (oldDkgHome === undefined) delete process.env.DKG_HOME;
else process.env.DKG_HOME = oldDkgHome;
rmSync(hermesHome, { recursive: true, force: true });
rmSync(configDkgHome, { recursive: true, force: true });
rmSync(daemonDkgHome, { recursive: true, force: true });
}
});

it('#1439: creates dashboard credentials in the daemon-url resolved DKG home when setup does not start the daemon', async () => {
const hermesHome = mkdtempSync(join(tmpdir(), 'hermes-profile-'));
const daemonDkgHome = mkdtempSync(join(tmpdir(), 'dkg-home-daemon-url-'));
vi.stubGlobal('fetch', async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const oldDkgHome = process.env.DKG_HOME;
delete process.env.DKG_HOME;
const resolver = vi.mocked(resolveDkgHome);
const originalResolveDkgHome = resolver.getMockImplementation();
resolver.mockReturnValue(daemonDkgHome);
try {
const afterConfigBootstrap = vi.fn(async () => {});

await runSetup(
{
hermesHome,
daemonUrl: 'http://127.0.0.1:9300',
verify: false,
start: false,
fund: false,
},
{ afterConfigBootstrap },
);

expect(resolveDkgHome).toHaveBeenCalledWith({ daemonUrl: 'http://127.0.0.1:9300' });
expect(afterConfigBootstrap).toHaveBeenCalledTimes(1);
expect(afterConfigBootstrap.mock.calls[0][0]).toBe(daemonDkgHome);
} finally {
if (originalResolveDkgHome) resolver.mockImplementation(originalResolveDkgHome);
if (oldDkgHome === undefined) delete process.env.DKG_HOME;
else process.env.DKG_HOME = oldDkgHome;
rmSync(hermesHome, { recursive: true, force: true });
rmSync(daemonDkgHome, { recursive: true, force: true });
}
});

it('#1443: a failing post-config setup hook does not flip setup to degraded', async () => {
const hermesHome = mkdtempSync(join(tmpdir(), 'hermes-profile-'));
const dkgHome = mkdtempSync(join(tmpdir(), 'dkg-home-1439f-'));
vi.stubGlobal('fetch', async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const oldDkgHome = process.env.DKG_HOME;
process.env.DKG_HOME = dkgHome;
try {
const afterConfigBootstrap = vi.fn(async () => { throw new Error('invalid credential file'); });
const result = await runHermesSetup(
{ hermesHome, verify: false, start: false, fund: false },
{ afterConfigBootstrap },
);
expect(afterConfigBootstrap).toHaveBeenCalledTimes(1);
expect(result.warnings.join('\n')).not.toContain('dashboard login credentials');
expect(result.errors.join('\n')).not.toContain('dashboard login credentials');
} finally {
if (oldDkgHome === undefined) delete process.env.DKG_HOME;
else process.env.DKG_HOME = oldDkgHome;
rmSync(hermesHome, { recursive: true, force: true });
rmSync(dkgHome, { recursive: true, force: true });
}
});

// issue #1306 — a failing wallet pre-creation is best-effort and must NOT push
// into the status-bearing `warnings[]` (which would flip the integration to
// `degraded` in the daemon-UI). It goes to console.warn instead. Call
Expand Down
29 changes: 29 additions & 0 deletions packages/adapter-openclaw/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1645,7 +1645,23 @@ export function verifyMemorySlotInvariants(configPath?: string): void {
* `SetupOptions` so the adapter stays free of a `@origintrail-official/dkg-agent`
* dependency: the cli layer (which has dkg-agent) provides `loadOpWallets`.
*/
type PostConfigSetupHook = (dkgHome: string) => Promise<unknown>;

export interface RunSetupDeps {
/**
* Optional setup lifecycle hook invoked after DKG node config bootstrap and
* before wallet creation / daemon start. CLI setup entry points use this for
* CLI-owned best-effort side effects while daemon/UI-driven adapter setup
* omits it. Required setup work should stay explicit instead of being hidden
* behind this optional hook.
*/
afterConfigBootstrap?: PostConfigSetupHook;
/**
* @deprecated Use `afterConfigBootstrap`. Kept as a runtime fallback so
* JavaScript callers on the previous dependency shape do not silently lose
* dashboard credential provisioning.
*/
ensureDashboardCredentials?: PostConfigSetupHook;
/**
* Eagerly create the node's operational wallets (generate-if-absent) after
* the config write and before the daemon starts, so faucet funding and
Expand Down Expand Up @@ -1742,6 +1758,19 @@ export async function runSetup(options: SetupOptions, deps: RunSetupDeps = {}):
log(`[dry-run] Would write ${join(dkgDir(), 'config.json')} (${network.networkName}, port ${apiPort})`);
}

// Run CLI-owned setup side effects after the node config home is known and
// before wallets / daemon startup. Best-effort so adapter setup remains
// recoverable and daemon/UI-driven setup can omit terminal-only side effects.
const afterConfigBootstrap = deps.afterConfigBootstrap ?? deps.ensureDashboardCredentials;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Post-config setup hook handling is duplicated across setup orchestrators

What's wrong
The PR adds the same optional lifecycle hook, deprecated dashboard-specific fallback, and try/catch best-effort execution to multiple setup flows. That spreads one policy across packages and makes the general adapter setup APIs carry dashboard credential terminology they otherwise tried to avoid.

Example
Changing the post-config hook failure policy or removing the deprecated ensureDashboardCredentials fallback currently requires coordinated edits in OpenClaw, Hermes, and MCP setup code.

Suggested direction
Centralize the hook selection and best-effort execution behind a shared helper or setup lifecycle abstraction. Keep adapter-facing deps generic and contain the deprecated dashboard-specific compatibility shim at the CLI boundary where possible.

Confidence note
This assumes the three setup paths are expected to keep identical lifecycle semantics; if the divergence is intentional, the shared helper should make those differences explicit.

For Agents
Look at packages/adapter-openclaw/src/setup.ts, packages/adapter-hermes/src/setup.ts, and packages/cli/src/mcp-setup.ts. Introduce one shared post-config setup hook runner or a common setup lifecycle dependency type. Preserve dry-run skipping, best-effort error handling, and legacy JS fallback behavior, then update the existing setup hook tests to target the shared contract once.

if (!dryRun && afterConfigBootstrap) {
const dashboardCredentialHome = dkgDir();
try {
await afterConfigBootstrap(dashboardCredentialHome);
} catch (err: any) {
warn(`Post-config setup hook failed (${err?.message ?? String(err)}); continuing.`);
}
}

// Eagerly ensure the node's wallets exist BEFORE the daemon starts (issue
// #1306) — matching `dkg init` — so faucet funding (testnet) and manual
// mainnet funding have wallets to target even if the daemon never fully
Expand Down
53 changes: 53 additions & 0 deletions packages/adapter-openclaw/test/setup.part-26.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,59 @@ describe('runSetup Step 5 — faucet funding', () => {
}
});

it('#1443: invokes the post-config setup hook, skipped on --dry-run', async () => {
const env = setupFaucetEnv();
try {
const afterConfigBootstrap = vi.fn(async () => {});
await runSetup(
{ workspace: env.workspace, network: 'testnet', start: false, verify: false, fund: false },
{ afterConfigBootstrap },
);
expect(afterConfigBootstrap).toHaveBeenCalledTimes(1);
expect(afterConfigBootstrap.mock.calls[0][0]).toBe(env.dkgHome);

afterConfigBootstrap.mockClear();
await runSetup(
{ workspace: env.workspace, network: 'testnet', start: false, verify: false, dryRun: true },
{ afterConfigBootstrap },
);
expect(afterConfigBootstrap).not.toHaveBeenCalled();
} finally {
env.restore();
}
});

it('#1451: honors deprecated ensureDashboardCredentials hook as a runtime fallback', async () => {
const env = setupFaucetEnv();
try {
const ensureDashboardCredentials = vi.fn(async () => {});
await runSetup(
{ workspace: env.workspace, network: 'testnet', start: false, verify: false, fund: false },
{ ensureDashboardCredentials },
);
expect(ensureDashboardCredentials).toHaveBeenCalledTimes(1);
expect(ensureDashboardCredentials.mock.calls[0][0]).toBe(env.dkgHome);
} finally {
env.restore();
}
});

it('#1443: a failing post-config setup hook is best-effort', async () => {
const env = setupFaucetEnv();
try {
const afterConfigBootstrap = vi.fn(async () => { throw new Error('invalid credential file'); });
await expect(
runSetup(
{ workspace: env.workspace, network: 'testnet', start: false, verify: false, fund: false },
{ afterConfigBootstrap },
),
).resolves.toBeUndefined();
expect(afterConfigBootstrap).toHaveBeenCalledTimes(1);
} finally {
env.restore();
}
});

it('#1306: a failing loadOpWallets is best-effort — setup still completes', async () => {
const env = setupFaucetEnv();
try {
Expand Down
Loading