@ceralive/modem-control 1.1.0 → 1.2.1
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/README.md +35 -6
- package/dist/backend/at-lease.d.ts +3 -1
- package/dist/backend/at-lease.js +11 -2
- package/dist/backend/recovery-ladder.d.ts +1 -1
- package/dist/backend/transition-preconditions.d.ts +35 -10
- package/dist/backend/transition-preconditions.js +78 -10
- package/dist/backend/usb-mode-transition.js +39 -19
- package/dist/capability/five-g-preference.d.ts +2 -2
- package/dist/capability/support-claim.d.ts +2 -2
- package/dist/domain/shadow-divergence.d.ts +1 -1
- package/dist/fcc/coverage.d.ts +20 -20
- package/dist/hardware/router-parsers.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/journal/legacy-ceraui.d.ts +2 -2
- package/dist/observations/metric.d.ts +15 -1
- package/dist/observations/provenance.d.ts +2 -2
- package/dist/observations/reading.d.ts +1 -1
- package/dist/observations/state-separation.d.ts +1 -1
- package/dist/operation-ids.d.ts +2 -0
- package/dist/operation-ids.js +26 -0
- package/dist/ports/location.d.ts +1 -1
- package/dist/providers/contracts.d.ts +2 -2
- package/dist/providers/huawei-hilink/provider.d.ts +12 -12
- package/dist/providers/huawei-hilink/runtime.d.ts +2 -2
- package/dist/providers/modem-manager/errors.d.ts +1 -1
- package/dist/providers/modem-manager/generic-operations.d.ts +3 -1
- package/dist/providers/modem-manager/generic-operations.js +3 -1
- package/dist/providers/modem-manager/index.d.ts +1 -0
- package/dist/providers/modem-manager/index.js +1 -0
- package/dist/providers/modem-manager/provider.d.ts +2 -0
- package/dist/providers/modem-manager/provider.js +3 -0
- package/dist/providers/modem-manager/runtime-composition-operation.d.ts +32 -0
- package/dist/providers/modem-manager/runtime-composition-operation.js +153 -0
- package/dist/providers/modem-manager/types.d.ts +2 -0
- package/dist/providers/network-manager/types.d.ts +11 -3
- package/dist/providers/ufi-himi/operations.d.ts +1 -1
- package/dist/providers/ufi-himi/prohibitions.d.ts +22 -22
- package/dist/providers/ufi-himi/provider.d.ts +4 -4
- package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +1 -1
- package/dist/providers/ufi-himi/transport.d.ts +2 -2
- package/dist/providers/zte-goform/provider.d.ts +18 -13
- package/dist/providers/zte-goform/provider.js +16 -16
- package/dist/providers/zte-goform/session.d.ts +8 -2
- package/dist/providers/zte-goform/session.js +95 -31
- package/dist/radio/mode-combinations.d.ts +11 -1
- package/dist/safety/flock-resource-ownership.js +18 -31
- package/dist/usb-mode/catalog-schema.d.ts +18 -18
- package/dist/usb-mode/index.d.ts +1 -0
- package/dist/usb-mode/index.js +1 -0
- package/dist/usb-mode/ingestion.d.ts +4 -4
- package/dist/usb-mode/runtime-capability.d.ts +59 -0
- package/dist/usb-mode/runtime-capability.js +157 -0
- package/dist/ussd/refusal.d.ts +1 -1
- package/dist/ussd/session.d.ts +24 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ tree; see [Shape gate](#shape-gate).
|
|
|
16
16
|
npm install @ceralive/modem-control # or: bun add @ceralive/modem-control
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
ESM only (`"type": "module"`). Node 26 and Bun 1.
|
|
19
|
+
ESM only (`"type": "module"`). Node 26 and Bun 1.4 are the runtimes the published
|
|
20
20
|
tarball is exercised against on every CI run.
|
|
21
21
|
|
|
22
22
|
## Public entry points
|
|
@@ -83,10 +83,11 @@ provider runtime. See [`../docs/HUAWEI-HILINK-PROVIDER.md`](../docs/HUAWEI-HILIN
|
|
|
83
83
|
|
|
84
84
|
### ZTE goform provider
|
|
85
85
|
|
|
86
|
-
`createZteGoformDefinition()` exposes
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
`createZteGoformDefinition()` exposes three incompatible authentication profiles without
|
|
87
|
+
fallback between them: MF79U legacy base64 under `LOGIN`, MF79U `LD`-salted SHA-256 under
|
|
88
|
+
the same bare `LOGIN`, and MF266 salted SHA-256 under `LOGIN_MULTI_USER`. One batched
|
|
89
|
+
pre-auth evidence GET selects the exact shape and refuses a reported lockout before any
|
|
90
|
+
credential POST. MF266 derives `AD` from the probed version data and `RD`. Session material stays
|
|
90
91
|
in memory. Unknown ZTE firmware is fingerprinted into a read-only telemetry profile, and
|
|
91
92
|
Wi-Fi writes are absent from every operation surface. See
|
|
92
93
|
[`../docs/MF79U-DIAGNOSIS.md`](../docs/MF79U-DIAGNOSIS.md).
|
|
@@ -110,7 +111,10 @@ through `OperationEngine` are refused before execution too.
|
|
|
110
111
|
`05c6:9024` is evidence of an RNDIS+ADB composition, not a permission. `05c6:9091` is a
|
|
111
112
|
firmware-chosen product id and is **not** proof of DIAG — only an interface descriptor is,
|
|
112
113
|
and production access stays `prohibited` regardless. The supervised, read-only, bench-only
|
|
113
|
-
probe is documented in [`../docs/UFI-DIAG-PROBE.md`](../docs/UFI-DIAG-PROBE.md)
|
|
114
|
+
probe is documented in [`../docs/UFI-DIAG-PROBE.md`](../docs/UFI-DIAG-PROBE.md), together
|
|
115
|
+
with the read-only descriptor capture (`scripts/ufi-himi-capture.sh`) and its bundle
|
|
116
|
+
schema, interface-role classifier and redaction sweep (`scripts/ufi-himi-evidence.ts`).
|
|
117
|
+
Neither ships in the package: `files: ["dist"]`, and bench tooling is not a public surface.
|
|
114
118
|
|
|
115
119
|
### NetworkManager adapter — saved vs applied
|
|
116
120
|
|
|
@@ -160,6 +164,26 @@ ModemManager exposes no USB `vid:pid`, so the package cannot build a `BandSku` a
|
|
|
160
164
|
Both operations expose `describe(context)` alongside their static `descriptor`, because
|
|
161
165
|
a static descriptor cannot carry a device's own catalog or its certification state.
|
|
162
166
|
|
|
167
|
+
### USB composition — runtime-derived targets, two proof tiers
|
|
168
|
+
|
|
169
|
+
`operations().usbComposition` asks a known vendor for its current and enumerated USB
|
|
170
|
+
composition modes and offers only targets from that reply after the reply also proves a
|
|
171
|
+
represented return path. Its suppression vocabulary is `unknown-vendor`,
|
|
172
|
+
`no-return-path`, `blocked-by-state`, and `provisioning-disabled`; suppressed states expose
|
|
173
|
+
no targets. Unknown/disabled/blocked decisions happen before transport contact, and a
|
|
174
|
+
capability read sends only the named READ/TEST forms, never a SET.
|
|
175
|
+
|
|
176
|
+
The current and enumeration replies must both carry a successful AT result before their raw
|
|
177
|
+
bodies are parsed. If either query fails, the operation returns the existing `no-return-path`
|
|
178
|
+
suppression with an empty offer; parseable-looking data followed by `ERROR` cannot make a
|
|
179
|
+
disruptive target writable.
|
|
180
|
+
|
|
181
|
+
A reviewed catalog transition still provides the strongest success proof: canonical mode
|
|
182
|
+
and USB descriptors must both match. Without one, the weaker fallback proof is the
|
|
183
|
+
re-enumerated device's own post-switch READ reporting the target. AT `OK` is never success.
|
|
184
|
+
The write remains disruptive and requires admission, journal, rollback, and readback hooks.
|
|
185
|
+
Band writes do not share this policy and remain behind their four-proof certification gate.
|
|
186
|
+
|
|
163
187
|
### SIM presence is evidence, never inference
|
|
164
188
|
|
|
165
189
|
`readSimPresence` returns the presence together with the `SimPresenceEvidence` that
|
|
@@ -188,6 +212,11 @@ holder PID/start-time metadata, and clean release when the holder process dies.
|
|
|
188
212
|
is mandatory input; `DEFAULT_MODEM_CONTROL_LOCK_PATH` is only a conventional value callers
|
|
189
213
|
may select. There is no pass-through ownership implementation.
|
|
190
214
|
|
|
215
|
+
The adapter holds the lock with an external `/bin/cat` whose pipe round-trip acknowledges
|
|
216
|
+
successful acquisition. It never launches `process.execPath -e`: a compiled Bun executable's
|
|
217
|
+
`process.execPath` points back to the application, so re-executing it would parse `-e` as an
|
|
218
|
+
application option and misreport the resulting exit as contention.
|
|
219
|
+
|
|
191
220
|
One `createModemControlCompositionRoot()` may be live per process. A second construction
|
|
192
221
|
throws, and `actorFor(physicalModemId)` shares one actor for that modem across all callers in
|
|
193
222
|
the root. `UhubctlPort` has no control-package implementation; an embedding process must
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type EpochMillis } from '../domain/index.js';
|
|
2
|
-
/**
|
|
2
|
+
/** Named read-only fence: the exact vendor READ/TEST forms reviewed for runtime discovery. */
|
|
3
|
+
export declare const AT_RUNTIME_QUERY_ALLOWLIST: ReadonlySet<string>;
|
|
4
|
+
/** Identify plus reviewed runtime queries. Exact catalog/runtime SET commands union in per use. */
|
|
3
5
|
export declare const AT_BASELINE_ALLOWLIST: ReadonlySet<string>;
|
|
4
6
|
/** Union the baseline allowlist with a catalog entry's declared transition commands. */
|
|
5
7
|
export declare function computeAtAllowlist(commands: Iterable<string>): ReadonlySet<string>;
|
package/dist/backend/at-lease.js
CHANGED
|
@@ -12,8 +12,17 @@
|
|
|
12
12
|
// stripped before it is stored.
|
|
13
13
|
import { epochMillis } from '../domain/index.js';
|
|
14
14
|
import { redact } from '../redact.js';
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import { RUNTIME_COMPOSITION_QUERY_REGISTRY } from '../usb-mode/runtime-capability.js';
|
|
16
|
+
/** Named read-only fence: the exact vendor READ/TEST forms reviewed for runtime discovery. */
|
|
17
|
+
export const AT_RUNTIME_QUERY_ALLOWLIST = new Set(Object.values(RUNTIME_COMPOSITION_QUERY_REGISTRY).flatMap(({ current, enumerate }) => [
|
|
18
|
+
current,
|
|
19
|
+
enumerate,
|
|
20
|
+
]));
|
|
21
|
+
/** Identify plus reviewed runtime queries. Exact catalog/runtime SET commands union in per use. */
|
|
22
|
+
export const AT_BASELINE_ALLOWLIST = new Set([
|
|
23
|
+
'ATI',
|
|
24
|
+
...AT_RUNTIME_QUERY_ALLOWLIST,
|
|
25
|
+
]);
|
|
17
26
|
/** Union the baseline allowlist with a catalog entry's declared transition commands. */
|
|
18
27
|
export function computeAtAllowlist(commands) {
|
|
19
28
|
return new Set([...AT_BASELINE_ALLOWLIST, ...commands]);
|
|
@@ -5,7 +5,7 @@ import type { ModemActor } from './modem-actor.js';
|
|
|
5
5
|
import { type PowerHook } from './power-contract.js';
|
|
6
6
|
import type { FaultAttribution } from './recovery-attribution.js';
|
|
7
7
|
import { type RecoveryBudget, type RecoveryBudgetState } from './recovery-budget.js';
|
|
8
|
-
export declare const LADDER_ORDER: readonly [
|
|
8
|
+
export declare const LADDER_ORDER: readonly ['nmCycle', 'mmCycle', 'reset', 'powerCycle'];
|
|
9
9
|
export type RecoveryRung = (typeof LADDER_ORDER)[number];
|
|
10
10
|
/** Context handed to each disruptive rung. */
|
|
11
11
|
export interface RecoveryStepContext {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EpochMillis, IdentityConfidence } from '../domain/index.js';
|
|
2
2
|
import type { ConnectionId, DeviceIfname } from '../ports/index.js';
|
|
3
|
-
import { type CatalogEntry, type CertifiedCatalog, type MmUsbMode, type PermittedTransition, type SkuDiscriminator } from '../usb-mode/index.js';
|
|
3
|
+
import { type CatalogEntry, type CertifiedCatalog, type MmUsbMode, type PermittedTransition, type RuntimeCompositionCapability, type RuntimeCompositionMode, type RuntimeCompositionVendor, type SkuDiscriminator } from '../usb-mode/index.js';
|
|
4
4
|
import type { InterlockTarget, LifecycleInterlock } from './lifecycle-interlock.js';
|
|
5
5
|
/** A held "transition active" interlock — released when the transaction ends. */
|
|
6
6
|
export interface InterlockHold {
|
|
@@ -21,12 +21,8 @@ export interface TransitionReadiness {
|
|
|
21
21
|
/** The identity confidence from A3.2's ladder; `'low'` refuses the transition. */
|
|
22
22
|
readonly identityConfidence: IdentityConfidence;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
export interface UsbModeTransitionRequest {
|
|
24
|
+
type UsbModeTransitionRequestBase = {
|
|
26
25
|
readonly stableKey: string;
|
|
27
|
-
readonly sku: SkuDiscriminator;
|
|
28
|
-
readonly fromMode: MmUsbMode;
|
|
29
|
-
readonly toMode: MmUsbMode;
|
|
30
26
|
readonly connectionId: ConnectionId;
|
|
31
27
|
readonly deviceIfname: DeviceIfname;
|
|
32
28
|
/** Physical-topology UID captured BEFORE the switch — survives re-enumeration. */
|
|
@@ -40,7 +36,22 @@ export interface UsbModeTransitionRequest {
|
|
|
40
36
|
readonly now: EpochMillis;
|
|
41
37
|
/** Live readiness, re-polled at entry AND in-actor (TOCTOU-safe). */
|
|
42
38
|
probeReadiness(): Promise<TransitionReadiness>;
|
|
43
|
-
}
|
|
39
|
+
};
|
|
40
|
+
export type CatalogUsbModeTransitionRequest = UsbModeTransitionRequestBase & {
|
|
41
|
+
readonly strategy?: 'catalog';
|
|
42
|
+
readonly sku: SkuDiscriminator;
|
|
43
|
+
readonly fromMode: MmUsbMode;
|
|
44
|
+
readonly toMode: MmUsbMode;
|
|
45
|
+
};
|
|
46
|
+
export type RuntimeUsbModeTransitionRequest = UsbModeTransitionRequestBase & {
|
|
47
|
+
readonly strategy: 'runtime';
|
|
48
|
+
readonly vendor: RuntimeCompositionVendor;
|
|
49
|
+
readonly sku?: SkuDiscriminator;
|
|
50
|
+
readonly fromMode: RuntimeCompositionMode;
|
|
51
|
+
readonly toMode: RuntimeCompositionMode;
|
|
52
|
+
readonly capability: RuntimeCompositionCapability;
|
|
53
|
+
};
|
|
54
|
+
export type UsbModeTransitionRequest = CatalogUsbModeTransitionRequest | RuntimeUsbModeTransitionRequest;
|
|
44
55
|
/** How a transition ended. */
|
|
45
56
|
export type UsbModeTransitionOutcome = {
|
|
46
57
|
readonly status: 'refused';
|
|
@@ -57,11 +68,24 @@ export type UsbModeTransitionOutcome = {
|
|
|
57
68
|
readonly reason: string;
|
|
58
69
|
readonly steps: readonly string[];
|
|
59
70
|
};
|
|
60
|
-
|
|
71
|
+
export type UsbModeTransitionPlan = {
|
|
72
|
+
readonly atCommand: string;
|
|
73
|
+
readonly applyCommand?: string;
|
|
74
|
+
readonly proof: {
|
|
75
|
+
readonly tier: 'catalog-descriptors';
|
|
76
|
+
readonly transition: PermittedTransition;
|
|
77
|
+
} | {
|
|
78
|
+
readonly tier: 'runtime-requery';
|
|
79
|
+
readonly vendor: RuntimeCompositionVendor;
|
|
80
|
+
readonly target: RuntimeCompositionMode;
|
|
81
|
+
readonly currentQuery: string;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
61
84
|
export type PreconditionResult = {
|
|
62
85
|
readonly ok: true;
|
|
63
|
-
readonly entry
|
|
64
|
-
readonly
|
|
86
|
+
readonly entry?: CatalogEntry;
|
|
87
|
+
readonly plan: UsbModeTransitionPlan;
|
|
88
|
+
readonly allowlistedCommands: readonly string[];
|
|
65
89
|
} | {
|
|
66
90
|
readonly ok: false;
|
|
67
91
|
readonly reason: string;
|
|
@@ -74,3 +98,4 @@ export type PreconditionResult = {
|
|
|
74
98
|
* (identity, interlock), so a refusal touches as little as possible.
|
|
75
99
|
*/
|
|
76
100
|
export declare function checkTransitionPreconditions(request: UsbModeTransitionRequest, catalog: CertifiedCatalog, interlock: TransitionInterlock): Promise<PreconditionResult>;
|
|
101
|
+
export {};
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// transition→streaming gate (mark "transition active" until released). The Phase-A
|
|
13
13
|
// stub allows both; Phase B wires CeraUI's streaming-admission check into the same
|
|
14
14
|
// interface without changing the transaction.
|
|
15
|
-
import { findCatalogEntry, findPermittedTransition, } from '../usb-mode/index.js';
|
|
15
|
+
import { buildRuntimeCompositionSetCommand, findCatalogEntry, findPermittedTransition, RUNTIME_COMPOSITION_QUERY_REGISTRY, } from '../usb-mode/index.js';
|
|
16
16
|
const NO_OP_HOLD = {
|
|
17
17
|
release() {
|
|
18
18
|
return Promise.resolve();
|
|
@@ -27,6 +27,54 @@ export const ALLOW_ALL_TRANSITION_INTERLOCK = {
|
|
|
27
27
|
return Promise.resolve(NO_OP_HOLD);
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
|
+
function sameRuntimeMode(left, right) {
|
|
31
|
+
return Object.is(left, right);
|
|
32
|
+
}
|
|
33
|
+
function runtimePlan(request, catalog) {
|
|
34
|
+
const capability = request.capability;
|
|
35
|
+
if (capability.status !== 'available')
|
|
36
|
+
return { ok: false, reason: 'runtime capability unknown' };
|
|
37
|
+
if (!sameRuntimeMode(capability.current, request.fromMode))
|
|
38
|
+
return { ok: false, reason: 'runtime current mode changed' };
|
|
39
|
+
if (!capability.returnPathProven ||
|
|
40
|
+
!capability.offerable.some((mode) => sameRuntimeMode(mode, request.toMode)))
|
|
41
|
+
return { ok: false, reason: 'no-return-path' };
|
|
42
|
+
const atCommand = buildRuntimeCompositionSetCommand(request.vendor, request.toMode);
|
|
43
|
+
if (atCommand === undefined)
|
|
44
|
+
return { ok: false, reason: 'runtime target command unavailable' };
|
|
45
|
+
const entry = request.sku === undefined ? undefined : findCatalogEntry(catalog, request.sku);
|
|
46
|
+
if (entry !== undefined) {
|
|
47
|
+
const reviewed = entry.permittedTransitions.find((transition) => transition.atCommand === atCommand);
|
|
48
|
+
if (reviewed !== undefined) {
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
entry,
|
|
52
|
+
plan: {
|
|
53
|
+
atCommand: reviewed.atCommand,
|
|
54
|
+
...(reviewed.applyCommand === undefined ? {} : { applyCommand: reviewed.applyCommand }),
|
|
55
|
+
proof: { tier: 'catalog-descriptors', transition: reviewed },
|
|
56
|
+
},
|
|
57
|
+
allowlistedCommands: entry.permittedTransitions.flatMap((transition) => transition.applyCommand === undefined
|
|
58
|
+
? [transition.atCommand]
|
|
59
|
+
: [transition.atCommand, transition.applyCommand]),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
...(entry === undefined ? {} : { entry }),
|
|
66
|
+
plan: {
|
|
67
|
+
atCommand,
|
|
68
|
+
proof: {
|
|
69
|
+
tier: 'runtime-requery',
|
|
70
|
+
vendor: request.vendor,
|
|
71
|
+
target: request.toMode,
|
|
72
|
+
currentQuery: RUNTIME_COMPOSITION_QUERY_REGISTRY[request.vendor].current,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
allowlistedCommands: [atCommand],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
30
78
|
/**
|
|
31
79
|
* Check every transition precondition against the LIVE inputs. Called at entry and
|
|
32
80
|
* again in-actor; a request that passed at entry can fail here if the identity
|
|
@@ -41,17 +89,37 @@ export async function checkTransitionPreconditions(request, catalog, interlock)
|
|
|
41
89
|
if (!request.maintenance) {
|
|
42
90
|
return { ok: false, reason: 'maintenance flag is required' };
|
|
43
91
|
}
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
92
|
+
let staticResult;
|
|
93
|
+
if (request.strategy === 'runtime') {
|
|
94
|
+
staticResult = runtimePlan(request, catalog);
|
|
47
95
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ok: false,
|
|
52
|
-
|
|
96
|
+
else {
|
|
97
|
+
const entry = findCatalogEntry(catalog, request.sku);
|
|
98
|
+
if (entry === undefined) {
|
|
99
|
+
return { ok: false, reason: `uncertified SKU ${request.sku.vidPid} ${request.sku.model}` };
|
|
100
|
+
}
|
|
101
|
+
const transition = findPermittedTransition(entry, request.fromMode, request.toMode);
|
|
102
|
+
if (transition === undefined) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
reason: `transition ${request.fromMode}->${request.toMode} not permitted for ${request.sku.model}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
staticResult = {
|
|
109
|
+
ok: true,
|
|
110
|
+
entry,
|
|
111
|
+
plan: {
|
|
112
|
+
atCommand: transition.atCommand,
|
|
113
|
+
...(transition.applyCommand === undefined ? {} : { applyCommand: transition.applyCommand }),
|
|
114
|
+
proof: { tier: 'catalog-descriptors', transition },
|
|
115
|
+
},
|
|
116
|
+
allowlistedCommands: entry.permittedTransitions.flatMap((candidate) => candidate.applyCommand === undefined
|
|
117
|
+
? [candidate.atCommand]
|
|
118
|
+
: [candidate.atCommand, candidate.applyCommand]),
|
|
53
119
|
};
|
|
54
120
|
}
|
|
121
|
+
if (!staticResult.ok)
|
|
122
|
+
return staticResult;
|
|
55
123
|
const readiness = await request.probeReadiness();
|
|
56
124
|
if (readiness.identityConfidence === 'low') {
|
|
57
125
|
return { ok: false, reason: 'low-confidence identity — refusing to transition' };
|
|
@@ -60,5 +128,5 @@ export async function checkTransitionPreconditions(request, catalog, interlock)
|
|
|
60
128
|
if (!verdict.allow) {
|
|
61
129
|
return { ok: false, reason: `interlock held: ${verdict.reason}` };
|
|
62
130
|
}
|
|
63
|
-
return
|
|
131
|
+
return staticResult;
|
|
64
132
|
}
|
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
// 8 resolve new ifname → 9 reactivate (uuid, newIfname) → 10 release interlock
|
|
12
12
|
// (finally, always).
|
|
13
13
|
//
|
|
14
|
-
// THE POSTCONDITION IS THE ONLY PROOF OF SUCCESS. An AT `OK` proves nothing
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// THE POSTCONDITION IS THE ONLY PROOF OF SUCCESS. An AT `OK` proves nothing. Tier 1 is
|
|
15
|
+
// the strongest proof and remains unchanged: a reviewed catalog transition must match
|
|
16
|
+
// both descriptors and canonical mode. Tier 2 exists only when no reviewed transition
|
|
17
|
+
// matches: the re-enumerated device must report the raw target through its own vendor READ.
|
|
18
|
+
// Tier 2 is explicitly weaker because it proves reported mode, not descriptor composition.
|
|
19
|
+
// On a postcondition MISMATCH the whole transaction fails `degraded`, does NOT
|
|
17
20
|
// reactivate, and still releases the interlock via `finally`. A hung command trips the
|
|
18
21
|
// AT watchdog, which force-uninhibits so the system reprobes rather than wedging.
|
|
19
22
|
import { deviceIfname } from '../ports/index.js';
|
|
20
|
-
import { CERTIFIED_CATALOG } from '../usb-mode/index.js';
|
|
23
|
+
import { CERTIFIED_CATALOG, readRuntimeCompositionCurrent, } from '../usb-mode/index.js';
|
|
21
24
|
import { AtCommandLease, computeAtAllowlist, } from './at-lease.js';
|
|
22
25
|
import { descriptorsMatch, detectUsbMode } from './device-classifier.js';
|
|
23
26
|
import { ALLOW_ALL_TRANSITION_INTERLOCK, checkTransitionPreconditions, } from './transition-preconditions.js';
|
|
@@ -77,14 +80,14 @@ export class UsbModeTransition {
|
|
|
77
80
|
}
|
|
78
81
|
const hold = await this.#interlock.hold({ stableKey: request.stableKey });
|
|
79
82
|
try {
|
|
80
|
-
return await this.#runTransaction(request, recheck.
|
|
83
|
+
return await this.#runTransaction(request, recheck.allowlistedCommands, recheck.plan, steps);
|
|
81
84
|
}
|
|
82
85
|
finally {
|
|
83
86
|
steps.push('release-interlock');
|
|
84
87
|
await hold.release().catch(() => undefined);
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
|
-
async #runTransaction(request,
|
|
90
|
+
async #runTransaction(request, allowlistedCommands, plan, steps) {
|
|
88
91
|
let inhibit;
|
|
89
92
|
let reactivated = false;
|
|
90
93
|
const forceUninhibit = async () => {
|
|
@@ -98,7 +101,7 @@ export class UsbModeTransition {
|
|
|
98
101
|
};
|
|
99
102
|
const lease = new AtCommandLease({
|
|
100
103
|
sender: this.#atSender,
|
|
101
|
-
allowlist: computeAtAllowlist(
|
|
104
|
+
allowlist: computeAtAllowlist(allowlistedCommands),
|
|
102
105
|
timeoutMs: this.#watchdogMs,
|
|
103
106
|
onWatchdog: forceUninhibit,
|
|
104
107
|
...(this.#audit !== undefined ? { audit: this.#audit } : {}),
|
|
@@ -110,10 +113,10 @@ export class UsbModeTransition {
|
|
|
110
113
|
inhibit = await this.#modemManager.inhibit(request.inhibitUid);
|
|
111
114
|
// AT `OK` is IGNORED for success — only the postcondition below decides.
|
|
112
115
|
steps.push('at-command');
|
|
113
|
-
await lease.run(
|
|
114
|
-
if (
|
|
116
|
+
await lease.run(plan.atCommand, { inhibitUid: request.inhibitUid });
|
|
117
|
+
if (plan.applyCommand !== undefined) {
|
|
115
118
|
steps.push('apply-command');
|
|
116
|
-
await lease.run(
|
|
119
|
+
await lease.run(plan.applyCommand, { inhibitUid: request.inhibitUid });
|
|
117
120
|
}
|
|
118
121
|
steps.push('await-port-drop');
|
|
119
122
|
await this.#awaitPortDrop(request.cachedPhysicalUid);
|
|
@@ -126,15 +129,32 @@ export class UsbModeTransition {
|
|
|
126
129
|
steps.push('await-reenumeration');
|
|
127
130
|
const device = await this.#awaitReenumeration(request.cachedPhysicalUid);
|
|
128
131
|
steps.push('postcondition');
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
if (plan.proof.tier === 'catalog-descriptors') {
|
|
133
|
+
const observedMode = detectUsbMode(device);
|
|
134
|
+
const descriptorsOk = descriptorsMatch(device, plan.proof.transition.expectedDescriptors);
|
|
135
|
+
if (observedMode !== plan.proof.transition.to || !descriptorsOk) {
|
|
136
|
+
return {
|
|
137
|
+
status: 'failed',
|
|
138
|
+
degraded: true,
|
|
139
|
+
reason: `postcondition mismatch: observed ${observedMode ?? 'unknown'} vs target ${plan.proof.transition.to}; descriptors ${descriptorsOk ? 'ok' : 'mismatch'}`,
|
|
140
|
+
steps,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
steps.push('postcondition-runtime-read');
|
|
146
|
+
const response = await lease.run(plan.proof.currentQuery, {
|
|
147
|
+
inhibitUid: request.inhibitUid,
|
|
148
|
+
});
|
|
149
|
+
const observed = readRuntimeCompositionCurrent(plan.proof.vendor, response.raw);
|
|
150
|
+
if (!Object.is(observed, plan.proof.target)) {
|
|
151
|
+
return {
|
|
152
|
+
status: 'failed',
|
|
153
|
+
degraded: true,
|
|
154
|
+
reason: `runtime readback mismatch: observed ${observed ?? 'unknown'} vs target ${plan.proof.target}`,
|
|
155
|
+
steps,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
138
158
|
}
|
|
139
159
|
steps.push('resolve-ifname');
|
|
140
160
|
const newIfname = this.#resolveIfname(device);
|
|
@@ -13,7 +13,7 @@ import type { CapabilityEvidence } from './support-claim.js';
|
|
|
13
13
|
* selector structurally cannot express.
|
|
14
14
|
* 5g-off — allow everything the modem supports EXCEPT 5G.
|
|
15
15
|
*/
|
|
16
|
-
export declare const FIVE_G_PREFERENCES: readonly [
|
|
16
|
+
export declare const FIVE_G_PREFERENCES: readonly ['5g-only', 'prefer-5g', 'prefer-4g', '5g-off'];
|
|
17
17
|
export type FiveGPreference = (typeof FIVE_G_PREFERENCES)[number];
|
|
18
18
|
/** One `(allowed, preferred)` pair, the shape MM's `CurrentModes` really carries. */
|
|
19
19
|
export interface RadioModeSet {
|
|
@@ -44,7 +44,7 @@ export interface FiveGRadioTarget {
|
|
|
44
44
|
* reads as "nobody asked"; a stated `not-exposed-by-modemmanager` tells an
|
|
45
45
|
* operator looking for an SA toggle why there is none.
|
|
46
46
|
*/
|
|
47
|
-
export declare const NR_MODE_UNSUPPORTED_REASON:
|
|
47
|
+
export declare const NR_MODE_UNSUPPORTED_REASON: 'not-exposed-by-modemmanager';
|
|
48
48
|
export interface NrModeSelection {
|
|
49
49
|
readonly supported: false;
|
|
50
50
|
readonly reason: typeof NR_MODE_UNSUPPORTED_REASON;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The seven gated capability modules. */
|
|
2
|
-
export declare const CAPABILITY_MODULES: readonly [
|
|
2
|
+
export declare const CAPABILITY_MODULES: readonly ['band-lock', 'sms', 'five-g-pref', 'fcc-auto-unlock', 'gps', 'ussd', 'esim'];
|
|
3
3
|
export type CapabilityModule = (typeof CAPABILITY_MODULES)[number];
|
|
4
4
|
/**
|
|
5
5
|
* The ladder, lowest rung first. `resolveSupportClaim` answers with the highest
|
|
@@ -12,7 +12,7 @@ export type CapabilityModule = (typeof CAPABILITY_MODULES)[number];
|
|
|
12
12
|
* certified — capable AND proven on this exact model+firmware. The ONLY rung
|
|
13
13
|
* a support matrix or a doc may claim.
|
|
14
14
|
*/
|
|
15
|
-
export declare const SUPPORT_CLAIM_STATES: readonly [
|
|
15
|
+
export declare const SUPPORT_CLAIM_STATES: readonly ['unavailable', 'implemented', 'enabled', 'capable', 'certified'];
|
|
16
16
|
export type SupportClaimState = (typeof SUPPORT_CLAIM_STATES)[number];
|
|
17
17
|
/** What a probe found. `unknown` is an answer about the READ, not the device. */
|
|
18
18
|
export type CapabilityEvidence = 'present' | 'absent' | 'unknown';
|
|
@@ -9,7 +9,7 @@ export interface ShadowModemState {
|
|
|
9
9
|
readonly simPresent?: boolean;
|
|
10
10
|
readonly networkType?: ShadowGeneration;
|
|
11
11
|
}
|
|
12
|
-
export declare const SHADOW_COMPARABLE_FIELDS: readonly [
|
|
12
|
+
export declare const SHADOW_COMPARABLE_FIELDS: readonly ['present', 'registration', 'signalBucket', 'operatorName', 'simPresent', 'networkType'];
|
|
13
13
|
export type ShadowComparableField = (typeof SHADOW_COMPARABLE_FIELDS)[number];
|
|
14
14
|
export interface ShadowFieldDivergence {
|
|
15
15
|
readonly field: ShadowComparableField;
|
package/dist/fcc/coverage.d.ts
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
/** The four vendor scripts ModemManager 1.24.2 ships, and what each one drives. */
|
|
2
2
|
export declare const MM_FCC_UNLOCK_VENDOR_SCRIPTS: {
|
|
3
|
-
readonly '105b':
|
|
4
|
-
readonly '1199':
|
|
5
|
-
readonly '14c3':
|
|
6
|
-
readonly '2c7c':
|
|
3
|
+
readonly '105b': 'mbimcli';
|
|
4
|
+
readonly '1199': 'qmicli';
|
|
5
|
+
readonly '14c3': 'mbimcli';
|
|
6
|
+
readonly '2c7c': 'qmicli';
|
|
7
7
|
};
|
|
8
8
|
export type MmFccUnlockVendorScript = keyof typeof MM_FCC_UNLOCK_VENDOR_SCRIPTS;
|
|
9
9
|
/** The interpreters those scripts invoke, and the packages that provide them. */
|
|
10
10
|
export declare const MM_FCC_UNLOCK_RUNTIME_PACKAGES: {
|
|
11
|
-
readonly qmicli:
|
|
12
|
-
readonly mbimcli:
|
|
11
|
+
readonly qmicli: 'libqmi-utils';
|
|
12
|
+
readonly mbimcli: 'libmbim-utils';
|
|
13
13
|
};
|
|
14
14
|
/**
|
|
15
15
|
* ModemManager 1.24.2's COMPLETE shipped mapping — `data/dispatcher-fcc-unlock/
|
|
16
16
|
* meson.build`'s `vidpids` dict, verbatim. Fourteen entries; there are no others.
|
|
17
17
|
*/
|
|
18
18
|
export declare const MM_FCC_UNLOCK_COVERAGE: {
|
|
19
|
-
readonly '03f0:4e1d':
|
|
20
|
-
readonly '105b:e0ab':
|
|
21
|
-
readonly '105b:e0c3':
|
|
22
|
-
readonly '1199:9079':
|
|
23
|
-
readonly '14c3:4d75':
|
|
24
|
-
readonly '1eac:1001':
|
|
25
|
-
readonly '1eac:1004':
|
|
26
|
-
readonly '1eac:1007':
|
|
27
|
-
readonly '2c7c:030a':
|
|
28
|
-
readonly '2c7c:0313':
|
|
29
|
-
readonly '2c7c:0314':
|
|
30
|
-
readonly '2c7c:0801':
|
|
31
|
-
readonly '413c:81a3':
|
|
32
|
-
readonly '413c:81a8':
|
|
19
|
+
readonly '03f0:4e1d': '1199';
|
|
20
|
+
readonly '105b:e0ab': '105b';
|
|
21
|
+
readonly '105b:e0c3': '105b';
|
|
22
|
+
readonly '1199:9079': '1199';
|
|
23
|
+
readonly '14c3:4d75': '14c3';
|
|
24
|
+
readonly '1eac:1001': '2c7c';
|
|
25
|
+
readonly '1eac:1004': '2c7c';
|
|
26
|
+
readonly '1eac:1007': '2c7c';
|
|
27
|
+
readonly '2c7c:030a': '2c7c';
|
|
28
|
+
readonly '2c7c:0313': '2c7c';
|
|
29
|
+
readonly '2c7c:0314': '2c7c';
|
|
30
|
+
readonly '2c7c:0801': '2c7c';
|
|
31
|
+
readonly '413c:81a3': '1199';
|
|
32
|
+
readonly '413c:81a8': '1199';
|
|
33
33
|
};
|
|
34
34
|
export type MmFccUnlockKey = keyof typeof MM_FCC_UNLOCK_COVERAGE;
|
|
35
35
|
/**
|
|
@@ -8,7 +8,7 @@ export type SimPresenceFacts = {
|
|
|
8
8
|
/** The mmcli spelling of `MM_MODEM_STATE_FAILED_REASON_SIM_MISSING` — the ONLY proof of absence. */
|
|
9
9
|
export declare const SIM_MISSING_FAILED_REASON = "sim-missing";
|
|
10
10
|
/** The fields a presence decision may be drawn from, in the order they are inspected. */
|
|
11
|
-
export declare const SIM_PRESENCE_FIELDS: readonly [
|
|
11
|
+
export declare const SIM_PRESENCE_FIELDS: readonly ['sim', 'simSlots', 'failedReason'];
|
|
12
12
|
export type SimPresenceField = (typeof SIM_PRESENCE_FIELDS)[number];
|
|
13
13
|
/**
|
|
14
14
|
* WHICH FACT decided a SIM presence — the whole point of this type.
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './hardware/router-parsers.js';
|
|
|
8
8
|
export * from './journal/index.js';
|
|
9
9
|
export * from './location/index.js';
|
|
10
10
|
export * from './observations/index.js';
|
|
11
|
+
export * from './operation-ids.js';
|
|
11
12
|
export * from './operations/index.js';
|
|
12
13
|
export * from './ports/index.js';
|
|
13
14
|
export * from './providers/index.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export * from './hardware/router-parsers.js';
|
|
|
15
15
|
export * from './journal/index.js';
|
|
16
16
|
export * from './location/index.js';
|
|
17
17
|
export * from './observations/index.js';
|
|
18
|
+
export * from './operation-ids.js';
|
|
18
19
|
export * from './operations/index.js';
|
|
19
20
|
export * from './ports/index.js';
|
|
20
21
|
export * from './providers/index.js';
|
|
@@ -5,9 +5,9 @@ import { type JournalOperationRecord, type JournalRecovery } from './recovery.js
|
|
|
5
5
|
export declare const LEGACY_CERAUI_JOURNAL_VERSION = 1;
|
|
6
6
|
/** CeraUI's cap on retained history entries per slot. */
|
|
7
7
|
export declare const LEGACY_CERAUI_HISTORY_CAP = 32;
|
|
8
|
-
export declare const LEGACY_CERAUI_MUTATION_STATES: readonly [
|
|
8
|
+
export declare const LEGACY_CERAUI_MUTATION_STATES: readonly ['armed', 'executing', 'completed', 'failed', 'acknowledged', 'device-absent-quarantine', 'decommissioned', 'recommission-pending'];
|
|
9
9
|
export type LegacyCeraUiMutationState = (typeof LEGACY_CERAUI_MUTATION_STATES)[number];
|
|
10
|
-
export declare const LEGACY_CERAUI_ACK_MODES: readonly [
|
|
10
|
+
export declare const LEGACY_CERAUI_ACK_MODES: readonly ['verified-rollback', 'force-rebaseline'];
|
|
11
11
|
export type LegacyCeraUiAckMode = (typeof LEGACY_CERAUI_ACK_MODES)[number];
|
|
12
12
|
export interface LegacyCeraUiHistoryEntry {
|
|
13
13
|
readonly state: LegacyCeraUiMutationState;
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import type { RouterSignalMetric, RouterSignalUnknownReason } from '../hardware/router-parsers.js';
|
|
2
2
|
import type { MetricProvenance } from './provenance.js';
|
|
3
|
-
export declare const METRIC_UNKNOWN_REASONS: readonly [
|
|
3
|
+
export declare const METRIC_UNKNOWN_REASONS: readonly [
|
|
4
|
+
/** A positive claim about the SOURCE: it cannot express this datum at all. */
|
|
5
|
+
'unsupported',
|
|
6
|
+
/** The source answered and simply did not include this field. */
|
|
7
|
+
'not-reported',
|
|
8
|
+
/** Nobody has read this yet. Says nothing about the source or the device. */
|
|
9
|
+
'not-observed',
|
|
10
|
+
/** The source answered with something this layer could not decode. */
|
|
11
|
+
'malformed',
|
|
12
|
+
/** The source refused the read; the session needs re-authentication. */
|
|
13
|
+
'auth-expired',
|
|
14
|
+
/** The source answered with an explicit refusal code for this field. */
|
|
15
|
+
'refused',
|
|
16
|
+
/** The source could not be reached for this read. */
|
|
17
|
+
'unreachable'];
|
|
4
18
|
export type MetricUnknownReason = (typeof METRIC_UNKNOWN_REASONS)[number];
|
|
5
19
|
/**
|
|
6
20
|
* The two classes an unknown reason falls into.
|
|
@@ -11,7 +11,7 @@ import type { EpochMillis, ObservationAuthority, SourceEpoch } from '../domain/i
|
|
|
11
11
|
* reading. Giving it its own kind is what keeps a bearer observation attributable to
|
|
12
12
|
* NM rather than laundered through whichever provider happened to read the modem.
|
|
13
13
|
*/
|
|
14
|
-
export declare const OBSERVATION_SOURCE_KINDS: readonly [
|
|
14
|
+
export declare const OBSERVATION_SOURCE_KINDS: readonly ['modemmanager', 'huawei-hilink', 'zte-goform', 'ufi-himiapi', 'networkmanager'];
|
|
15
15
|
export type ObservationSourceKind = (typeof OBSERVATION_SOURCE_KINDS)[number];
|
|
16
16
|
/**
|
|
17
17
|
* A provider-native value, kept exactly as the provider expressed it.
|
|
@@ -45,7 +45,7 @@ export type MetricProvenance = {
|
|
|
45
45
|
* and never carries its value: a body can hold an ICCID or a one-time code, and a
|
|
46
46
|
* diagnostic is the one place a reviewer reads verbatim.
|
|
47
47
|
*/
|
|
48
|
-
export declare const OBSERVATION_DIAGNOSTIC_CODES: readonly [
|
|
48
|
+
export declare const OBSERVATION_DIAGNOSTIC_CODES: readonly ['unparseable-body', 'empty-body', 'auth-expired', 'field-shape-unrecognized'];
|
|
49
49
|
export type ObservationDiagnosticCode = (typeof OBSERVATION_DIAGNOSTIC_CODES)[number];
|
|
50
50
|
export type ObservationDiagnosticNote = {
|
|
51
51
|
readonly code: ObservationDiagnosticCode;
|
|
@@ -34,7 +34,7 @@ export type ObservationReading<T> = (ReadingBase & {
|
|
|
34
34
|
readonly reason: MetricUnknownReason;
|
|
35
35
|
readonly provenance: MetricProvenance;
|
|
36
36
|
});
|
|
37
|
-
export declare const OBSERVATION_READING_STATES: readonly [
|
|
37
|
+
export declare const OBSERVATION_READING_STATES: readonly ['fresh', 'stale', 'unavailable', 'unknown'];
|
|
38
38
|
export type ObservationReadingState = (typeof OBSERVATION_READING_STATES)[number];
|
|
39
39
|
/**
|
|
40
40
|
* Project one metric out of an envelope.
|
|
@@ -38,7 +38,7 @@ export type ModemStateView<TDesired, TApplied, TObserved> = {
|
|
|
38
38
|
readonly applied: AppliedConfiguration<TApplied> | null;
|
|
39
39
|
readonly observed: ObservedState<TObserved>;
|
|
40
40
|
};
|
|
41
|
-
export declare const STATE_VIEW_SLOTS: readonly [
|
|
41
|
+
export declare const STATE_VIEW_SLOTS: readonly ['desired', 'applied', 'observed'];
|
|
42
42
|
export type StateViewSlot = (typeof STATE_VIEW_SLOTS)[number];
|
|
43
43
|
export type SlotComparison = {
|
|
44
44
|
readonly status: 'aligned';
|