@modelprofile.com/authswitch 9.0.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/authority-contract.d.ts +67 -1
- package/dist_ts/authority-contract.js +13 -2
- package/dist_ts/authority-import-contract.d.ts +6 -0
- package/dist_ts/classes.authoritybroker.d.ts +21 -15
- package/dist_ts/classes.authoritybroker.js +94 -31
- package/dist_ts/classes.authorityclient.d.ts +22 -2
- package/dist_ts/classes.authorityclient.js +79 -13
- package/dist_ts/classes.authoritydaemon.d.ts +7 -0
- package/dist_ts/classes.authoritydaemon.js +45 -31
- package/dist_ts/classes.authoritydatabase.d.ts +21 -3
- package/dist_ts/classes.authoritydatabase.js +88 -11
- package/dist_ts/classes.authorityimport.js +2 -2
- package/dist_ts/classes.authoritymodels.js +5 -3
- package/dist_ts/classes.codexmanaged.d.ts +0 -9
- package/dist_ts/classes.codexmanaged.js +8 -28
- package/dist_ts/codexcontract.d.ts +30 -0
- package/dist_ts/codexcontract.js +174 -0
- package/dist_ts/ts_migration/0004_container_setup_owner.d.ts +12 -0
- package/dist_ts/ts_migration/0004_container_setup_owner.js +19 -0
- package/dist_ts/ts_migration/index.js +3 -1
- package/package.json +8 -8
- package/readme.md +75 -17
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +71 -4
- package/ts/authority-import-contract.ts +6 -0
- package/ts/classes.authoritybroker.ts +94 -32
- package/ts/classes.authorityclient.ts +86 -15
- package/ts/classes.authoritydaemon.ts +44 -23
- package/ts/classes.authoritydatabase.ts +88 -11
- package/ts/classes.authorityimport.ts +1 -1
- package/ts/classes.authoritymodels.ts +4 -1
- package/ts/classes.codexmanaged.ts +6 -26
- package/ts/codexcontract.ts +200 -0
- package/ts/ts_migration/0004_container_setup_owner.ts +19 -0
- package/ts/ts_migration/index.ts +2 -0
|
@@ -444,7 +444,10 @@ export const assertStoredAuthorityGrant: (value: unknown) => asserts value is IS
|
|
|
444
444
|
throw new Error('Claude handoff grant has incompatible custody.');
|
|
445
445
|
}
|
|
446
446
|
if (value.purpose === 'openai_managed' && value.providerId !== 'openai') throw new Error('OpenAI grant provider mismatch.');
|
|
447
|
-
|
|
447
|
+
// No native tool refreshes a container setup token: the authority holds it, or nobody does.
|
|
448
|
+
if (value.purpose === 'claude_container_setup' && value.owner !== 'daemon' && value.owner !== 'none') {
|
|
449
|
+
throw new Error('Container setup grant has a native owner.');
|
|
450
|
+
}
|
|
448
451
|
};
|
|
449
452
|
|
|
450
453
|
export const assertStoredAuthorityBinding: (value: unknown) => asserts value is IStoredAuthorityBinding = value => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { commitinfo } from './00_commitinfo_data.js';
|
|
3
3
|
import type { AuthSwitchAuthorityBroker } from './classes.authoritybroker.js';
|
|
4
|
+
import { requireVerifiedCodexServer, verifyManagedCodexContract, type IManagedCodexContract } from './codexcontract.js';
|
|
4
5
|
|
|
5
6
|
export interface IAuthSwitchManagedCodexOptions {
|
|
6
7
|
/** Private runtime root; only the disposable control socket lives here. */
|
|
@@ -28,15 +29,6 @@ export interface IAuthSwitchCodexInteractiveCommand {
|
|
|
28
29
|
env: NodeJS.ProcessEnv;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
/**
|
|
32
|
-
* The one Codex release whose app-server contract managed Codex was verified against: the
|
|
33
|
-
* external-token login and its refresh callback, thread/turn start and resume, `--listen` and
|
|
34
|
-
* how it publishes that socket, `--strict-config`, the ephemeral credential store and
|
|
35
|
-
* `--remote`. Both the CLI and the app-server it serves must report exactly this release;
|
|
36
|
-
* any other is refused unverified.
|
|
37
|
-
*/
|
|
38
|
-
export const MANAGED_CODEX_VERSION = '0.157.0';
|
|
39
|
-
|
|
40
32
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
41
33
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
42
34
|
const isAlive = (child: plugins.childProcess.ChildProcess): boolean => child.exitCode === null && child.signalCode === null;
|
|
@@ -162,20 +154,6 @@ export class AuthSwitchManagedCodex {
|
|
|
162
154
|
return picked;
|
|
163
155
|
}
|
|
164
156
|
|
|
165
|
-
private async requireVersion(): Promise<void> {
|
|
166
|
-
const executable = this.options.executable ?? 'codex';
|
|
167
|
-
const output = await new Promise<string>((resolve, reject) => {
|
|
168
|
-
plugins.childProcess.execFile(executable, ['--version'], { timeout: 5_000, maxBuffer: 4096,
|
|
169
|
-
windowsHide: true, env: this.environment() }, (error, stdout) => {
|
|
170
|
-
if (error) reject(new Error('Managed Codex version probe failed.'));
|
|
171
|
-
else resolve(stdout.trim());
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
if (output !== `codex-cli ${MANAGED_CODEX_VERSION}`) {
|
|
175
|
-
throw new Error(`Managed Codex requires verified codex-cli ${MANAGED_CODEX_VERSION}.`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
157
|
public start(): Promise<void> {
|
|
180
158
|
if (this.closed || this.startPromise) throw new Error('Managed Codex can start only once.');
|
|
181
159
|
this.startPromise = this.performStart();
|
|
@@ -206,7 +184,9 @@ export class AuthSwitchManagedCodex {
|
|
|
206
184
|
} catch (error) {
|
|
207
185
|
if (!isRecord(error) || error.code !== 'ENOENT') throw error;
|
|
208
186
|
}
|
|
209
|
-
|
|
187
|
+
// Nothing is recorded or started for a Codex that is too old or no longer offers what this uses.
|
|
188
|
+
const contract: IManagedCodexContract = await verifyManagedCodexContract({
|
|
189
|
+
executable: this.options.executable ?? 'codex', env: this.environment(), scratchDirectory: this.runDirectory });
|
|
210
190
|
if (this.closed) throw new Error('Managed Codex startup was cancelled.');
|
|
211
191
|
const initial = await this.options.broker.resolveAccess(this.options.bindingId,
|
|
212
192
|
this.options.capability, 60_000);
|
|
@@ -258,8 +238,8 @@ export class AuthSwitchManagedCodex {
|
|
|
258
238
|
});
|
|
259
239
|
this.client = client;
|
|
260
240
|
await client.connect();
|
|
261
|
-
// The executable can be replaced between the
|
|
262
|
-
|
|
241
|
+
// The executable can be replaced between the check and the spawn (Codex updates itself).
|
|
242
|
+
requireVerifiedCodexServer(contract, client.serverVersion);
|
|
263
243
|
const login: unknown = await client.request('account/login/start', {
|
|
264
244
|
type: 'chatgptAuthTokens', accessToken: initial.accessToken, chatgptAccountId: initial.accountId,
|
|
265
245
|
}, 5_000);
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { AuthSwitchRefusal } from './authority-contract.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What managed Codex needs from the Codex it starts, checked at every start instead of pinning one release.
|
|
6
|
+
*
|
|
7
|
+
* The floor is `@modelprofile.com/mcp-crossharness`'s: 0.156.0 is the first release that publishes the
|
|
8
|
+
* `--listen unix://` alias managed Codex connects through, and every other surface it uses is older. Above
|
|
9
|
+
* the floor nothing is assumed. Codex describes its own app-server protocol (`app-server
|
|
10
|
+
* generate-json-schema`), and that description must still offer every request managed Codex sends and the one
|
|
11
|
+
* it answers, with the fields it sends. The external-token login in particular is marked unstable upstream,
|
|
12
|
+
* so a release that drops or reshapes it is refused at start, by name, rather than failing a session later
|
|
13
|
+
* -- the refresh callback, for one, would otherwise only be missed at the first expired token.
|
|
14
|
+
*/
|
|
15
|
+
export const MANAGED_CODEX_MINIMUM_VERSION = plugins.crossharness.CODEX_UNIX_SOCKET_MINIMUM_VERSION;
|
|
16
|
+
|
|
17
|
+
/** The verified release, exactly as `codex --version` printed it; the app-server must report the same. */
|
|
18
|
+
export interface IManagedCodexContract {
|
|
19
|
+
version: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IManagedCodexContractOptions {
|
|
23
|
+
executable: string;
|
|
24
|
+
env: NodeJS.ProcessEnv;
|
|
25
|
+
/** A private, existing directory; the schema is written below it and removed before this returns. */
|
|
26
|
+
scratchDirectory: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type TSchema = Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
const supportInstruction = 'Managed Codex needs an authswitch release that supports this Codex, or a Codex '
|
|
32
|
+
+ 'release that still offers it.';
|
|
33
|
+
const maximumSchemaFileBytes = 16 * 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
const isRecord = (value: unknown): value is TSchema =>
|
|
36
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
37
|
+
const strings = (value: unknown): string[] =>
|
|
38
|
+
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
|
39
|
+
|
|
40
|
+
const run = (executable: string, args: string[], env: NodeJS.ProcessEnv, timeoutMs: number): Promise<string | null> =>
|
|
41
|
+
new Promise(resolve => {
|
|
42
|
+
plugins.childProcess.execFile(executable, args, { timeout: timeoutMs, maxBuffer: 64 * 1024,
|
|
43
|
+
windowsHide: true, env }, (error, stdout) => resolve(error ? null : stdout));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** Follows local `#/definitions/<name>` references; any other reference resolves to nothing. */
|
|
47
|
+
const resolve = (root: TSchema, node: unknown): TSchema | null => {
|
|
48
|
+
let current = node;
|
|
49
|
+
for (let depth = 0; depth < 8 && isRecord(current); depth++) {
|
|
50
|
+
const ref = current.$ref;
|
|
51
|
+
if (typeof ref !== 'string') return current;
|
|
52
|
+
const match = /^#\/definitions\/([^/]+)$/.exec(ref);
|
|
53
|
+
const definitions = root.definitions;
|
|
54
|
+
current = match && isRecord(definitions) ? definitions[match[1]] : undefined;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** The schema alternatives a node offers: itself, or each member of its `oneOf`/`anyOf`. */
|
|
60
|
+
const alternatives = (root: TSchema, node: unknown): TSchema[] => {
|
|
61
|
+
const resolved = resolve(root, node);
|
|
62
|
+
if (!resolved) return [];
|
|
63
|
+
const members = [...(Array.isArray(resolved.oneOf) ? resolved.oneOf : []),
|
|
64
|
+
...(Array.isArray(resolved.anyOf) ? resolved.anyOf : [])];
|
|
65
|
+
return members.length ? members.flatMap(member => alternatives(root, member)) : [resolved];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Every string a node accepts as an enum value, through references and alternatives. */
|
|
69
|
+
const enumValues = (root: TSchema, node: unknown): string[] =>
|
|
70
|
+
alternatives(root, node).flatMap(member => strings(member.enum));
|
|
71
|
+
|
|
72
|
+
const acceptsNull = (root: TSchema, node: unknown): boolean => alternatives(root, node)
|
|
73
|
+
.some(member => member.type === 'null' || strings(member.type).includes('null'));
|
|
74
|
+
|
|
75
|
+
/** The object alternative whose `discriminator` property is exactly `value`, or the node itself. */
|
|
76
|
+
const variant = (root: TSchema, node: unknown, discriminator: string, value: string): TSchema | null =>
|
|
77
|
+
alternatives(root, node).find(member => {
|
|
78
|
+
const properties = member.properties;
|
|
79
|
+
const tag = isRecord(properties) ? resolve(root, properties[discriminator]) : null;
|
|
80
|
+
return tag !== null && strings(tag.enum).length === 1 && strings(tag.enum)[0] === value;
|
|
81
|
+
}) ?? null;
|
|
82
|
+
|
|
83
|
+
/** Declares every field managed Codex sends, and requires none it does not send. */
|
|
84
|
+
const acceptsFields = (object: TSchema | null, sent: readonly string[]): object is TSchema => {
|
|
85
|
+
if (!object || !isRecord(object.properties)) return false;
|
|
86
|
+
const properties = object.properties;
|
|
87
|
+
return sent.every(field => Object.hasOwn(properties, field))
|
|
88
|
+
&& strings(object.required).every(field => sent.includes(field));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const property = (object: TSchema | null, name: string): unknown =>
|
|
92
|
+
object && isRecord(object.properties) ? object.properties[name] : undefined;
|
|
93
|
+
|
|
94
|
+
const requestMethods = (root: TSchema): string[] => alternatives(root, root)
|
|
95
|
+
.flatMap(member => isRecord(member.properties) ? strings(resolve(root, member.properties.method)?.enum) : []);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Each surface managed Codex uses, and whether a protocol description still offers it. Reads nothing but
|
|
99
|
+
* the schema files named here, so a release that moves a file fails the element that needs it.
|
|
100
|
+
*/
|
|
101
|
+
const elements: ReadonlyArray<{ name: string; files: string[]; offered: (schemas: TSchema[]) => boolean }> = [
|
|
102
|
+
{
|
|
103
|
+
name: 'the requests account/login/start, thread/start, thread/resume and turn/start',
|
|
104
|
+
files: ['ClientRequest.json'],
|
|
105
|
+
offered: ([client]) => ['account/login/start', 'thread/start', 'thread/resume', 'turn/start']
|
|
106
|
+
.every(method => requestMethods(client).includes(method)),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'the external ChatGPT token login (account/login/start chatgptAuthTokens)',
|
|
110
|
+
files: ['v2/LoginAccountParams.json', 'v2/LoginAccountResponse.json'],
|
|
111
|
+
offered: ([params, response]) =>
|
|
112
|
+
acceptsFields(variant(params, params, 'type', 'chatgptAuthTokens'), ['type', 'accessToken', 'chatgptAccountId'])
|
|
113
|
+
&& variant(response, response, 'type', 'chatgptAuthTokens') !== null,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'the token refresh callback (account/chatgptAuthTokens/refresh)',
|
|
117
|
+
files: ['ServerRequest.json', 'ChatgptAuthTokensRefreshParams.json', 'ChatgptAuthTokensRefreshResponse.json'],
|
|
118
|
+
offered: ([server, params, response]) => requestMethods(server).includes('account/chatgptAuthTokens/refresh')
|
|
119
|
+
&& enumValues(params, property(params, 'reason')).includes('unauthorized')
|
|
120
|
+
&& acceptsFields(response, ['accessToken', 'chatgptAccountId', 'chatgptPlanType'])
|
|
121
|
+
&& acceptsNull(response, property(response, 'chatgptPlanType')),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'thread/start and thread/resume with a working directory, approval policy and model',
|
|
125
|
+
files: ['v2/ThreadStartParams.json', 'v2/ThreadResumeParams.json'],
|
|
126
|
+
offered: ([start, resume]) => acceptsFields(start, ['cwd', 'approvalPolicy', 'model'])
|
|
127
|
+
&& acceptsFields(resume, ['threadId', 'cwd', 'approvalPolicy', 'model'])
|
|
128
|
+
&& [start, resume].every(schema => ['never', 'on-request']
|
|
129
|
+
.every(policy => enumValues(schema, property(schema, 'approvalPolicy')).includes(policy))),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'turn/start with text input, approval policy and model',
|
|
133
|
+
files: ['v2/TurnStartParams.json'],
|
|
134
|
+
offered: ([turn]) => {
|
|
135
|
+
if (!acceptsFields(turn, ['threadId', 'input', 'approvalPolicy', 'model'])) return false;
|
|
136
|
+
const input = resolve(turn, property(turn, 'input'));
|
|
137
|
+
return acceptsFields(variant(turn, input?.items, 'type', 'text'), ['type', 'text', 'text_elements'])
|
|
138
|
+
&& ['never', 'on-request']
|
|
139
|
+
.every(policy => enumValues(turn, property(turn, 'approvalPolicy')).includes(policy));
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const readSchema = async (directory: string, relative: string): Promise<TSchema | null> => {
|
|
145
|
+
try {
|
|
146
|
+
const file = plugins.path.join(directory, ...relative.split('/'));
|
|
147
|
+
const stat = await plugins.fs.promises.lstat(file);
|
|
148
|
+
if (!stat.isFile() || stat.size > maximumSchemaFileBytes) return null;
|
|
149
|
+
const value: unknown = JSON.parse(await plugins.fs.promises.readFile(file, 'utf8'));
|
|
150
|
+
return isRecord(value) ? value : null;
|
|
151
|
+
} catch { return null; }
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const refuse = (instruction: string): never => { throw new AuthSwitchRefusal('codex_unsupported', instruction); };
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Proves, before anything is recorded or started, that `executable` is a Codex managed Codex can run: its
|
|
158
|
+
* version is the floor or newer, and its own protocol description offers every surface managed Codex uses.
|
|
159
|
+
* Every refusal is `codex_unsupported` and names the version or the missing surface.
|
|
160
|
+
*/
|
|
161
|
+
export const verifyManagedCodexContract = async (options: IManagedCodexContractOptions): Promise<IManagedCodexContract> => {
|
|
162
|
+
const printed = await run(options.executable, ['--version'], options.env, 5_000);
|
|
163
|
+
const version = /^codex-cli (\S+)$/.exec(printed?.trim() ?? '')?.[1] ?? null;
|
|
164
|
+
try {
|
|
165
|
+
plugins.crossharness.requireCodexVersion(version, MANAGED_CODEX_MINIMUM_VERSION);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (!(error instanceof plugins.crossharness.CodexVersionUnsupportedError)) throw error;
|
|
168
|
+
refuse(error.reason === 'unreadable'
|
|
169
|
+
? `This Codex did not report a readable version. Install Codex ${MANAGED_CODEX_MINIMUM_VERSION} or newer.`
|
|
170
|
+
: `Codex ${version} is older than ${MANAGED_CODEX_MINIMUM_VERSION}, the first release managed Codex runs. `
|
|
171
|
+
+ 'Update Codex, then start managed Codex again.');
|
|
172
|
+
}
|
|
173
|
+
const verified = version!;
|
|
174
|
+
const directory = await plugins.fs.promises.mkdtemp(plugins.path.join(options.scratchDirectory, 'schema-'));
|
|
175
|
+
try {
|
|
176
|
+
if (await run(options.executable, ['app-server', 'generate-json-schema', '--out', directory],
|
|
177
|
+
options.env, 30_000) === null) {
|
|
178
|
+
refuse(`Codex ${verified} did not describe its app-server protocol, so managed Codex cannot check it. `
|
|
179
|
+
+ supportInstruction);
|
|
180
|
+
}
|
|
181
|
+
for (const element of elements) {
|
|
182
|
+
const schemas = await Promise.all(element.files.map(file => readSchema(directory, file)));
|
|
183
|
+
if (schemas.some(schema => schema === null) || !element.offered(schemas as TSchema[])) {
|
|
184
|
+
refuse(`Codex ${verified} does not offer ${element.name}. ${supportInstruction}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
await plugins.fs.promises.rm(directory, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
return { version: verified };
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** The app-server a start connected to must be the release that was verified; Codex can replace itself. */
|
|
194
|
+
export const requireVerifiedCodexServer = (contract: IManagedCodexContract, serverVersion: string | undefined): void => {
|
|
195
|
+
if (serverVersion === contract.version) return;
|
|
196
|
+
const reported = serverVersion !== undefined && plugins.crossharness.parseCodexVersion(serverVersion)
|
|
197
|
+
? `Codex ${serverVersion}` : 'an unreadable version';
|
|
198
|
+
refuse(`The Codex app-server reports ${reported}, not the checked Codex ${contract.version}; Codex was `
|
|
199
|
+
+ 'replaced while managed Codex started. Start managed Codex again.');
|
|
200
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Refuses a store holding a Claude container setup grant that a native tool would own.
|
|
5
|
+
*
|
|
6
|
+
* Until 9.1.0 the grant record allowed a container setup grant to be owned by `legacy_native`. No release
|
|
7
|
+
* ever wrote one: the importer creates only Claude host, OpenCode and managed OpenAI grants, and no route
|
|
8
|
+
* creates a container setup grant at all. 9.1.0 narrows the record to what the authority means by it -- the
|
|
9
|
+
* authority holds that token, or nobody does -- so a row outside that would fail every read of its account.
|
|
10
|
+
* Such a row was never written by this package, and nothing about it can be repaired by guessing, so the
|
|
11
|
+
* store is refused before the first write and left exactly as it was.
|
|
12
|
+
*/
|
|
13
|
+
export const refuseNativeContainerSetupOwners = async (db: plugins.nosqldb.SmartdataDb): Promise<void> => {
|
|
14
|
+
const grants = db.mongoDb.collection('authswitch_authority_grants');
|
|
15
|
+
if (await grants.findOne({ purpose: 'claude_container_setup', owner: { $nin: ['daemon', 'none'] } })) {
|
|
16
|
+
throw new Error('Authswitch authority store holds a Claude container setup login owned by a native tool, '
|
|
17
|
+
+ 'which no release could have recorded; set the store aside and sign in to each account again.');
|
|
18
|
+
}
|
|
19
|
+
};
|
package/ts/ts_migration/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import { migrateAuthorityMeta } from './0001_authority_meta.js';
|
|
3
3
|
import { removeAuthorityBackupRecords } from './0002_remove_backup_records.js';
|
|
4
4
|
import { addClaudeHandoffProofFailure } from './0003_claude_handoff_proof.js';
|
|
5
|
+
import { refuseNativeContainerSetupOwners } from './0004_container_setup_owner.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Runs on every start of an existing store, in this fixed order. Each module recognises the published shapes it
|
|
@@ -11,4 +12,5 @@ export const runAuthorityMigrations = async (db: plugins.nosqldb.SmartdataDb): P
|
|
|
11
12
|
await migrateAuthorityMeta(db);
|
|
12
13
|
await removeAuthorityBackupRecords(db);
|
|
13
14
|
await addClaudeHandoffProofFailure(db);
|
|
15
|
+
await refuseNativeContainerSetupOwners(db);
|
|
14
16
|
};
|