@tetsuo-ai/agenc 0.17.0 → 0.19.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/generated/agenc-runtime-manifest-v2.json +104 -102
- package/generated/startup-preflight.mjs +1 -0
- package/lib/generated-wrapper.mjs +49 -12
- package/lib/home-authority.d.mts +26 -0
- package/lib/home-authority.mjs +64 -0
- package/lib/runtime-manager.mjs +3 -13
- package/lib/sqlite-lock.d.mts +4 -0
- package/lib/sqlite-lock.mjs +164 -7
- package/package.json +6 -3
- package/scripts/postinstall.mjs +23 -2
- package/src/launcher.mjs +163 -38
package/lib/sqlite-lock.mjs
CHANGED
|
@@ -73,6 +73,8 @@ const DARWIN_ACL_KNOWN_TOKENS = new Set([
|
|
|
73
73
|
...DARWIN_ACL_INHERITANCE_FLAGS,
|
|
74
74
|
...DARWIN_ACL_MUTATION_RIGHTS,
|
|
75
75
|
]);
|
|
76
|
+
const DARWIN_ACL_VERDICT_CACHE_LIMIT = 512;
|
|
77
|
+
const darwinAclVerdicts = new Map();
|
|
76
78
|
|
|
77
79
|
const WINDOWS_SECURITY_SCRIPT = String.raw`
|
|
78
80
|
$ErrorActionPreference = 'Stop'
|
|
@@ -225,6 +227,17 @@ function throwIfExpired(context, path, cause) {
|
|
|
225
227
|
}
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
function reportProgress(context, phase) {
|
|
231
|
+
try {
|
|
232
|
+
const result = context.onProgress?.(phase);
|
|
233
|
+
if (result !== undefined) {
|
|
234
|
+
void Promise.resolve(result).catch(() => {});
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Diagnostics must never change lock acquisition or release semantics.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
228
241
|
function processLockRegistry() {
|
|
229
242
|
const current = process[REGISTRY_SYMBOL];
|
|
230
243
|
if (current !== undefined) {
|
|
@@ -410,10 +423,45 @@ function validateDarwinAclListing(stdout, path, role) {
|
|
|
410
423
|
}
|
|
411
424
|
|
|
412
425
|
async function assertDarwinPathSecurity(path, role, context) {
|
|
426
|
+
return assertDarwinPathSecurityWithIO(
|
|
427
|
+
path,
|
|
428
|
+
role,
|
|
429
|
+
context,
|
|
430
|
+
execFileUtf8,
|
|
431
|
+
(candidate) => lstat(candidate, { bigint: true }),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// HFS+ reports change time in whole seconds, so an ACL edit within the same
|
|
436
|
+
// second can leave the identity unchanged. Only sub-second change times are
|
|
437
|
+
// precise enough to cache a verdict.
|
|
438
|
+
function hasSubsecondChangeTime(stats) {
|
|
439
|
+
if (typeof stats.ctimeNs === "bigint") return stats.ctimeNs % 1_000_000_000n !== 0n;
|
|
440
|
+
return Number.isFinite(stats.ctimeMs) && stats.ctimeMs % 1000 !== 0;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function darwinAclIdentity(stats) {
|
|
444
|
+
return [
|
|
445
|
+
stats.dev, stats.ino, stats.ctimeNs ?? stats.ctimeMs,
|
|
446
|
+
stats.mode, stats.uid, stats.gid,
|
|
447
|
+
process.getuid?.(), process.geteuid?.(),
|
|
448
|
+
].join(":");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function assertDarwinPathSecurityWithIO(path, role, context, lister, statPath) {
|
|
413
452
|
throwIfExpired(context, path);
|
|
453
|
+
// A Darwin ACL edit advances ctime. Re-stat on every lookup so an old
|
|
454
|
+
// successful verdict cannot authorize a changed inode, owner, or ACL.
|
|
455
|
+
const before = darwinAclIdentity(await statPath(path));
|
|
456
|
+
const cacheKey = `${role}\0${path}`;
|
|
457
|
+
if (darwinAclVerdicts.get(cacheKey) === before) {
|
|
458
|
+
throwIfExpired(context, path);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
darwinAclVerdicts.delete(cacheKey);
|
|
414
462
|
let result;
|
|
415
463
|
try {
|
|
416
|
-
result = await
|
|
464
|
+
result = await lister(
|
|
417
465
|
"/bin/ls",
|
|
418
466
|
["-ldeq", path],
|
|
419
467
|
{
|
|
@@ -431,6 +479,16 @@ async function assertDarwinPathSecurity(path, role, context) {
|
|
|
431
479
|
}
|
|
432
480
|
validateDarwinAclListing(result.stdout, path, role);
|
|
433
481
|
throwIfExpired(context, path);
|
|
482
|
+
// A path changed while ls ran must never seed a verdict for its new state.
|
|
483
|
+
const afterStats = await statPath(path);
|
|
484
|
+
const after = darwinAclIdentity(afterStats);
|
|
485
|
+
throwIfExpired(context, path);
|
|
486
|
+
if (after === before && hasSubsecondChangeTime(afterStats)) {
|
|
487
|
+
if (darwinAclVerdicts.size >= DARWIN_ACL_VERDICT_CACHE_LIMIT) {
|
|
488
|
+
darwinAclVerdicts.delete(darwinAclVerdicts.keys().next().value);
|
|
489
|
+
}
|
|
490
|
+
darwinAclVerdicts.set(cacheKey, before);
|
|
491
|
+
}
|
|
434
492
|
}
|
|
435
493
|
|
|
436
494
|
function trustedWindowsPowerShellPath(
|
|
@@ -706,6 +764,7 @@ export async function assertLocalPrivateDirectory(
|
|
|
706
764
|
label = "AgenC operation",
|
|
707
765
|
deadline: suppliedDeadline,
|
|
708
766
|
allowTrustedStickyLeaf = false,
|
|
767
|
+
onProgress,
|
|
709
768
|
} = {},
|
|
710
769
|
) {
|
|
711
770
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
@@ -717,6 +776,9 @@ export async function assertLocalPrivateDirectory(
|
|
|
717
776
|
if (typeof allowTrustedStickyLeaf !== "boolean") {
|
|
718
777
|
throw new TypeError("allowTrustedStickyLeaf must be boolean");
|
|
719
778
|
}
|
|
779
|
+
if (onProgress !== undefined && typeof onProgress !== "function") {
|
|
780
|
+
throw new TypeError("lock onProgress must be a function");
|
|
781
|
+
}
|
|
720
782
|
const context = {
|
|
721
783
|
deadline: Math.min(
|
|
722
784
|
suppliedDeadline ?? Number.POSITIVE_INFINITY,
|
|
@@ -724,7 +786,9 @@ export async function assertLocalPrivateDirectory(
|
|
|
724
786
|
),
|
|
725
787
|
label,
|
|
726
788
|
timeoutMs,
|
|
789
|
+
onProgress,
|
|
727
790
|
};
|
|
791
|
+
reportProgress(context, "private directory validation started");
|
|
728
792
|
const absolute = resolve(requestedPath);
|
|
729
793
|
throwIfExpired(context, absolute);
|
|
730
794
|
const canonical = await realpath(absolute);
|
|
@@ -764,6 +828,7 @@ export async function assertLocalPrivateDirectory(
|
|
|
764
828
|
identityFromStats(stats, path);
|
|
765
829
|
}
|
|
766
830
|
if (process.platform === "win32") {
|
|
831
|
+
reportProgress(context, "Windows ancestor ACL validation started");
|
|
767
832
|
await assertWindowsPathSecurity(
|
|
768
833
|
ancestors.map((path, index) => ({
|
|
769
834
|
path,
|
|
@@ -771,6 +836,7 @@ export async function assertLocalPrivateDirectory(
|
|
|
771
836
|
})),
|
|
772
837
|
context,
|
|
773
838
|
);
|
|
839
|
+
reportProgress(context, "Windows ancestor ACL validation complete");
|
|
774
840
|
} else {
|
|
775
841
|
await assertLocalFilesystem(canonical, context);
|
|
776
842
|
if (process.platform === "darwin") {
|
|
@@ -794,6 +860,7 @@ export async function assertLocalPrivateDirectory(
|
|
|
794
860
|
throw new Error(`agenc: protected directory identity changed during validation: ${path}`);
|
|
795
861
|
}
|
|
796
862
|
}
|
|
863
|
+
reportProgress(context, "private directory validation complete");
|
|
797
864
|
return canonical;
|
|
798
865
|
}
|
|
799
866
|
|
|
@@ -840,6 +907,7 @@ export async function assertLocalPrivateFile(
|
|
|
840
907
|
timeoutMs = 60_000,
|
|
841
908
|
label = "AgenC operation",
|
|
842
909
|
deadline: suppliedDeadline,
|
|
910
|
+
onProgress,
|
|
843
911
|
} = {},
|
|
844
912
|
) {
|
|
845
913
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
@@ -848,6 +916,9 @@ export async function assertLocalPrivateFile(
|
|
|
848
916
|
if (suppliedDeadline !== undefined && !Number.isFinite(suppliedDeadline)) {
|
|
849
917
|
throw new TypeError("lock deadline must be finite");
|
|
850
918
|
}
|
|
919
|
+
if (onProgress !== undefined && typeof onProgress !== "function") {
|
|
920
|
+
throw new TypeError("lock onProgress must be a function");
|
|
921
|
+
}
|
|
851
922
|
const context = {
|
|
852
923
|
deadline: Math.min(
|
|
853
924
|
suppliedDeadline ?? Number.POSITIVE_INFINITY,
|
|
@@ -855,13 +926,16 @@ export async function assertLocalPrivateFile(
|
|
|
855
926
|
),
|
|
856
927
|
label,
|
|
857
928
|
timeoutMs,
|
|
929
|
+
onProgress,
|
|
858
930
|
};
|
|
931
|
+
reportProgress(context, "private file validation started");
|
|
859
932
|
const absolute = resolve(requestedPath);
|
|
860
933
|
const parent = dirname(absolute);
|
|
861
934
|
const canonicalParent = await assertLocalPrivateDirectory(parent, {
|
|
862
935
|
timeoutMs,
|
|
863
936
|
label,
|
|
864
937
|
deadline: context.deadline,
|
|
938
|
+
...(onProgress === undefined ? {} : { onProgress }),
|
|
865
939
|
});
|
|
866
940
|
if (canonicalParent !== parent) {
|
|
867
941
|
throw new Error(`agenc: protected file parent must use its canonical path: ${parent}`);
|
|
@@ -886,7 +960,9 @@ export async function assertLocalPrivateFile(
|
|
|
886
960
|
throw new Error(`agenc: protected file must use its canonical path: ${absolute}`);
|
|
887
961
|
}
|
|
888
962
|
if (process.platform === "win32") {
|
|
963
|
+
reportProgress(context, "Windows file ACL validation started");
|
|
889
964
|
await assertWindowsPathSecurity([{ path: canonical, role: "file" }], context);
|
|
965
|
+
reportProgress(context, "Windows file ACL validation complete");
|
|
890
966
|
} else if (process.platform === "darwin") {
|
|
891
967
|
await assertDarwinPathSecurity(canonical, "file", context);
|
|
892
968
|
}
|
|
@@ -898,21 +974,30 @@ export async function assertLocalPrivateFile(
|
|
|
898
974
|
) {
|
|
899
975
|
throw new Error(`agenc: protected file identity changed during validation: ${canonical}`);
|
|
900
976
|
}
|
|
977
|
+
reportProgress(context, "private file validation complete");
|
|
901
978
|
return canonical;
|
|
902
979
|
}
|
|
903
980
|
|
|
904
981
|
async function prepareLockPath(requestedPath, context) {
|
|
905
982
|
const absolute = resolve(requestedPath);
|
|
983
|
+
reportProgress(context, "lock path preparation started");
|
|
906
984
|
throwIfExpired(context, absolute);
|
|
907
985
|
await mkdir(dirname(absolute), { recursive: true, mode: 0o700 });
|
|
986
|
+
reportProgress(context, "lock parent creation complete");
|
|
908
987
|
throwIfExpired(context, absolute);
|
|
909
988
|
const parent = await realpath(dirname(absolute));
|
|
989
|
+
reportProgress(context, "lock parent canonicalization complete");
|
|
910
990
|
throwIfExpired(context, absolute);
|
|
991
|
+
reportProgress(context, "lock parent security validation started");
|
|
911
992
|
const validatedParent = await assertLocalPrivateDirectory(parent, {
|
|
912
993
|
timeoutMs: context.timeoutMs,
|
|
913
994
|
label: context.label,
|
|
914
995
|
deadline: context.deadline,
|
|
996
|
+
...(context.onProgress === undefined
|
|
997
|
+
? {}
|
|
998
|
+
: { onProgress: context.onProgress }),
|
|
915
999
|
});
|
|
1000
|
+
reportProgress(context, "lock parent security validation complete");
|
|
916
1001
|
if (validatedParent !== parent) {
|
|
917
1002
|
throw new Error(`agenc: lock database parent must use its canonical path: ${parent}`);
|
|
918
1003
|
}
|
|
@@ -935,6 +1020,7 @@ async function prepareLockPath(requestedPath, context) {
|
|
|
935
1020
|
} catch (error) {
|
|
936
1021
|
if (error?.code !== "EEXIST") throw error;
|
|
937
1022
|
}
|
|
1023
|
+
reportProgress(context, "lock database file creation complete");
|
|
938
1024
|
throwIfExpired(context, path);
|
|
939
1025
|
const pathStats = await lstat(path, { bigint: true });
|
|
940
1026
|
assertRegularSingleLink(pathStats, path);
|
|
@@ -949,10 +1035,12 @@ async function prepareLockPath(requestedPath, context) {
|
|
|
949
1035
|
await assertDarwinPathSecurity(canonical, "lock database file", context);
|
|
950
1036
|
}
|
|
951
1037
|
} else {
|
|
1038
|
+
reportProgress(context, "lock database ACL validation started");
|
|
952
1039
|
await assertWindowsPathSecurity([
|
|
953
1040
|
{ path: parent, role: "leafDirectory" },
|
|
954
1041
|
{ path: canonical, role: "file" },
|
|
955
1042
|
], context);
|
|
1043
|
+
reportProgress(context, "lock database ACL validation complete");
|
|
956
1044
|
}
|
|
957
1045
|
throwIfExpired(context, canonical);
|
|
958
1046
|
const securedStats = await lstat(canonical, { bigint: true });
|
|
@@ -967,7 +1055,11 @@ async function prepareLockPath(requestedPath, context) {
|
|
|
967
1055
|
};
|
|
968
1056
|
}
|
|
969
1057
|
|
|
970
|
-
async function revalidatePreparedLock(
|
|
1058
|
+
async function revalidatePreparedLock(
|
|
1059
|
+
prepared,
|
|
1060
|
+
context,
|
|
1061
|
+
{ validateWindowsAcl = false } = {},
|
|
1062
|
+
) {
|
|
971
1063
|
throwIfExpired(context, prepared.path);
|
|
972
1064
|
const parentStats = await lstat(prepared.parent, { bigint: true });
|
|
973
1065
|
if (!parentStats.isDirectory() || parentStats.isSymbolicLink()) {
|
|
@@ -980,7 +1072,7 @@ async function revalidatePreparedLock(prepared, context) {
|
|
|
980
1072
|
if (stats.dev !== prepared.dev || stats.ino !== prepared.ino) {
|
|
981
1073
|
throw new Error(`agenc: lock database identity changed during acquisition: ${prepared.path}`);
|
|
982
1074
|
}
|
|
983
|
-
if (process.platform === "win32") {
|
|
1075
|
+
if (process.platform === "win32" && validateWindowsAcl) {
|
|
984
1076
|
await assertWindowsPathSecurity([
|
|
985
1077
|
{ path: prepared.parent, role: "leafDirectory" },
|
|
986
1078
|
{ path: prepared.path, role: "file" },
|
|
@@ -1076,18 +1168,27 @@ function busyTransitionError(path, mode) {
|
|
|
1076
1168
|
);
|
|
1077
1169
|
}
|
|
1078
1170
|
|
|
1079
|
-
function beginAndValidateLock(database, path) {
|
|
1171
|
+
function beginAndValidateLock(database, path, context) {
|
|
1080
1172
|
for (let phase = 0; phase < 8; phase += 1) {
|
|
1173
|
+
reportProgress(context, "SQLite transaction begin started");
|
|
1081
1174
|
database.exec("BEGIN IMMEDIATE");
|
|
1175
|
+
reportProgress(context, "SQLite transaction begin complete");
|
|
1176
|
+
reportProgress(context, "SQLite lock database inspection started");
|
|
1082
1177
|
const state = inspectLockDatabase(database, path);
|
|
1178
|
+
reportProgress(context, "SQLite lock database inspection complete");
|
|
1179
|
+
reportProgress(context, "SQLite journal mode inspection started");
|
|
1083
1180
|
const journalMode = pragmaText(database, "journal_mode");
|
|
1181
|
+
reportProgress(context, "SQLite journal mode inspection complete");
|
|
1084
1182
|
if (journalMode !== "delete") {
|
|
1183
|
+
reportProgress(context, "SQLite journal mode reset started");
|
|
1085
1184
|
database.exec("ROLLBACK");
|
|
1086
1185
|
const selected = pragmaText(database, "journal_mode=DELETE");
|
|
1087
1186
|
if (selected !== "delete") throw busyTransitionError(path, selected);
|
|
1187
|
+
reportProgress(context, "SQLite journal mode reset complete");
|
|
1088
1188
|
continue;
|
|
1089
1189
|
}
|
|
1090
1190
|
if (state === "empty") {
|
|
1191
|
+
reportProgress(context, "SQLite lock database initialization started");
|
|
1091
1192
|
database.exec(`
|
|
1092
1193
|
PRAGMA application_id = ${LOCK_APPLICATION_ID};
|
|
1093
1194
|
CREATE TABLE agenc_local_process_lock (
|
|
@@ -1098,6 +1199,7 @@ function beginAndValidateLock(database, path) {
|
|
|
1098
1199
|
VALUES (1, ${LOCK_FORMAT_VERSION});
|
|
1099
1200
|
COMMIT;
|
|
1100
1201
|
`);
|
|
1202
|
+
reportProgress(context, "SQLite lock database initialization complete");
|
|
1101
1203
|
continue;
|
|
1102
1204
|
}
|
|
1103
1205
|
return;
|
|
@@ -1134,7 +1236,32 @@ async function waitForBusyRetry(context, path, attempt, cause) {
|
|
|
1134
1236
|
if (remaining <= 0) throw timeoutError(context, path, cause);
|
|
1135
1237
|
const exponentialCap = Math.min(MAX_BUSY_RETRY_MS, 2 ** Math.min(attempt, 6));
|
|
1136
1238
|
const jitter = Math.max(1, Math.floor(Math.random() * (exponentialCap + 1)));
|
|
1137
|
-
|
|
1239
|
+
const waitMs = Math.min(remaining, jitter);
|
|
1240
|
+
const signal = context.retryWakeSignal;
|
|
1241
|
+
if (signal === undefined || context.retryWakeConsumed) {
|
|
1242
|
+
await delay(waitMs);
|
|
1243
|
+
} else {
|
|
1244
|
+
// A hint only shortens one sleep. The next attempt must still validate
|
|
1245
|
+
// the path and acquire the SQLite transaction against the same deadline.
|
|
1246
|
+
let timer;
|
|
1247
|
+
let wake;
|
|
1248
|
+
try {
|
|
1249
|
+
await new Promise((resolve) => {
|
|
1250
|
+
wake = () => {
|
|
1251
|
+
context.retryWakeConsumed = true;
|
|
1252
|
+
resolve();
|
|
1253
|
+
};
|
|
1254
|
+
timer = setTimeout(resolve, waitMs);
|
|
1255
|
+
signal.addEventListener("abort", wake, { once: true });
|
|
1256
|
+
// Covers readiness before subscription as well as before acquisition.
|
|
1257
|
+
if (signal.aborted) wake();
|
|
1258
|
+
reportProgress(context, "SQLite busy retry wait started");
|
|
1259
|
+
});
|
|
1260
|
+
} finally {
|
|
1261
|
+
clearTimeout(timer);
|
|
1262
|
+
if (wake !== undefined) signal.removeEventListener("abort", wake);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1138
1265
|
throwIfExpired(context, path, cause);
|
|
1139
1266
|
}
|
|
1140
1267
|
|
|
@@ -1143,16 +1270,29 @@ async function acquireSqliteDatabase(DatabaseSync, prepared, context) {
|
|
|
1143
1270
|
let lastBusy;
|
|
1144
1271
|
while (true) {
|
|
1145
1272
|
throwIfExpired(context, prepared.path, lastBusy);
|
|
1146
|
-
|
|
1273
|
+
reportProgress(context, "SQLite pre-open lock validation started");
|
|
1274
|
+
await revalidatePreparedLock(prepared, context, {
|
|
1275
|
+
// The creation path already validated the complete ancestor chain and
|
|
1276
|
+
// the exact parent/file ACLs. Revalidate ACLs after contention because
|
|
1277
|
+
// another holder and an unbounded interval have crossed this attempt.
|
|
1278
|
+
validateWindowsAcl: attempt > 0,
|
|
1279
|
+
});
|
|
1280
|
+
reportProgress(context, "SQLite pre-open lock validation complete");
|
|
1147
1281
|
let database;
|
|
1148
1282
|
try {
|
|
1283
|
+
reportProgress(context, "SQLite database open started");
|
|
1149
1284
|
database = new DatabaseSync(prepared.path, {
|
|
1150
1285
|
allowExtension: false,
|
|
1151
1286
|
timeout: 0,
|
|
1152
1287
|
});
|
|
1288
|
+
reportProgress(context, "SQLite database open complete");
|
|
1289
|
+
reportProgress(context, "SQLite connection hardening started");
|
|
1153
1290
|
configureLocalSqliteLockConnection(database);
|
|
1291
|
+
reportProgress(context, "SQLite connection hardening complete");
|
|
1292
|
+
reportProgress(context, "SQLite post-open lock validation started");
|
|
1154
1293
|
await revalidatePreparedLock(prepared, context);
|
|
1155
|
-
|
|
1294
|
+
reportProgress(context, "SQLite post-open lock validation complete");
|
|
1295
|
+
beginAndValidateLock(database, prepared.path, context);
|
|
1156
1296
|
throwIfExpired(context, prepared.path, lastBusy);
|
|
1157
1297
|
return database;
|
|
1158
1298
|
} catch (error) {
|
|
@@ -1207,6 +1347,8 @@ export async function acquireLocalSqliteLocks(
|
|
|
1207
1347
|
timeoutMs = 60_000,
|
|
1208
1348
|
label = "AgenC operation",
|
|
1209
1349
|
deadline: suppliedDeadline,
|
|
1350
|
+
onProgress,
|
|
1351
|
+
retryWakeSignal,
|
|
1210
1352
|
} = {},
|
|
1211
1353
|
) {
|
|
1212
1354
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
@@ -1218,6 +1360,12 @@ export async function acquireLocalSqliteLocks(
|
|
|
1218
1360
|
if (!Array.isArray(requestedPaths)) {
|
|
1219
1361
|
throw new TypeError("lock paths must be an array");
|
|
1220
1362
|
}
|
|
1363
|
+
if (onProgress !== undefined && typeof onProgress !== "function") {
|
|
1364
|
+
throw new TypeError("lock onProgress must be a function");
|
|
1365
|
+
}
|
|
1366
|
+
if (retryWakeSignal !== undefined && !(retryWakeSignal instanceof AbortSignal)) {
|
|
1367
|
+
throw new TypeError("lock retryWakeSignal must be an AbortSignal");
|
|
1368
|
+
}
|
|
1221
1369
|
if (requestedPaths.length === 0) return () => {};
|
|
1222
1370
|
|
|
1223
1371
|
const startedAt = performance.now();
|
|
@@ -1228,6 +1376,9 @@ export async function acquireLocalSqliteLocks(
|
|
|
1228
1376
|
),
|
|
1229
1377
|
label,
|
|
1230
1378
|
timeoutMs,
|
|
1379
|
+
onProgress,
|
|
1380
|
+
retryWakeSignal,
|
|
1381
|
+
retryWakeConsumed: false,
|
|
1231
1382
|
};
|
|
1232
1383
|
const firstDisplayPath = resolve(requestedPaths[0]);
|
|
1233
1384
|
throwIfExpired(context, firstDisplayPath);
|
|
@@ -1238,6 +1389,7 @@ export async function acquireLocalSqliteLocks(
|
|
|
1238
1389
|
const prepared = await prepareLockPath(requestedPath, context);
|
|
1239
1390
|
preparedByIdentity.set(prepared.identityKey, prepared);
|
|
1240
1391
|
}
|
|
1392
|
+
reportProgress(context, "lock path preparation complete");
|
|
1241
1393
|
const preparedLocks = [...preparedByIdentity.values()].sort((left, right) =>
|
|
1242
1394
|
left.identityKey < right.identityKey ? -1 : left.identityKey > right.identityKey ? 1 : 0);
|
|
1243
1395
|
const pendingLocal = [];
|
|
@@ -1249,8 +1401,11 @@ export async function acquireLocalSqliteLocks(
|
|
|
1249
1401
|
const release = await acquireInProcessLock(prepared, context);
|
|
1250
1402
|
pendingLocal.push({ prepared, release });
|
|
1251
1403
|
}
|
|
1404
|
+
reportProgress(context, "in-process lock acquisition complete");
|
|
1252
1405
|
throwIfExpired(context, currentPath);
|
|
1406
|
+
reportProgress(context, "SQLite module import started");
|
|
1253
1407
|
const { DatabaseSync } = await import("node:sqlite");
|
|
1408
|
+
reportProgress(context, "SQLite module import complete");
|
|
1254
1409
|
throwIfExpired(context, currentPath);
|
|
1255
1410
|
for (const { prepared, release } of pendingLocal) {
|
|
1256
1411
|
currentPath = prepared.path;
|
|
@@ -1260,7 +1415,9 @@ export async function acquireLocalSqliteLocks(
|
|
|
1260
1415
|
inProcessReleased: false,
|
|
1261
1416
|
};
|
|
1262
1417
|
acquired.push(item);
|
|
1418
|
+
reportProgress(context, "SQLite transaction acquisition started");
|
|
1263
1419
|
item.database = await acquireSqliteDatabase(DatabaseSync, prepared, context);
|
|
1420
|
+
reportProgress(context, "SQLite transaction acquisition complete");
|
|
1264
1421
|
}
|
|
1265
1422
|
} catch (error) {
|
|
1266
1423
|
const cleanupErrors = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tetsuo-ai/agenc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Public AgenC CLI launcher package",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"src/launcher.mjs",
|
|
22
22
|
"lib/activation-lock-identity.d.mts",
|
|
23
23
|
"lib/activation-lock-identity.mjs",
|
|
24
|
+
"lib/home-authority.d.mts",
|
|
25
|
+
"lib/home-authority.mjs",
|
|
24
26
|
"lib/generated-wrapper.d.mts",
|
|
25
27
|
"lib/generated-wrapper.mjs",
|
|
26
28
|
"lib/runtime-archive.d.mts",
|
|
@@ -32,11 +34,12 @@
|
|
|
32
34
|
"lib/sqlite-lock.d.mts",
|
|
33
35
|
"lib/sqlite-lock.mjs",
|
|
34
36
|
"scripts/postinstall.mjs",
|
|
35
|
-
"generated/agenc-runtime-manifest-v2.json"
|
|
37
|
+
"generated/agenc-runtime-manifest-v2.json",
|
|
38
|
+
"generated/startup-preflight.mjs"
|
|
36
39
|
],
|
|
37
40
|
"scripts": {
|
|
38
41
|
"postinstall": "node ./scripts/postinstall.mjs",
|
|
39
|
-
"prepack": "node ./scripts/check-package-ready.mjs && node ../../scripts/canonicalize-package-modes.mjs .",
|
|
42
|
+
"prepack": "node ../../runtime/scripts/check-launcher-preflight.mjs --check && node ./scripts/check-package-ready.mjs && node ../../scripts/canonicalize-package-modes.mjs .",
|
|
40
43
|
"build:runtime-tarball": "node ./scripts/build-runtime-tarball.mjs",
|
|
41
44
|
"gen:manifest": "node ./scripts/gen-manifest.mjs",
|
|
42
45
|
"test": "node --test test/*.test.mjs"
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// Runs on `npm install @tetsuo-ai/agenc`. Pre-fetches the platform runtime so
|
|
3
3
|
// the first `agenc` invocation is fast. Best-effort: a failure here (offline
|
|
4
4
|
// install, CI, etc.) is NOT fatal — the launcher fetches lazily on first run.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
5
8
|
|
|
6
9
|
// Skip in obviously non-interactive / packaging contexts.
|
|
7
10
|
if (process.env.AGENC_SKIP_POSTINSTALL === "1" || process.env.CI === "true") {
|
|
@@ -9,8 +12,26 @@ if (process.env.AGENC_SKIP_POSTINSTALL === "1" || process.env.CI === "true") {
|
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
try {
|
|
12
|
-
const {
|
|
13
|
-
await
|
|
15
|
+
const { ensureRuntimeLaunch } = await import("../lib/runtime-manager.mjs");
|
|
16
|
+
const launch = await ensureRuntimeLaunch();
|
|
17
|
+
const helper = join(dirname(launch.runtimeBin), "prepare-peer-credentials.mjs");
|
|
18
|
+
if (existsSync(helper)) {
|
|
19
|
+
// Use the installed runtime's Node ABI and private library directory.
|
|
20
|
+
// Preparation only warms the user cache; signed package bytes stay intact.
|
|
21
|
+
const result = spawnSync(launch.nodeBin, [helper], {
|
|
22
|
+
env: {
|
|
23
|
+
...process.env,
|
|
24
|
+
...(launch.nodeLibraryPath === undefined
|
|
25
|
+
? {}
|
|
26
|
+
: { LD_LIBRARY_PATH: launch.nodeLibraryPath }),
|
|
27
|
+
},
|
|
28
|
+
stdio: "inherit",
|
|
29
|
+
timeout: 30_000,
|
|
30
|
+
});
|
|
31
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
32
|
+
process.stderr.write("agenc: native peer credential preparation skipped; startup will retry.\n");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
14
35
|
} catch (err) {
|
|
15
36
|
process.stderr.write(
|
|
16
37
|
`agenc: runtime pre-fetch skipped (${err?.message ?? err}); it will be fetched on first run.\n`,
|