mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 20:55:48 +00:00
fix: preserve migrated auth isolation
This commit is contained in:
@@ -203,7 +203,22 @@ class DeviceAuthStore private constructor(
|
||||
removeLegacyEntry(normalizedDevice, normalizedRole)
|
||||
return null
|
||||
}
|
||||
return migrateLegacyEntry(normalizedDevice, normalizedRole)
|
||||
return migrateLegacyEntriesForDevice(normalizedDevice)[normalizedRole]
|
||||
}
|
||||
|
||||
private fun migrateLegacyEntriesForDevice(normalizedDevice: String): Map<String, DeviceAuthEntry> {
|
||||
val prefix = tokenKeyPrefix(normalizedDevice)
|
||||
return legacyPrefs
|
||||
.keysWithPrefix(prefix)
|
||||
.mapNotNull { key ->
|
||||
val role = normalizeRole(key.removePrefix(prefix))
|
||||
if (role.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
migrateLegacyEntry(normalizedDevice, role)?.let { role to it }
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
private fun migrateLegacyEntry(
|
||||
|
||||
@@ -83,6 +83,39 @@ class DeviceAuthStoreTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadEntryMigratesAllLegacyRolesBeforeSQLiteRowsExist() {
|
||||
val app = RuntimeEnvironment.getApplication()
|
||||
val prefs = legacyPrefs(app)
|
||||
prefs.putString("gateway.deviceToken.device-1.operator", " operator-token ")
|
||||
prefs.putString(
|
||||
"gateway.deviceTokenMeta.device-1.operator",
|
||||
"""{"scopes":["operator.write"],"updatedAtMs":1700000000000}""",
|
||||
)
|
||||
prefs.putString("gateway.deviceToken.device-1.node", " node-token ")
|
||||
prefs.putString(
|
||||
"gateway.deviceTokenMeta.device-1.node",
|
||||
"""{"scopes":["node.connect"],"updatedAtMs":1700000000001}""",
|
||||
)
|
||||
val store = DeviceAuthStore(app, legacyPrefsOverride = prefs)
|
||||
|
||||
val operator = store.loadEntry("device-1", "operator")
|
||||
val node = store.loadEntry("device-1", "node")
|
||||
|
||||
assertEquals("operator-token", operator?.token)
|
||||
assertEquals(listOf("operator.write"), operator?.scopes)
|
||||
assertEquals("node-token", node?.token)
|
||||
assertEquals(listOf("node.connect"), node?.scopes)
|
||||
assertEquals(
|
||||
"__openclaw_secure_prefs__",
|
||||
OpenClawSQLiteStateStore(app).readDeviceAuthToken("device-1", "operator")?.token,
|
||||
)
|
||||
assertEquals(
|
||||
"__openclaw_secure_prefs__",
|
||||
OpenClawSQLiteStateStore(app).readDeviceAuthToken("device-1", "node")?.token,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadEntryDoesNotResurrectLegacyRoleAfterSQLiteRowsExist() {
|
||||
val app = RuntimeEnvironment.getApplication()
|
||||
|
||||
@@ -396,6 +396,28 @@ describe("createAcpxRuntimeService", () => {
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
it("scopes generated wrapper roots by state dir and gateway instance", async () => {
|
||||
const stateDirA = path.join(await makeTempDir(), "state");
|
||||
const stateDirB = path.join(await makeTempDir(), "state");
|
||||
|
||||
const rootA1 = resolveAcpxWrapperRoot({
|
||||
gatewayInstanceId: "gw-a",
|
||||
stateDir: stateDirA,
|
||||
});
|
||||
const rootA2 = resolveAcpxWrapperRoot({
|
||||
gatewayInstanceId: "gw-b",
|
||||
stateDir: stateDirA,
|
||||
});
|
||||
const rootB1 = resolveAcpxWrapperRoot({
|
||||
gatewayInstanceId: "gw-a",
|
||||
stateDir: stateDirB,
|
||||
});
|
||||
|
||||
expect(rootA1).not.toBe(rootA2);
|
||||
expect(rootA1).not.toBe(rootB1);
|
||||
expect(path.dirname(path.dirname(rootA1))).toBe(resolveAcpxWrapperRoot());
|
||||
});
|
||||
|
||||
it("runs wrapper-root orphan cleanup before dropping pending ACPX leases", async () => {
|
||||
const workspaceDir = await makeTempDir();
|
||||
const ctx = createServiceContext(workspaceDir);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { inspect } from "node:util";
|
||||
@@ -78,8 +78,28 @@ type CreateAcpxRuntimeServiceParams = {
|
||||
processCleanupDeps?: AcpxProcessCleanupDeps;
|
||||
};
|
||||
|
||||
export function resolveAcpxWrapperRoot(): string {
|
||||
return path.join(resolvePreferredOpenClawTmpDir(), "acpx");
|
||||
function sanitizeWrapperRootSegment(value: string, fallback: string): string {
|
||||
const segment = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
return segment || fallback;
|
||||
}
|
||||
|
||||
function hashWrapperRootStateDir(stateDir: string): string {
|
||||
return createHash("sha256").update(path.resolve(stateDir)).digest("hex").slice(0, 16);
|
||||
}
|
||||
|
||||
export function resolveAcpxWrapperRoot(params?: {
|
||||
gatewayInstanceId: string;
|
||||
stateDir: string;
|
||||
}): string {
|
||||
const baseRoot = path.join(resolvePreferredOpenClawTmpDir(), "acpx");
|
||||
if (!params) {
|
||||
return baseRoot;
|
||||
}
|
||||
return path.join(
|
||||
baseRoot,
|
||||
hashWrapperRootStateDir(params.stateDir),
|
||||
sanitizeWrapperRootSegment(params.gatewayInstanceId, "gateway"),
|
||||
);
|
||||
}
|
||||
|
||||
function loadRuntimeModule(): Promise<AcpxRuntimeModule> {
|
||||
@@ -533,7 +553,13 @@ export function createAcpxRuntimeService(
|
||||
...basePluginConfig,
|
||||
probeAgent: basePluginConfig.probeAgent ?? resolveAllowedAgentsProbeAgent(ctx),
|
||||
};
|
||||
const wrapperRoot = resolveAcpxWrapperRoot();
|
||||
const gatewayInstanceId = await measureAcpxStartup(ctx, "gateway-instance-id", () =>
|
||||
resolveGatewayInstanceId(),
|
||||
);
|
||||
const wrapperRoot = resolveAcpxWrapperRoot({
|
||||
gatewayInstanceId,
|
||||
stateDir: ctx.stateDir,
|
||||
});
|
||||
const pluginConfig = await measureAcpxStartup(ctx, "config.prepare-codex-auth", () =>
|
||||
prepareAcpxCodexAuthConfig({
|
||||
pluginConfig: effectiveBasePluginConfig,
|
||||
@@ -544,9 +570,6 @@ export function createAcpxRuntimeService(
|
||||
await measureAcpxStartup(ctx, "filesystem.prepare", async () => {
|
||||
await fs.mkdir(wrapperRoot, { recursive: true });
|
||||
});
|
||||
const gatewayInstanceId = await measureAcpxStartup(ctx, "gateway-instance-id", () =>
|
||||
resolveGatewayInstanceId(),
|
||||
);
|
||||
const processLeaseStore = createAcpxProcessLeaseStore();
|
||||
const startupReap = await measureAcpxStartup(ctx, "process-leases.reap", () =>
|
||||
reapOpenAcpxProcessLeases({
|
||||
|
||||
Reference in New Issue
Block a user