@hasna/todos 0.15.18 → 0.15.20
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/cli/cloud-router.d.ts +20 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +1306 -122
- package/dist/cli/stage-a.d.ts +21 -10
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +283 -16
- package/dist/db/audit.d.ts +8 -0
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/plans.d.ts +4 -0
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts +7 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.js +599 -26
- package/dist/lib/cli-help.d.ts +3 -2
- package/dist/lib/cli-help.d.ts.map +1 -1
- package/dist/lib/stale-lock-handoff.d.ts +25 -0
- package/dist/lib/stale-lock-handoff.d.ts.map +1 -0
- package/dist/mcp/index.js +954 -46
- package/dist/mcp.js +3 -1
- package/dist/project-registration.js +4250 -3684
- package/dist/registry.js +283 -16
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/v1.generated.d.ts +40 -1
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1287 -379
- package/dist/server/openapi.d.ts +232 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/audit-history-import.d.ts +14 -0
- package/dist/storage/audit-history-import.d.ts.map +1 -0
- package/dist/storage/interfaces.d.ts +22 -1
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/shadow.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +596 -25
- package/dist/task-manifest.js +24 -1
- package/dist/types/index.d.ts +50 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -1
package/dist/server/index.js
CHANGED
|
@@ -70,7 +70,7 @@ var package_default;
|
|
|
70
70
|
var init_package = __esm(() => {
|
|
71
71
|
package_default = {
|
|
72
72
|
name: "@hasna/todos",
|
|
73
|
-
version: "0.15.
|
|
73
|
+
version: "0.15.20",
|
|
74
74
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
75
75
|
type: "module",
|
|
76
76
|
main: "dist/index.js",
|
|
@@ -141,6 +141,8 @@ var init_package = __esm(() => {
|
|
|
141
141
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
142
142
|
"dev:serve": "bun run src/server/index.ts",
|
|
143
143
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
144
|
+
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
145
|
+
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
144
146
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
145
147
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
146
148
|
},
|
|
@@ -861,7 +863,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
861
863
|
function isTerminalStatus(status) {
|
|
862
864
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
863
865
|
}
|
|
864
|
-
var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
866
|
+
var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
865
867
|
var init_types = __esm(() => {
|
|
866
868
|
TASK_STATUSES = [
|
|
867
869
|
"pending",
|
|
@@ -948,6 +950,19 @@ var init_types = __esm(() => {
|
|
|
948
950
|
this.name = "ResourceConflictError";
|
|
949
951
|
}
|
|
950
952
|
};
|
|
953
|
+
PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
|
|
954
|
+
planId;
|
|
955
|
+
expectedUpdatedAt;
|
|
956
|
+
currentUpdatedAt;
|
|
957
|
+
static code = "PLAN_REVISION_CONFLICT";
|
|
958
|
+
constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
|
|
959
|
+
super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
|
|
960
|
+
this.planId = planId;
|
|
961
|
+
this.expectedUpdatedAt = expectedUpdatedAt;
|
|
962
|
+
this.currentUpdatedAt = currentUpdatedAt;
|
|
963
|
+
this.name = "PlanRevisionConflictError";
|
|
964
|
+
}
|
|
965
|
+
};
|
|
951
966
|
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
952
967
|
planId;
|
|
953
968
|
static code = "PLAN_NOT_FOUND";
|
|
@@ -970,6 +985,16 @@ var init_types = __esm(() => {
|
|
|
970
985
|
this.name = "LockError";
|
|
971
986
|
}
|
|
972
987
|
};
|
|
988
|
+
StaleLockHandoffError = class StaleLockHandoffError extends Error {
|
|
989
|
+
code;
|
|
990
|
+
details;
|
|
991
|
+
constructor(code, message, details = {}) {
|
|
992
|
+
super(message);
|
|
993
|
+
this.code = code;
|
|
994
|
+
this.details = details;
|
|
995
|
+
this.name = "StaleLockHandoffError";
|
|
996
|
+
}
|
|
997
|
+
};
|
|
973
998
|
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
974
999
|
agentId;
|
|
975
1000
|
static code = "AGENT_NOT_FOUND";
|
|
@@ -1183,6 +1208,501 @@ var init_plan_project_link_contract = __esm(() => {
|
|
|
1183
1208
|
};
|
|
1184
1209
|
});
|
|
1185
1210
|
|
|
1211
|
+
// src/lib/config.ts
|
|
1212
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1213
|
+
import { dirname, join as join3 } from "path";
|
|
1214
|
+
function getConfigPath() {
|
|
1215
|
+
return join3(getTodosGlobalDir(), "config.json");
|
|
1216
|
+
}
|
|
1217
|
+
function loadConfig() {
|
|
1218
|
+
if (cached)
|
|
1219
|
+
return cached;
|
|
1220
|
+
if (!existsSync3(getConfigPath())) {
|
|
1221
|
+
cached = {};
|
|
1222
|
+
return cached;
|
|
1223
|
+
}
|
|
1224
|
+
const config = readJsonFile(getConfigPath()) || {};
|
|
1225
|
+
if (typeof config.sync_agents === "string") {
|
|
1226
|
+
config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1227
|
+
}
|
|
1228
|
+
cached = config;
|
|
1229
|
+
return cached;
|
|
1230
|
+
}
|
|
1231
|
+
function saveConfig(config) {
|
|
1232
|
+
const configPath = getConfigPath();
|
|
1233
|
+
ensureDir(dirname(configPath));
|
|
1234
|
+
writeJsonFile(configPath, config);
|
|
1235
|
+
cached = config;
|
|
1236
|
+
return config;
|
|
1237
|
+
}
|
|
1238
|
+
function getAgentPoolForProject(workingDir) {
|
|
1239
|
+
const config = loadConfig();
|
|
1240
|
+
if (workingDir && config.project_pools) {
|
|
1241
|
+
let bestKey = null;
|
|
1242
|
+
let bestLen = 0;
|
|
1243
|
+
for (const key of Object.keys(config.project_pools)) {
|
|
1244
|
+
if (workingDir.startsWith(key) && key.length > bestLen) {
|
|
1245
|
+
bestKey = key;
|
|
1246
|
+
bestLen = key.length;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (bestKey && config.project_pools[bestKey]) {
|
|
1250
|
+
return config.project_pools[bestKey];
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return config.agent_pool || null;
|
|
1254
|
+
}
|
|
1255
|
+
function getCompletionGuardConfig(projectPath) {
|
|
1256
|
+
const config = loadConfig();
|
|
1257
|
+
const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
|
|
1258
|
+
if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
|
|
1259
|
+
return { ...global, ...config.project_overrides[projectPath].completion_guard };
|
|
1260
|
+
}
|
|
1261
|
+
return global;
|
|
1262
|
+
}
|
|
1263
|
+
var cached = null, GUARD_DEFAULTS;
|
|
1264
|
+
var init_config2 = __esm(() => {
|
|
1265
|
+
init_sync_utils();
|
|
1266
|
+
GUARD_DEFAULTS = {
|
|
1267
|
+
enabled: false,
|
|
1268
|
+
min_work_seconds: 30,
|
|
1269
|
+
max_completions_per_window: 5,
|
|
1270
|
+
window_minutes: 10,
|
|
1271
|
+
cooldown_seconds: 60
|
|
1272
|
+
};
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// src/lib/redaction.ts
|
|
1276
|
+
function unique(values) {
|
|
1277
|
+
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
1278
|
+
}
|
|
1279
|
+
function cloneRegex(regex) {
|
|
1280
|
+
return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
|
|
1281
|
+
}
|
|
1282
|
+
function customPatterns() {
|
|
1283
|
+
return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
|
|
1284
|
+
try {
|
|
1285
|
+
return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
|
|
1286
|
+
} catch {
|
|
1287
|
+
return [];
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
function secretPatterns() {
|
|
1292
|
+
return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
|
|
1293
|
+
}
|
|
1294
|
+
function isRedactionPlaceholderMatch(match) {
|
|
1295
|
+
const trimmed = match.trim();
|
|
1296
|
+
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(trimmed) || new RegExp(`=\\s*['"]?${REDACTION_PLACEHOLDER}['"]?$`).test(trimmed);
|
|
1297
|
+
}
|
|
1298
|
+
function isRedactionPlaceholderKey(key) {
|
|
1299
|
+
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(key.trim());
|
|
1300
|
+
}
|
|
1301
|
+
function isSecretKey(key) {
|
|
1302
|
+
if (isRedactionPlaceholderKey(key))
|
|
1303
|
+
return false;
|
|
1304
|
+
if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
|
|
1305
|
+
return false;
|
|
1306
|
+
if (DEFAULT_SECRET_KEY_PATTERN.test(key))
|
|
1307
|
+
return true;
|
|
1308
|
+
return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
|
|
1309
|
+
}
|
|
1310
|
+
function redactEvidenceText(value) {
|
|
1311
|
+
let redacted = value;
|
|
1312
|
+
for (const pattern of secretPatterns()) {
|
|
1313
|
+
const regex = cloneRegex(pattern.regex);
|
|
1314
|
+
const replacement = pattern.replacement ?? "[REDACTED]";
|
|
1315
|
+
redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
|
|
1316
|
+
}
|
|
1317
|
+
return redacted;
|
|
1318
|
+
}
|
|
1319
|
+
function redactValue(value) {
|
|
1320
|
+
if (typeof value === "string")
|
|
1321
|
+
return redactEvidenceText(value);
|
|
1322
|
+
if (Array.isArray(value))
|
|
1323
|
+
return value.map(redactValue);
|
|
1324
|
+
if (value && typeof value === "object") {
|
|
1325
|
+
const redacted = {};
|
|
1326
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1327
|
+
if (isSecretKey(key)) {
|
|
1328
|
+
redacted[key] = "[REDACTED]";
|
|
1329
|
+
} else {
|
|
1330
|
+
redacted[key] = redactValue(child);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return redacted;
|
|
1334
|
+
}
|
|
1335
|
+
return value;
|
|
1336
|
+
}
|
|
1337
|
+
function listSecretFindings(value) {
|
|
1338
|
+
const findings = [];
|
|
1339
|
+
for (const pattern of secretPatterns()) {
|
|
1340
|
+
const matches = value.match(cloneRegex(pattern.regex))?.filter((match) => !isRedactionPlaceholderMatch(match));
|
|
1341
|
+
if (matches?.length)
|
|
1342
|
+
findings.push({ pattern: pattern.name, count: matches.length });
|
|
1343
|
+
}
|
|
1344
|
+
return findings;
|
|
1345
|
+
}
|
|
1346
|
+
function getSecretSafetyConfig() {
|
|
1347
|
+
return {
|
|
1348
|
+
redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
|
|
1349
|
+
redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
function upsertSecretSafetyConfig(input) {
|
|
1353
|
+
const config = loadConfig();
|
|
1354
|
+
const next = {
|
|
1355
|
+
redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
|
|
1356
|
+
redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
|
|
1357
|
+
};
|
|
1358
|
+
saveConfig({ ...config, secret_safety: next });
|
|
1359
|
+
return next;
|
|
1360
|
+
}
|
|
1361
|
+
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
1362
|
+
var init_redaction = __esm(() => {
|
|
1363
|
+
init_config2();
|
|
1364
|
+
DEFAULT_SECRET_PATTERNS = [
|
|
1365
|
+
{ name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
|
|
1366
|
+
{ name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
|
|
1367
|
+
{ name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
1368
|
+
{ name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
|
|
1369
|
+
{ name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
1370
|
+
{ name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
1371
|
+
{ name: "env-secret-assignment", regex: /\b([A-Za-z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*)\s*=\s*['"]?[^'"\s]{8,}/gi, replacement: "$1=[REDACTED]" },
|
|
1372
|
+
{ name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
|
|
1373
|
+
];
|
|
1374
|
+
DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
|
|
1375
|
+
NON_SECRET_USAGE_KEYS = new Set([
|
|
1376
|
+
"tokens",
|
|
1377
|
+
"total_tokens",
|
|
1378
|
+
"token_count",
|
|
1379
|
+
"input_tokens",
|
|
1380
|
+
"output_tokens",
|
|
1381
|
+
"prompt_tokens",
|
|
1382
|
+
"completion_tokens",
|
|
1383
|
+
"cost_tokens"
|
|
1384
|
+
]);
|
|
1385
|
+
REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
// src/lib/secret-redaction.ts
|
|
1389
|
+
function isAllowlisted(text, match, allowlist) {
|
|
1390
|
+
const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
|
|
1391
|
+
return allowlist.some((re) => re.test(context) || re.test(match));
|
|
1392
|
+
}
|
|
1393
|
+
function scanTextForSecrets(text, options = {}) {
|
|
1394
|
+
const allowlist = [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []];
|
|
1395
|
+
const matches = [];
|
|
1396
|
+
const patterns = [
|
|
1397
|
+
...DEFAULT_PATTERNS,
|
|
1398
|
+
...options.custom_patterns?.map((p, i) => ({ name: `custom_${i}`, pattern: p })) ?? []
|
|
1399
|
+
];
|
|
1400
|
+
for (const { name, pattern, allowlist_ok } of patterns) {
|
|
1401
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
1402
|
+
let m;
|
|
1403
|
+
while ((m = re.exec(text)) !== null) {
|
|
1404
|
+
const match = m[0];
|
|
1405
|
+
if (allowlist_ok && isAllowlisted(text, match, allowlist))
|
|
1406
|
+
continue;
|
|
1407
|
+
if (isAllowlisted(text, match, allowlist))
|
|
1408
|
+
continue;
|
|
1409
|
+
const line = text.slice(0, m.index).split(`
|
|
1410
|
+
`).length;
|
|
1411
|
+
matches.push({ pattern: name, match: match.slice(0, 12) + (match.length > 12 ? "\u2026" : ""), index: m.index, line });
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
return {
|
|
1415
|
+
schema_version: SECRET_REDACTION_SCHEMA,
|
|
1416
|
+
clean: matches.length === 0,
|
|
1417
|
+
matches
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function redactText(text, options = {}) {
|
|
1421
|
+
const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
|
|
1422
|
+
let out = text;
|
|
1423
|
+
for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
|
|
1424
|
+
out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
|
|
1425
|
+
if (allowlist_ok && isAllowlisted(out, match, [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []])) {
|
|
1426
|
+
return match;
|
|
1427
|
+
}
|
|
1428
|
+
return placeholder;
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
for (const custom of options.custom_patterns ?? []) {
|
|
1432
|
+
out = out.replace(custom, placeholder);
|
|
1433
|
+
}
|
|
1434
|
+
for (const fn of customRedactors) {
|
|
1435
|
+
out = fn(out);
|
|
1436
|
+
}
|
|
1437
|
+
return out;
|
|
1438
|
+
}
|
|
1439
|
+
function redactExportRecord(record) {
|
|
1440
|
+
const base = redactValue(record);
|
|
1441
|
+
for (const [key, value] of Object.entries(base)) {
|
|
1442
|
+
if (typeof value === "string") {
|
|
1443
|
+
base[key] = redactText(value);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return base;
|
|
1447
|
+
}
|
|
1448
|
+
var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
|
|
1449
|
+
var init_secret_redaction = __esm(() => {
|
|
1450
|
+
init_redaction();
|
|
1451
|
+
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
1452
|
+
DEFAULT_PATTERNS = [
|
|
1453
|
+
{ name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
|
|
1454
|
+
{ name: "github_pat", pattern: /\bghp_[a-zA-Z0-9]{20,}\b/g },
|
|
1455
|
+
{ name: "github_oauth", pattern: /\bgho_[a-zA-Z0-9]{20,}\b/g },
|
|
1456
|
+
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
1457
|
+
{ name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
|
|
1458
|
+
{ name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
1459
|
+
{ name: "private_key_block", pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
1460
|
+
{ name: "generic_api_key", pattern: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[a-zA-Z0-9\-._]{8,}['"]?/gi, allowlist_ok: true }
|
|
1461
|
+
];
|
|
1462
|
+
DEFAULT_ALLOWLIST = [
|
|
1463
|
+
/\[REDACTED\]/,
|
|
1464
|
+
/example\.com/i,
|
|
1465
|
+
/your-api-key-here/i,
|
|
1466
|
+
/sk-test/i,
|
|
1467
|
+
/ghp_xxx/i
|
|
1468
|
+
];
|
|
1469
|
+
customRedactors = [];
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
// src/lib/prewrite-secrets.ts
|
|
1473
|
+
function mergeFindings(findings) {
|
|
1474
|
+
const byPattern = new Map;
|
|
1475
|
+
for (const finding of findings) {
|
|
1476
|
+
const existing = byPattern.get(finding.pattern);
|
|
1477
|
+
if (!existing) {
|
|
1478
|
+
byPattern.set(finding.pattern, {
|
|
1479
|
+
pattern: finding.pattern,
|
|
1480
|
+
count: finding.count,
|
|
1481
|
+
lines: Array.from(new Set(finding.lines)).sort((a, b) => a - b)
|
|
1482
|
+
});
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
existing.count += finding.count;
|
|
1486
|
+
existing.lines = Array.from(new Set([...existing.lines, ...finding.lines])).sort((a, b) => a - b);
|
|
1487
|
+
}
|
|
1488
|
+
return [...byPattern.values()].sort((left, right) => left.pattern.localeCompare(right.pattern));
|
|
1489
|
+
}
|
|
1490
|
+
function redactPreWriteText(value) {
|
|
1491
|
+
return redactText(redactEvidenceText(value));
|
|
1492
|
+
}
|
|
1493
|
+
function scanPreWriteText(value, context = "text") {
|
|
1494
|
+
const redacted = redactPreWriteText(value);
|
|
1495
|
+
if (redacted === value) {
|
|
1496
|
+
return {
|
|
1497
|
+
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
1498
|
+
clean: true,
|
|
1499
|
+
context,
|
|
1500
|
+
findings: []
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
const structural = scanTextForSecrets(value).matches.map((match) => ({
|
|
1504
|
+
pattern: match.pattern,
|
|
1505
|
+
count: 1,
|
|
1506
|
+
lines: match.line ? [match.line] : []
|
|
1507
|
+
}));
|
|
1508
|
+
const configured = listSecretFindings(value).map((finding) => ({
|
|
1509
|
+
pattern: finding.pattern,
|
|
1510
|
+
count: finding.count,
|
|
1511
|
+
lines: []
|
|
1512
|
+
}));
|
|
1513
|
+
return {
|
|
1514
|
+
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
1515
|
+
clean: false,
|
|
1516
|
+
context,
|
|
1517
|
+
findings: mergeFindings([...structural, ...configured])
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function assertPreWriteTextClean(value, context = "text") {
|
|
1521
|
+
const scan = scanPreWriteText(value, context);
|
|
1522
|
+
if (!scan.clean)
|
|
1523
|
+
throw new PreWriteSecretError(context, scan.findings);
|
|
1524
|
+
}
|
|
1525
|
+
function sanitizePreWriteText(value, context = "text", options = {}) {
|
|
1526
|
+
const mode = options.mode ?? "redact";
|
|
1527
|
+
if (mode === "block") {
|
|
1528
|
+
assertPreWriteTextClean(value, context);
|
|
1529
|
+
return value;
|
|
1530
|
+
}
|
|
1531
|
+
return redactPreWriteText(value);
|
|
1532
|
+
}
|
|
1533
|
+
function sanitizePreWriteValue(value, context = "value", options = {}) {
|
|
1534
|
+
if (typeof value === "string")
|
|
1535
|
+
return sanitizePreWriteText(value, context, options);
|
|
1536
|
+
if (Array.isArray(value)) {
|
|
1537
|
+
return value.map((item, index) => sanitizePreWriteValue(item, `${context}[${index}]`, options));
|
|
1538
|
+
}
|
|
1539
|
+
if (value && typeof value === "object") {
|
|
1540
|
+
const sanitized = {};
|
|
1541
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1542
|
+
const safeKey = sanitizePreWriteText(key, `${context}.$key`, options);
|
|
1543
|
+
sanitized[safeKey] = sanitizePreWriteValue(child, `${context}.${safeKey}`, options);
|
|
1544
|
+
}
|
|
1545
|
+
return redactValue(sanitized);
|
|
1546
|
+
}
|
|
1547
|
+
return value;
|
|
1548
|
+
}
|
|
1549
|
+
var PREWRITE_SECRET_SCAN_SCHEMA, PreWriteSecretError;
|
|
1550
|
+
var init_prewrite_secrets = __esm(() => {
|
|
1551
|
+
init_redaction();
|
|
1552
|
+
init_secret_redaction();
|
|
1553
|
+
PREWRITE_SECRET_SCAN_SCHEMA = ["todos", "prewrite_secret_scan", "v1"].join(".");
|
|
1554
|
+
PreWriteSecretError = class PreWriteSecretError extends Error {
|
|
1555
|
+
context;
|
|
1556
|
+
findings;
|
|
1557
|
+
constructor(context, findings) {
|
|
1558
|
+
const summary = findings.map((finding) => `${finding.pattern}:${finding.count}`).join(", ");
|
|
1559
|
+
super(`Secret pattern detected before persistence in ${context}: ${summary}`);
|
|
1560
|
+
this.name = "PreWriteSecretError";
|
|
1561
|
+
this.context = context;
|
|
1562
|
+
this.findings = findings;
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
// src/lib/stale-lock-handoff.ts
|
|
1568
|
+
function normalizeExactTaskId(value) {
|
|
1569
|
+
if (typeof value !== "string" || !EXACT_TASK_UUID_RE.test(value.trim())) {
|
|
1570
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_TASK_ID", "stale-lock handoff requires one exact full task UUID", { task_id: typeof value === "string" ? value : null });
|
|
1571
|
+
}
|
|
1572
|
+
return value.trim().toLowerCase();
|
|
1573
|
+
}
|
|
1574
|
+
function requireNonEmptyString(value, field) {
|
|
1575
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1576
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `${field} must be a non-empty string`, { field });
|
|
1577
|
+
}
|
|
1578
|
+
const trimmed = value.trim();
|
|
1579
|
+
if (field === "reason" && trimmed.length > MAX_REASON_LENGTH) {
|
|
1580
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `reason must be at most ${MAX_REASON_LENGTH} characters`, { field, max_length: MAX_REASON_LENGTH });
|
|
1581
|
+
}
|
|
1582
|
+
return trimmed;
|
|
1583
|
+
}
|
|
1584
|
+
function requireCanonicalLockVersion(value) {
|
|
1585
|
+
if (typeof value !== "string" || !CANONICAL_LOCK_VERSION_RE.test(value)) {
|
|
1586
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must be the exact canonical locked_at timestamp (YYYY-MM-DDTHH:mm:ss.sssZ)", { field: "expected_lock_version" });
|
|
1587
|
+
}
|
|
1588
|
+
const parsed = Date.parse(value);
|
|
1589
|
+
if (Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
|
|
1590
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must name a real canonical UTC instant", { field: "expected_lock_version" });
|
|
1591
|
+
}
|
|
1592
|
+
return value;
|
|
1593
|
+
}
|
|
1594
|
+
function requireStaleThreshold(value) {
|
|
1595
|
+
if (!Number.isSafeInteger(value) || Number(value) <= 0) {
|
|
1596
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "stale_after_seconds must be a positive safe integer", { field: "stale_after_seconds" });
|
|
1597
|
+
}
|
|
1598
|
+
return Number(value);
|
|
1599
|
+
}
|
|
1600
|
+
function prepareStaleLockHandoff(input, options = {}) {
|
|
1601
|
+
const taskId = normalizeExactTaskId(input.task_id);
|
|
1602
|
+
const actor = requireNonEmptyString(input.actor, "actor");
|
|
1603
|
+
const expectedHolder = requireNonEmptyString(input.expected_holder, "expected_holder");
|
|
1604
|
+
const newHolder = requireNonEmptyString(input.new_holder, "new_holder");
|
|
1605
|
+
const expectedLockVersion = requireCanonicalLockVersion(input.expected_lock_version);
|
|
1606
|
+
const staleAfterSeconds = requireStaleThreshold(input.stale_after_seconds);
|
|
1607
|
+
const reason = sanitizePreWriteText(requireNonEmptyString(input.reason, "reason"), "stale_lock_handoff.reason").trim();
|
|
1608
|
+
if (!reason) {
|
|
1609
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "reason must remain non-empty after safety filtering", { field: "reason" });
|
|
1610
|
+
}
|
|
1611
|
+
if (canonicalAgentRef(actor) !== canonicalAgentRef(newHolder)) {
|
|
1612
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_ACTOR_MISMATCH", "new_holder must match the authenticated actor", { actor, new_holder: newHolder });
|
|
1613
|
+
}
|
|
1614
|
+
if (canonicalAgentRef(expectedHolder) === canonicalAgentRef(newHolder)) {
|
|
1615
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "new_holder must differ from expected_holder", { field: "new_holder" });
|
|
1616
|
+
}
|
|
1617
|
+
const operationTimestamp = options.now ?? new Date().toISOString();
|
|
1618
|
+
if (!CANONICAL_LOCK_VERSION_RE.test(operationTimestamp) || new Date(Date.parse(operationTimestamp)).toISOString() !== operationTimestamp) {
|
|
1619
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "operation timestamp must be a canonical UTC instant");
|
|
1620
|
+
}
|
|
1621
|
+
const staleCutoff = new Date(Date.parse(operationTimestamp) - staleAfterSeconds * 1000).toISOString();
|
|
1622
|
+
return {
|
|
1623
|
+
task_id: taskId,
|
|
1624
|
+
actor,
|
|
1625
|
+
expected_holder: expectedHolder,
|
|
1626
|
+
expected_lock_version: expectedLockVersion,
|
|
1627
|
+
stale_after_seconds: staleAfterSeconds,
|
|
1628
|
+
new_holder: newHolder,
|
|
1629
|
+
reason,
|
|
1630
|
+
operation_timestamp: operationTimestamp,
|
|
1631
|
+
stale_cutoff: staleCutoff,
|
|
1632
|
+
receipt_id: options.receiptId ?? crypto.randomUUID()
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function buildStaleLockHandoffReceipt(input) {
|
|
1636
|
+
return {
|
|
1637
|
+
schema_version: STALE_LOCK_HANDOFF_SCHEMA_VERSION,
|
|
1638
|
+
receipt_id: input.receipt_id,
|
|
1639
|
+
task_id: input.task_id,
|
|
1640
|
+
actor: input.actor,
|
|
1641
|
+
previous_holder: input.expected_holder,
|
|
1642
|
+
previous_lock_version: input.expected_lock_version,
|
|
1643
|
+
new_holder: input.new_holder,
|
|
1644
|
+
new_lock_version: input.operation_timestamp,
|
|
1645
|
+
stale_after_seconds: input.stale_after_seconds,
|
|
1646
|
+
stale_cutoff: input.stale_cutoff,
|
|
1647
|
+
reason: input.reason,
|
|
1648
|
+
created_at: input.operation_timestamp
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
function staleLockHandoffHistory(receipt, machineId) {
|
|
1652
|
+
return {
|
|
1653
|
+
id: receipt.receipt_id,
|
|
1654
|
+
task_id: receipt.task_id,
|
|
1655
|
+
action: STALE_LOCK_HANDOFF_ACTION,
|
|
1656
|
+
field: STALE_LOCK_HANDOFF_FIELD,
|
|
1657
|
+
old_value: JSON.stringify({
|
|
1658
|
+
holder: receipt.previous_holder,
|
|
1659
|
+
lock_version: receipt.previous_lock_version
|
|
1660
|
+
}),
|
|
1661
|
+
new_value: JSON.stringify(receipt),
|
|
1662
|
+
agent_id: receipt.actor,
|
|
1663
|
+
created_at: receipt.created_at,
|
|
1664
|
+
machine_id: machineId
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function throwStaleLockHandoffConflict(task, input) {
|
|
1668
|
+
if (!task.locked_by || !task.locked_at) {
|
|
1669
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_LOCKED", `Task ${input.task_id} does not have a complete lock to hand off`, { task_id: input.task_id });
|
|
1670
|
+
}
|
|
1671
|
+
if (task.locked_at !== input.expected_lock_version) {
|
|
1672
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_VERSION_MISMATCH", `Task ${input.task_id} lock version changed`, {
|
|
1673
|
+
task_id: input.task_id,
|
|
1674
|
+
expected_lock_version: input.expected_lock_version,
|
|
1675
|
+
current_lock_version: task.locked_at
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
if (task.locked_by !== input.expected_holder) {
|
|
1679
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_HOLDER_MISMATCH", `Task ${input.task_id} lock holder changed`, {
|
|
1680
|
+
task_id: input.task_id,
|
|
1681
|
+
expected_holder: input.expected_holder,
|
|
1682
|
+
current_holder: task.locked_by
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
if (isTerminalStatus(task.status)) {
|
|
1686
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_TERMINAL", `Task ${input.task_id} is ${task.status} and cannot transfer a lock`, { task_id: input.task_id, status: task.status });
|
|
1687
|
+
}
|
|
1688
|
+
if (task.locked_at >= input.stale_cutoff) {
|
|
1689
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_STALE", `Task ${input.task_id} lock is not older than the supplied stale threshold`, {
|
|
1690
|
+
task_id: input.task_id,
|
|
1691
|
+
current_lock_version: task.locked_at,
|
|
1692
|
+
stale_cutoff: input.stale_cutoff
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_CONFLICT", `Task ${input.task_id} changed during stale-lock handoff`, { task_id: input.task_id });
|
|
1696
|
+
}
|
|
1697
|
+
var STALE_LOCK_HANDOFF_SCHEMA_VERSION = "todos.stale-lock-handoff.v1", STALE_LOCK_HANDOFF_ACTION = "stale_lock_handoff", STALE_LOCK_HANDOFF_FIELD = "lock", EXACT_TASK_UUID_RE, CANONICAL_LOCK_VERSION_RE, MAX_REASON_LENGTH = 4096;
|
|
1698
|
+
var init_stale_lock_handoff = __esm(() => {
|
|
1699
|
+
init_types();
|
|
1700
|
+
init_creator_identity();
|
|
1701
|
+
init_prewrite_secrets();
|
|
1702
|
+
EXACT_TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1703
|
+
CANONICAL_LOCK_VERSION_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1186
1706
|
// src/lib/slugs.ts
|
|
1187
1707
|
function normalizeSlug(value) {
|
|
1188
1708
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -1849,181 +2369,56 @@ var init_integrity = __esm(() => {
|
|
|
1849
2369
|
};
|
|
1850
2370
|
});
|
|
1851
2371
|
|
|
1852
|
-
// src/
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
function loadConfig() {
|
|
1859
|
-
if (cached)
|
|
1860
|
-
return cached;
|
|
1861
|
-
if (!existsSync3(getConfigPath())) {
|
|
1862
|
-
cached = {};
|
|
1863
|
-
return cached;
|
|
1864
|
-
}
|
|
1865
|
-
const config = readJsonFile(getConfigPath()) || {};
|
|
1866
|
-
if (typeof config.sync_agents === "string") {
|
|
1867
|
-
config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1868
|
-
}
|
|
1869
|
-
cached = config;
|
|
1870
|
-
return cached;
|
|
1871
|
-
}
|
|
1872
|
-
function saveConfig(config) {
|
|
1873
|
-
const configPath = getConfigPath();
|
|
1874
|
-
ensureDir(dirname(configPath));
|
|
1875
|
-
writeJsonFile(configPath, config);
|
|
1876
|
-
cached = config;
|
|
1877
|
-
return config;
|
|
1878
|
-
}
|
|
1879
|
-
function getAgentPoolForProject(workingDir) {
|
|
1880
|
-
const config = loadConfig();
|
|
1881
|
-
if (workingDir && config.project_pools) {
|
|
1882
|
-
let bestKey = null;
|
|
1883
|
-
let bestLen = 0;
|
|
1884
|
-
for (const key of Object.keys(config.project_pools)) {
|
|
1885
|
-
if (workingDir.startsWith(key) && key.length > bestLen) {
|
|
1886
|
-
bestKey = key;
|
|
1887
|
-
bestLen = key.length;
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
if (bestKey && config.project_pools[bestKey]) {
|
|
1891
|
-
return config.project_pools[bestKey];
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
return config.agent_pool || null;
|
|
1895
|
-
}
|
|
1896
|
-
function getCompletionGuardConfig(projectPath) {
|
|
1897
|
-
const config = loadConfig();
|
|
1898
|
-
const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
|
|
1899
|
-
if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
|
|
1900
|
-
return { ...global, ...config.project_overrides[projectPath].completion_guard };
|
|
1901
|
-
}
|
|
1902
|
-
return global;
|
|
1903
|
-
}
|
|
1904
|
-
var cached = null, GUARD_DEFAULTS;
|
|
1905
|
-
var init_config2 = __esm(() => {
|
|
1906
|
-
init_sync_utils();
|
|
1907
|
-
GUARD_DEFAULTS = {
|
|
1908
|
-
enabled: false,
|
|
1909
|
-
min_work_seconds: 30,
|
|
1910
|
-
max_completions_per_window: 5,
|
|
1911
|
-
window_minutes: 10,
|
|
1912
|
-
cooldown_seconds: 60
|
|
1913
|
-
};
|
|
1914
|
-
});
|
|
1915
|
-
|
|
1916
|
-
// src/lib/redaction.ts
|
|
1917
|
-
function unique(values) {
|
|
1918
|
-
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
1919
|
-
}
|
|
1920
|
-
function cloneRegex(regex) {
|
|
1921
|
-
return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
|
|
1922
|
-
}
|
|
1923
|
-
function customPatterns() {
|
|
1924
|
-
return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
|
|
1925
|
-
try {
|
|
1926
|
-
return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
|
|
1927
|
-
} catch {
|
|
1928
|
-
return [];
|
|
1929
|
-
}
|
|
2372
|
+
// src/storage/audit-history-import.ts
|
|
2373
|
+
function auditHistoryRowsAreFieldIdentical(left, right) {
|
|
2374
|
+
return AUDIT_HISTORY_FIELDS.every((field) => {
|
|
2375
|
+
const leftValue = field === "machine_id" ? left[field] ?? null : left[field];
|
|
2376
|
+
const rightValue = field === "machine_id" ? right[field] ?? null : right[field];
|
|
2377
|
+
return leftValue === rightValue;
|
|
1930
2378
|
});
|
|
1931
2379
|
}
|
|
1932
|
-
function
|
|
1933
|
-
return
|
|
2380
|
+
function divergentAuditHistoryReplayError(id) {
|
|
2381
|
+
return `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row ${id} differs from stored row`;
|
|
1934
2382
|
}
|
|
1935
|
-
function
|
|
1936
|
-
|
|
1937
|
-
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(trimmed) || new RegExp(`=\\s*['"]?${REDACTION_PLACEHOLDER}['"]?$`).test(trimmed);
|
|
2383
|
+
function forbiddenAuditHistoryTombstoneError(id) {
|
|
2384
|
+
return `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone ${id} is not allowed`;
|
|
1938
2385
|
}
|
|
1939
|
-
function
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
|
|
1950
|
-
}
|
|
1951
|
-
function redactEvidenceText(value) {
|
|
1952
|
-
let redacted = value;
|
|
1953
|
-
for (const pattern of secretPatterns()) {
|
|
1954
|
-
const regex = cloneRegex(pattern.regex);
|
|
1955
|
-
const replacement = pattern.replacement ?? "[REDACTED]";
|
|
1956
|
-
redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
|
|
1957
|
-
}
|
|
1958
|
-
return redacted;
|
|
1959
|
-
}
|
|
1960
|
-
function redactValue(value) {
|
|
1961
|
-
if (typeof value === "string")
|
|
1962
|
-
return redactEvidenceText(value);
|
|
1963
|
-
if (Array.isArray(value))
|
|
1964
|
-
return value.map(redactValue);
|
|
1965
|
-
if (value && typeof value === "object") {
|
|
1966
|
-
const redacted = {};
|
|
1967
|
-
for (const [key, child] of Object.entries(value)) {
|
|
1968
|
-
if (isSecretKey(key)) {
|
|
1969
|
-
redacted[key] = "[REDACTED]";
|
|
1970
|
-
} else {
|
|
1971
|
-
redacted[key] = redactValue(child);
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
return redacted;
|
|
2386
|
+
function parseAuditHistoryImportFailure(message) {
|
|
2387
|
+
const divergentPrefix = `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row `;
|
|
2388
|
+
const divergentSuffix = " differs from stored row";
|
|
2389
|
+
if (message.startsWith(divergentPrefix) && message.endsWith(divergentSuffix)) {
|
|
2390
|
+
return {
|
|
2391
|
+
code: AUDIT_HISTORY_DIVERGENT_REPLAY,
|
|
2392
|
+
auditHistoryId: message.slice(divergentPrefix.length, -divergentSuffix.length),
|
|
2393
|
+
conflict: true,
|
|
2394
|
+
status: 409
|
|
2395
|
+
};
|
|
1975
2396
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2397
|
+
const tombstonePrefix = `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone `;
|
|
2398
|
+
const tombstoneSuffix = " is not allowed";
|
|
2399
|
+
if (message.startsWith(tombstonePrefix) && message.endsWith(tombstoneSuffix)) {
|
|
2400
|
+
return {
|
|
2401
|
+
code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
|
|
2402
|
+
auditHistoryId: message.slice(tombstonePrefix.length, -tombstoneSuffix.length),
|
|
2403
|
+
conflict: false,
|
|
2404
|
+
status: 400
|
|
2405
|
+
};
|
|
1984
2406
|
}
|
|
1985
|
-
return
|
|
1986
|
-
}
|
|
1987
|
-
function getSecretSafetyConfig() {
|
|
1988
|
-
return {
|
|
1989
|
-
redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
|
|
1990
|
-
redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
|
|
1991
|
-
};
|
|
1992
|
-
}
|
|
1993
|
-
function upsertSecretSafetyConfig(input) {
|
|
1994
|
-
const config = loadConfig();
|
|
1995
|
-
const next = {
|
|
1996
|
-
redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
|
|
1997
|
-
redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
|
|
1998
|
-
};
|
|
1999
|
-
saveConfig({ ...config, secret_safety: next });
|
|
2000
|
-
return next;
|
|
2407
|
+
return null;
|
|
2001
2408
|
}
|
|
2002
|
-
var
|
|
2003
|
-
var
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2409
|
+
var AUDIT_HISTORY_DIVERGENT_REPLAY = "AUDIT_HISTORY_DIVERGENT_REPLAY", AUDIT_HISTORY_TOMBSTONE_FORBIDDEN = "AUDIT_HISTORY_TOMBSTONE_FORBIDDEN", AUDIT_HISTORY_FIELDS;
|
|
2410
|
+
var init_audit_history_import = __esm(() => {
|
|
2411
|
+
AUDIT_HISTORY_FIELDS = [
|
|
2412
|
+
"id",
|
|
2413
|
+
"task_id",
|
|
2414
|
+
"action",
|
|
2415
|
+
"field",
|
|
2416
|
+
"old_value",
|
|
2417
|
+
"new_value",
|
|
2418
|
+
"agent_id",
|
|
2419
|
+
"created_at",
|
|
2420
|
+
"machine_id"
|
|
2014
2421
|
];
|
|
2015
|
-
DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
|
|
2016
|
-
NON_SECRET_USAGE_KEYS = new Set([
|
|
2017
|
-
"tokens",
|
|
2018
|
-
"total_tokens",
|
|
2019
|
-
"token_count",
|
|
2020
|
-
"input_tokens",
|
|
2021
|
-
"output_tokens",
|
|
2022
|
-
"prompt_tokens",
|
|
2023
|
-
"completion_tokens",
|
|
2024
|
-
"cost_tokens"
|
|
2025
|
-
]);
|
|
2026
|
-
REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
2027
2422
|
});
|
|
2028
2423
|
|
|
2029
2424
|
// src/storage/postgres-adapter.ts
|
|
@@ -2056,6 +2451,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
2056
2451
|
getChangedSince: (since, filters) => getChangedSince(since, filters, store),
|
|
2057
2452
|
lock: (id, agentId) => lockTask(id, agentId, store),
|
|
2058
2453
|
unlock: (id, agentId) => unlockTask(id, agentId, store),
|
|
2454
|
+
handoffStaleLock: (input, context) => store.handoffStaleLock(input, context),
|
|
2059
2455
|
getByFingerprint: (fingerprint) => store.getTaskByFingerprint(fingerprint)
|
|
2060
2456
|
},
|
|
2061
2457
|
dependencies: {
|
|
@@ -2092,6 +2488,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
2092
2488
|
get: (id) => store.get("plans", id),
|
|
2093
2489
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
2094
2490
|
update: (id, input) => updatePlan(id, input, store),
|
|
2491
|
+
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
2095
2492
|
delete: (id, context) => store.deletePlan(id, context)
|
|
2096
2493
|
},
|
|
2097
2494
|
planProjectLinks: {
|
|
@@ -2201,6 +2598,91 @@ class PostgresJsonRecordStore {
|
|
|
2201
2598
|
LIMIT 1`, [this.service, type, id]);
|
|
2202
2599
|
return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
|
|
2203
2600
|
}
|
|
2601
|
+
async handoffStaleLock(input, context = {}) {
|
|
2602
|
+
const prepared = prepareStaleLockHandoff(input);
|
|
2603
|
+
const receipt = buildStaleLockHandoffReceipt(prepared);
|
|
2604
|
+
const history = staleLockHandoffHistory(receipt, this.machineId(context));
|
|
2605
|
+
await this.ensureSchema();
|
|
2606
|
+
const result = await this.options.client.query(`/* todos:stale-lock-handoff-atomic */ WITH
|
|
2607
|
+
target AS MATERIALIZED (
|
|
2608
|
+
SELECT payload
|
|
2609
|
+
FROM ${this.tableName}
|
|
2610
|
+
WHERE service = $1
|
|
2611
|
+
AND object_type = 'tasks'
|
|
2612
|
+
AND object_id = $2
|
|
2613
|
+
AND deleted_at IS NULL
|
|
2614
|
+
FOR UPDATE
|
|
2615
|
+
),
|
|
2616
|
+
updated AS (
|
|
2617
|
+
UPDATE ${this.tableName} AS task_record
|
|
2618
|
+
SET payload = jsonb_set(
|
|
2619
|
+
jsonb_set(
|
|
2620
|
+
jsonb_set(
|
|
2621
|
+
jsonb_set(
|
|
2622
|
+
task_record.payload,
|
|
2623
|
+
'{locked_by}',
|
|
2624
|
+
to_jsonb($6::text),
|
|
2625
|
+
true
|
|
2626
|
+
),
|
|
2627
|
+
'{locked_at}',
|
|
2628
|
+
to_jsonb($7::text),
|
|
2629
|
+
true
|
|
2630
|
+
),
|
|
2631
|
+
'{updated_at}',
|
|
2632
|
+
to_jsonb($7::text),
|
|
2633
|
+
true
|
|
2634
|
+
),
|
|
2635
|
+
'{version}',
|
|
2636
|
+
to_jsonb(COALESCE((task_record.payload->>'version')::integer, 0) + 1),
|
|
2637
|
+
true
|
|
2638
|
+
),
|
|
2639
|
+
updated_at = $7::timestamptz,
|
|
2640
|
+
source_machine_id = $10,
|
|
2641
|
+
version = COALESCE(task_record.version, 0) + 1
|
|
2642
|
+
FROM target
|
|
2643
|
+
WHERE task_record.service = $1
|
|
2644
|
+
AND task_record.object_type = 'tasks'
|
|
2645
|
+
AND task_record.object_id = $2
|
|
2646
|
+
AND task_record.deleted_at IS NULL
|
|
2647
|
+
AND target.payload->>'locked_by' = $3
|
|
2648
|
+
AND target.payload->>'locked_at' = $4
|
|
2649
|
+
AND todos_try_timestamptz(target.payload->>'locked_at') < $5::timestamptz
|
|
2650
|
+
AND COALESCE(target.payload->>'status', '') NOT IN ('completed', 'failed', 'cancelled')
|
|
2651
|
+
RETURNING task_record.payload
|
|
2652
|
+
),
|
|
2653
|
+
audit AS (
|
|
2654
|
+
INSERT INTO ${this.tableName} (
|
|
2655
|
+
service, object_type, object_id, payload, updated_at,
|
|
2656
|
+
deleted_at, source_machine_id, version
|
|
2657
|
+
)
|
|
2658
|
+
SELECT $1, 'audit_history', $8, $9::jsonb, $7::timestamptz,
|
|
2659
|
+
NULL, $10, NULL
|
|
2660
|
+
FROM updated
|
|
2661
|
+
RETURNING payload
|
|
2662
|
+
)
|
|
2663
|
+
SELECT
|
|
2664
|
+
(SELECT payload FROM target) AS current_payload,
|
|
2665
|
+
(SELECT payload FROM updated) AS updated_payload,
|
|
2666
|
+
(SELECT payload FROM audit) AS audit_payload`, [
|
|
2667
|
+
this.service,
|
|
2668
|
+
prepared.task_id,
|
|
2669
|
+
prepared.expected_holder,
|
|
2670
|
+
prepared.expected_lock_version,
|
|
2671
|
+
prepared.stale_cutoff,
|
|
2672
|
+
prepared.new_holder,
|
|
2673
|
+
prepared.operation_timestamp,
|
|
2674
|
+
receipt.receipt_id,
|
|
2675
|
+
jsonbParam(history),
|
|
2676
|
+
this.machineId(context)
|
|
2677
|
+
]);
|
|
2678
|
+
const row = result.rows[0];
|
|
2679
|
+
if (!row?.current_payload)
|
|
2680
|
+
throw new TaskNotFoundError(prepared.task_id);
|
|
2681
|
+
if (!row.updated_payload || !row.audit_payload) {
|
|
2682
|
+
throwStaleLockHandoffConflict(payloadRecord2(row.current_payload), prepared);
|
|
2683
|
+
}
|
|
2684
|
+
return receipt;
|
|
2685
|
+
}
|
|
2204
2686
|
async list(type) {
|
|
2205
2687
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
2206
2688
|
}
|
|
@@ -2470,6 +2952,28 @@ class PostgresJsonRecordStore {
|
|
|
2470
2952
|
}
|
|
2471
2953
|
return value;
|
|
2472
2954
|
}
|
|
2955
|
+
async insertImmutableAuditHistory(value, context = {}) {
|
|
2956
|
+
await this.ensureSchema();
|
|
2957
|
+
const inserted = await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
2958
|
+
service, object_type, object_id, payload, updated_at,
|
|
2959
|
+
deleted_at, source_machine_id, version
|
|
2960
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, NULL)
|
|
2961
|
+
ON CONFLICT (service, object_type, object_id) DO NOTHING
|
|
2962
|
+
RETURNING object_id`, [
|
|
2963
|
+
this.service,
|
|
2964
|
+
"audit_history",
|
|
2965
|
+
value.id,
|
|
2966
|
+
jsonbParam(value),
|
|
2967
|
+
value.created_at,
|
|
2968
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
2969
|
+
]);
|
|
2970
|
+
if (inserted.rows.length > 0)
|
|
2971
|
+
return "inserted";
|
|
2972
|
+
const existing = await this.get("audit_history", value.id);
|
|
2973
|
+
if (existing && auditHistoryRowsAreFieldIdentical(existing, value))
|
|
2974
|
+
return "identical";
|
|
2975
|
+
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
2976
|
+
}
|
|
2473
2977
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
|
|
2474
2978
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
2475
2979
|
if (planIds.length === 0)
|
|
@@ -2579,6 +3083,54 @@ class PostgresJsonRecordStore {
|
|
|
2579
3083
|
throw new PlanNotFoundError(value.id);
|
|
2580
3084
|
return payloadRecord2(row.payload);
|
|
2581
3085
|
}
|
|
3086
|
+
async completePlanAtRevision(id, expectedUpdatedAt, context = {}) {
|
|
3087
|
+
await this.ensureSchema();
|
|
3088
|
+
const result = await this.options.client.query(`/* todos:complete-plan-revision-cas */ WITH next_clock AS (
|
|
3089
|
+
SELECT date_trunc(
|
|
3090
|
+
'milliseconds',
|
|
3091
|
+
GREATEST(clock_timestamp(), ($3::text)::timestamptz + interval '2 milliseconds')
|
|
3092
|
+
) AS completed_at
|
|
3093
|
+
), stored AS (
|
|
3094
|
+
UPDATE ${this.tableName} AS record SET
|
|
3095
|
+
payload = record.payload || jsonb_build_object(
|
|
3096
|
+
'status', 'completed',
|
|
3097
|
+
'updated_at', to_char(
|
|
3098
|
+
next_clock.completed_at AT TIME ZONE 'UTC',
|
|
3099
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
3100
|
+
)
|
|
3101
|
+
),
|
|
3102
|
+
updated_at = next_clock.completed_at,
|
|
3103
|
+
deleted_at = NULL,
|
|
3104
|
+
source_machine_id = COALESCE($4, record.source_machine_id),
|
|
3105
|
+
version = COALESCE(record.version, 0) + 1
|
|
3106
|
+
FROM next_clock
|
|
3107
|
+
WHERE record.service = $1
|
|
3108
|
+
AND record.object_type = 'plans'
|
|
3109
|
+
AND record.object_id = $2
|
|
3110
|
+
AND record.deleted_at IS NULL
|
|
3111
|
+
AND record.payload->>'updated_at' = $3::text
|
|
3112
|
+
AND record.payload->>'status' IS DISTINCT FROM 'completed'
|
|
3113
|
+
RETURNING record.payload
|
|
3114
|
+
)
|
|
3115
|
+
SELECT payload FROM stored`, [
|
|
3116
|
+
this.service,
|
|
3117
|
+
id,
|
|
3118
|
+
expectedUpdatedAt,
|
|
3119
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
3120
|
+
]);
|
|
3121
|
+
const payload = result.rows[0]?.payload;
|
|
3122
|
+
if (payload)
|
|
3123
|
+
return { plan: payloadRecord2(payload), applied: true };
|
|
3124
|
+
const current = await this.get("plans", id);
|
|
3125
|
+
if (!current)
|
|
3126
|
+
throw new PlanNotFoundError(id);
|
|
3127
|
+
if (current.updated_at !== expectedUpdatedAt) {
|
|
3128
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
3129
|
+
}
|
|
3130
|
+
if (current.status === "completed")
|
|
3131
|
+
return { plan: current, applied: false };
|
|
3132
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
3133
|
+
}
|
|
2582
3134
|
async createTemplateWithTasks(template, tasks, context = {}) {
|
|
2583
3135
|
await this.ensureSchema();
|
|
2584
3136
|
const records = [
|
|
@@ -3910,6 +4462,11 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
3910
4462
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
3911
4463
|
if (result.errors.length > 0)
|
|
3912
4464
|
return result;
|
|
4465
|
+
const auditHistory = await preflightAuditHistoryImport(snapshot.auditHistory, snapshot.tombstones ?? [], store);
|
|
4466
|
+
result.errors.push(...auditHistory.errors);
|
|
4467
|
+
if (result.errors.length > 0)
|
|
4468
|
+
return result;
|
|
4469
|
+
result.skipped += auditHistory.identical;
|
|
3913
4470
|
const entries = [
|
|
3914
4471
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
3915
4472
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
@@ -3918,9 +4475,20 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
3918
4475
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
3919
4476
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
3920
4477
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
3921
|
-
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
3922
|
-
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
4478
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
3923
4479
|
];
|
|
4480
|
+
for (const row of auditHistory.rowsToInsert) {
|
|
4481
|
+
try {
|
|
4482
|
+
const outcome = await store.insertImmutableAuditHistory(row, context);
|
|
4483
|
+
if (outcome === "inserted")
|
|
4484
|
+
result.inserted += 1;
|
|
4485
|
+
else
|
|
4486
|
+
result.skipped += 1;
|
|
4487
|
+
} catch (error) {
|
|
4488
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
4489
|
+
return result;
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
3924
4492
|
for (const [type, row] of entries) {
|
|
3925
4493
|
try {
|
|
3926
4494
|
const existing = await store.get(type, row.id);
|
|
@@ -3954,6 +4522,32 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
3954
4522
|
}
|
|
3955
4523
|
return result;
|
|
3956
4524
|
}
|
|
4525
|
+
async function preflightAuditHistoryImport(rows, tombstones, store) {
|
|
4526
|
+
const errors = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
|
|
4527
|
+
const rowsToInsert = [];
|
|
4528
|
+
const seen = new Map;
|
|
4529
|
+
let identical = 0;
|
|
4530
|
+
for (const row of rows) {
|
|
4531
|
+
const prior = seen.get(row.id);
|
|
4532
|
+
if (prior) {
|
|
4533
|
+
if (auditHistoryRowsAreFieldIdentical(prior, row))
|
|
4534
|
+
identical += 1;
|
|
4535
|
+
else
|
|
4536
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
4537
|
+
continue;
|
|
4538
|
+
}
|
|
4539
|
+
seen.set(row.id, row);
|
|
4540
|
+
const existing = await store.get("audit_history", row.id);
|
|
4541
|
+
if (!existing) {
|
|
4542
|
+
rowsToInsert.push(row);
|
|
4543
|
+
} else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
4544
|
+
identical += 1;
|
|
4545
|
+
} else {
|
|
4546
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
return { rowsToInsert, identical, errors };
|
|
4550
|
+
}
|
|
3957
4551
|
async function requireRecord(type, id, store) {
|
|
3958
4552
|
const record = await store.get(type, id);
|
|
3959
4553
|
if (!record)
|
|
@@ -4060,9 +4654,11 @@ var init_postgres_adapter = __esm(() => {
|
|
|
4060
4654
|
init_types();
|
|
4061
4655
|
init_creator_identity();
|
|
4062
4656
|
init_plan_project_link_contract();
|
|
4657
|
+
init_stale_lock_handoff();
|
|
4063
4658
|
init_postgres_sync();
|
|
4064
4659
|
init_integrity();
|
|
4065
4660
|
init_redaction();
|
|
4661
|
+
init_audit_history_import();
|
|
4066
4662
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
4067
4663
|
});
|
|
4068
4664
|
|
|
@@ -12771,185 +13367,6 @@ var init_shared_events = __esm(() => {
|
|
|
12771
13367
|
init_event_emission_safety();
|
|
12772
13368
|
});
|
|
12773
13369
|
|
|
12774
|
-
// src/lib/secret-redaction.ts
|
|
12775
|
-
function isAllowlisted(text, match, allowlist) {
|
|
12776
|
-
const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
|
|
12777
|
-
return allowlist.some((re) => re.test(context) || re.test(match));
|
|
12778
|
-
}
|
|
12779
|
-
function scanTextForSecrets(text, options = {}) {
|
|
12780
|
-
const allowlist = [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []];
|
|
12781
|
-
const matches = [];
|
|
12782
|
-
const patterns = [
|
|
12783
|
-
...DEFAULT_PATTERNS,
|
|
12784
|
-
...options.custom_patterns?.map((p, i) => ({ name: `custom_${i}`, pattern: p })) ?? []
|
|
12785
|
-
];
|
|
12786
|
-
for (const { name, pattern, allowlist_ok } of patterns) {
|
|
12787
|
-
const re = new RegExp(pattern.source, pattern.flags);
|
|
12788
|
-
let m;
|
|
12789
|
-
while ((m = re.exec(text)) !== null) {
|
|
12790
|
-
const match = m[0];
|
|
12791
|
-
if (allowlist_ok && isAllowlisted(text, match, allowlist))
|
|
12792
|
-
continue;
|
|
12793
|
-
if (isAllowlisted(text, match, allowlist))
|
|
12794
|
-
continue;
|
|
12795
|
-
const line = text.slice(0, m.index).split(`
|
|
12796
|
-
`).length;
|
|
12797
|
-
matches.push({ pattern: name, match: match.slice(0, 12) + (match.length > 12 ? "\u2026" : ""), index: m.index, line });
|
|
12798
|
-
}
|
|
12799
|
-
}
|
|
12800
|
-
return {
|
|
12801
|
-
schema_version: SECRET_REDACTION_SCHEMA,
|
|
12802
|
-
clean: matches.length === 0,
|
|
12803
|
-
matches
|
|
12804
|
-
};
|
|
12805
|
-
}
|
|
12806
|
-
function redactText(text, options = {}) {
|
|
12807
|
-
const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
|
|
12808
|
-
let out = text;
|
|
12809
|
-
for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
|
|
12810
|
-
out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
|
|
12811
|
-
if (allowlist_ok && isAllowlisted(out, match, [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []])) {
|
|
12812
|
-
return match;
|
|
12813
|
-
}
|
|
12814
|
-
return placeholder;
|
|
12815
|
-
});
|
|
12816
|
-
}
|
|
12817
|
-
for (const custom of options.custom_patterns ?? []) {
|
|
12818
|
-
out = out.replace(custom, placeholder);
|
|
12819
|
-
}
|
|
12820
|
-
for (const fn of customRedactors) {
|
|
12821
|
-
out = fn(out);
|
|
12822
|
-
}
|
|
12823
|
-
return out;
|
|
12824
|
-
}
|
|
12825
|
-
function redactExportRecord(record) {
|
|
12826
|
-
const base = redactValue(record);
|
|
12827
|
-
for (const [key, value] of Object.entries(base)) {
|
|
12828
|
-
if (typeof value === "string") {
|
|
12829
|
-
base[key] = redactText(value);
|
|
12830
|
-
}
|
|
12831
|
-
}
|
|
12832
|
-
return base;
|
|
12833
|
-
}
|
|
12834
|
-
var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
|
|
12835
|
-
var init_secret_redaction = __esm(() => {
|
|
12836
|
-
init_redaction();
|
|
12837
|
-
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
12838
|
-
DEFAULT_PATTERNS = [
|
|
12839
|
-
{ name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
|
|
12840
|
-
{ name: "github_pat", pattern: /\bghp_[a-zA-Z0-9]{20,}\b/g },
|
|
12841
|
-
{ name: "github_oauth", pattern: /\bgho_[a-zA-Z0-9]{20,}\b/g },
|
|
12842
|
-
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
12843
|
-
{ name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
|
|
12844
|
-
{ name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
12845
|
-
{ name: "private_key_block", pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
12846
|
-
{ name: "generic_api_key", pattern: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[a-zA-Z0-9\-._]{8,}['"]?/gi, allowlist_ok: true }
|
|
12847
|
-
];
|
|
12848
|
-
DEFAULT_ALLOWLIST = [
|
|
12849
|
-
/\[REDACTED\]/,
|
|
12850
|
-
/example\.com/i,
|
|
12851
|
-
/your-api-key-here/i,
|
|
12852
|
-
/sk-test/i,
|
|
12853
|
-
/ghp_xxx/i
|
|
12854
|
-
];
|
|
12855
|
-
customRedactors = [];
|
|
12856
|
-
});
|
|
12857
|
-
|
|
12858
|
-
// src/lib/prewrite-secrets.ts
|
|
12859
|
-
function mergeFindings(findings) {
|
|
12860
|
-
const byPattern = new Map;
|
|
12861
|
-
for (const finding of findings) {
|
|
12862
|
-
const existing = byPattern.get(finding.pattern);
|
|
12863
|
-
if (!existing) {
|
|
12864
|
-
byPattern.set(finding.pattern, {
|
|
12865
|
-
pattern: finding.pattern,
|
|
12866
|
-
count: finding.count,
|
|
12867
|
-
lines: Array.from(new Set(finding.lines)).sort((a, b) => a - b)
|
|
12868
|
-
});
|
|
12869
|
-
continue;
|
|
12870
|
-
}
|
|
12871
|
-
existing.count += finding.count;
|
|
12872
|
-
existing.lines = Array.from(new Set([...existing.lines, ...finding.lines])).sort((a, b) => a - b);
|
|
12873
|
-
}
|
|
12874
|
-
return [...byPattern.values()].sort((left, right) => left.pattern.localeCompare(right.pattern));
|
|
12875
|
-
}
|
|
12876
|
-
function redactPreWriteText(value) {
|
|
12877
|
-
return redactText(redactEvidenceText(value));
|
|
12878
|
-
}
|
|
12879
|
-
function scanPreWriteText(value, context = "text") {
|
|
12880
|
-
const redacted = redactPreWriteText(value);
|
|
12881
|
-
if (redacted === value) {
|
|
12882
|
-
return {
|
|
12883
|
-
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
12884
|
-
clean: true,
|
|
12885
|
-
context,
|
|
12886
|
-
findings: []
|
|
12887
|
-
};
|
|
12888
|
-
}
|
|
12889
|
-
const structural = scanTextForSecrets(value).matches.map((match) => ({
|
|
12890
|
-
pattern: match.pattern,
|
|
12891
|
-
count: 1,
|
|
12892
|
-
lines: match.line ? [match.line] : []
|
|
12893
|
-
}));
|
|
12894
|
-
const configured = listSecretFindings(value).map((finding) => ({
|
|
12895
|
-
pattern: finding.pattern,
|
|
12896
|
-
count: finding.count,
|
|
12897
|
-
lines: []
|
|
12898
|
-
}));
|
|
12899
|
-
return {
|
|
12900
|
-
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
12901
|
-
clean: false,
|
|
12902
|
-
context,
|
|
12903
|
-
findings: mergeFindings([...structural, ...configured])
|
|
12904
|
-
};
|
|
12905
|
-
}
|
|
12906
|
-
function assertPreWriteTextClean(value, context = "text") {
|
|
12907
|
-
const scan = scanPreWriteText(value, context);
|
|
12908
|
-
if (!scan.clean)
|
|
12909
|
-
throw new PreWriteSecretError(context, scan.findings);
|
|
12910
|
-
}
|
|
12911
|
-
function sanitizePreWriteText(value, context = "text", options = {}) {
|
|
12912
|
-
const mode = options.mode ?? "redact";
|
|
12913
|
-
if (mode === "block") {
|
|
12914
|
-
assertPreWriteTextClean(value, context);
|
|
12915
|
-
return value;
|
|
12916
|
-
}
|
|
12917
|
-
return redactPreWriteText(value);
|
|
12918
|
-
}
|
|
12919
|
-
function sanitizePreWriteValue(value, context = "value", options = {}) {
|
|
12920
|
-
if (typeof value === "string")
|
|
12921
|
-
return sanitizePreWriteText(value, context, options);
|
|
12922
|
-
if (Array.isArray(value)) {
|
|
12923
|
-
return value.map((item, index) => sanitizePreWriteValue(item, `${context}[${index}]`, options));
|
|
12924
|
-
}
|
|
12925
|
-
if (value && typeof value === "object") {
|
|
12926
|
-
const sanitized = {};
|
|
12927
|
-
for (const [key, child] of Object.entries(value)) {
|
|
12928
|
-
const safeKey = sanitizePreWriteText(key, `${context}.$key`, options);
|
|
12929
|
-
sanitized[safeKey] = sanitizePreWriteValue(child, `${context}.${safeKey}`, options);
|
|
12930
|
-
}
|
|
12931
|
-
return redactValue(sanitized);
|
|
12932
|
-
}
|
|
12933
|
-
return value;
|
|
12934
|
-
}
|
|
12935
|
-
var PREWRITE_SECRET_SCAN_SCHEMA, PreWriteSecretError;
|
|
12936
|
-
var init_prewrite_secrets = __esm(() => {
|
|
12937
|
-
init_redaction();
|
|
12938
|
-
init_secret_redaction();
|
|
12939
|
-
PREWRITE_SECRET_SCAN_SCHEMA = ["todos", "prewrite_secret_scan", "v1"].join(".");
|
|
12940
|
-
PreWriteSecretError = class PreWriteSecretError extends Error {
|
|
12941
|
-
context;
|
|
12942
|
-
findings;
|
|
12943
|
-
constructor(context, findings) {
|
|
12944
|
-
const summary = findings.map((finding) => `${finding.pattern}:${finding.count}`).join(", ");
|
|
12945
|
-
super(`Secret pattern detected before persistence in ${context}: ${summary}`);
|
|
12946
|
-
this.name = "PreWriteSecretError";
|
|
12947
|
-
this.context = context;
|
|
12948
|
-
this.findings = findings;
|
|
12949
|
-
}
|
|
12950
|
-
};
|
|
12951
|
-
});
|
|
12952
|
-
|
|
12953
13370
|
// src/lib/activity-audit.ts
|
|
12954
13371
|
var exports_activity_audit = {};
|
|
12955
13372
|
__export(exports_activity_audit, {
|
|
@@ -13142,28 +13559,55 @@ var init_activity_audit = __esm(() => {
|
|
|
13142
13559
|
function sanitizeHistoryValue(value, context) {
|
|
13143
13560
|
return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
|
|
13144
13561
|
}
|
|
13145
|
-
function
|
|
13562
|
+
function insertTaskHistory(entry2, db) {
|
|
13146
13563
|
const d = db || getDatabase();
|
|
13147
|
-
const
|
|
13148
|
-
|
|
13149
|
-
|
|
13150
|
-
|
|
13151
|
-
|
|
13564
|
+
const safeEntry = {
|
|
13565
|
+
...entry2,
|
|
13566
|
+
field: entry2.field || null,
|
|
13567
|
+
old_value: sanitizeHistoryValue(entry2.old_value, "task_history.old_value"),
|
|
13568
|
+
new_value: sanitizeHistoryValue(entry2.new_value, "task_history.new_value"),
|
|
13569
|
+
agent_id: entry2.agent_id || null,
|
|
13570
|
+
machine_id: entry2.machine_id ?? currentStorageMachineId(d)
|
|
13571
|
+
};
|
|
13152
13572
|
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
|
|
13153
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
13573
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
13574
|
+
safeEntry.id,
|
|
13575
|
+
safeEntry.task_id,
|
|
13576
|
+
safeEntry.action,
|
|
13577
|
+
safeEntry.field,
|
|
13578
|
+
safeEntry.old_value,
|
|
13579
|
+
safeEntry.new_value,
|
|
13580
|
+
safeEntry.agent_id,
|
|
13581
|
+
safeEntry.created_at,
|
|
13582
|
+
safeEntry.machine_id ?? null
|
|
13583
|
+
]);
|
|
13154
13584
|
try {
|
|
13155
13585
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
13156
13586
|
logActivity2({
|
|
13157
13587
|
entity_type: "task",
|
|
13158
|
-
entity_id:
|
|
13159
|
-
action,
|
|
13160
|
-
field,
|
|
13161
|
-
old_value:
|
|
13162
|
-
new_value:
|
|
13163
|
-
actor_id:
|
|
13588
|
+
entity_id: safeEntry.task_id,
|
|
13589
|
+
action: safeEntry.action,
|
|
13590
|
+
field: safeEntry.field ?? undefined,
|
|
13591
|
+
old_value: safeEntry.old_value,
|
|
13592
|
+
new_value: safeEntry.new_value,
|
|
13593
|
+
actor_id: safeEntry.agent_id ?? undefined
|
|
13164
13594
|
}, d);
|
|
13165
13595
|
} catch {}
|
|
13166
|
-
return
|
|
13596
|
+
return safeEntry;
|
|
13597
|
+
}
|
|
13598
|
+
function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
13599
|
+
const d = db || getDatabase();
|
|
13600
|
+
return insertTaskHistory({
|
|
13601
|
+
id: uuid(),
|
|
13602
|
+
task_id: taskId,
|
|
13603
|
+
action,
|
|
13604
|
+
field: field || null,
|
|
13605
|
+
old_value: oldValue ?? null,
|
|
13606
|
+
new_value: newValue ?? null,
|
|
13607
|
+
agent_id: agentId || null,
|
|
13608
|
+
created_at: now(),
|
|
13609
|
+
machine_id: currentStorageMachineId(d)
|
|
13610
|
+
}, d);
|
|
13167
13611
|
}
|
|
13168
13612
|
function getTaskHistory(taskId, db) {
|
|
13169
13613
|
const d = db || getDatabase();
|
|
@@ -14130,6 +14574,7 @@ __export(exports_task_lifecycle, {
|
|
|
14130
14574
|
startTask: () => startTask2,
|
|
14131
14575
|
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
14132
14576
|
lockTask: () => lockTask2,
|
|
14577
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
14133
14578
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
14134
14579
|
getTaskLockStatus: () => getTaskLockStatus,
|
|
14135
14580
|
getStaleTasks: () => getStaleTasks,
|
|
@@ -14398,6 +14843,38 @@ function unlockTask2(id, agentId, db) {
|
|
|
14398
14843
|
WHERE id = ?`, [timestamp2, id]);
|
|
14399
14844
|
return true;
|
|
14400
14845
|
}
|
|
14846
|
+
function handoffStaleTaskLock(input, db) {
|
|
14847
|
+
const d = db || getDatabase();
|
|
14848
|
+
const prepared = prepareStaleLockHandoff(input);
|
|
14849
|
+
const receipt = buildStaleLockHandoffReceipt(prepared);
|
|
14850
|
+
const history = staleLockHandoffHistory(receipt, null);
|
|
14851
|
+
const transfer = d.transaction(() => {
|
|
14852
|
+
const result = d.run(`UPDATE tasks
|
|
14853
|
+
SET locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
14854
|
+
WHERE id = ?
|
|
14855
|
+
AND locked_by = ?
|
|
14856
|
+
AND locked_at = ?
|
|
14857
|
+
AND julianday(locked_at) < julianday(?)
|
|
14858
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')`, [
|
|
14859
|
+
prepared.new_holder,
|
|
14860
|
+
prepared.operation_timestamp,
|
|
14861
|
+
prepared.operation_timestamp,
|
|
14862
|
+
prepared.task_id,
|
|
14863
|
+
prepared.expected_holder,
|
|
14864
|
+
prepared.expected_lock_version,
|
|
14865
|
+
prepared.stale_cutoff
|
|
14866
|
+
]);
|
|
14867
|
+
if (result.changes === 0) {
|
|
14868
|
+
const current = getTask(prepared.task_id, d);
|
|
14869
|
+
if (!current)
|
|
14870
|
+
throw new TaskNotFoundError(prepared.task_id);
|
|
14871
|
+
throwStaleLockHandoffConflict(current, prepared);
|
|
14872
|
+
}
|
|
14873
|
+
insertTaskHistory(history, d);
|
|
14874
|
+
});
|
|
14875
|
+
transfer();
|
|
14876
|
+
return receipt;
|
|
14877
|
+
}
|
|
14401
14878
|
function getTaskLockStatus(id, db) {
|
|
14402
14879
|
const d = db || getDatabase();
|
|
14403
14880
|
const task = getTask(id, d);
|
|
@@ -14698,6 +15175,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
14698
15175
|
init_task_crud();
|
|
14699
15176
|
init_task_graph();
|
|
14700
15177
|
init_prewrite_secrets();
|
|
15178
|
+
init_stale_lock_handoff();
|
|
14701
15179
|
});
|
|
14702
15180
|
|
|
14703
15181
|
// src/db/task-crud.ts
|
|
@@ -16250,6 +16728,48 @@ function updatePlan2(id, input, db) {
|
|
|
16250
16728
|
return updatePlanStored(id, input, d);
|
|
16251
16729
|
})();
|
|
16252
16730
|
}
|
|
16731
|
+
function nextPlanCompletionTimestamp(expectedUpdatedAt) {
|
|
16732
|
+
const expected = Date.parse(expectedUpdatedAt);
|
|
16733
|
+
const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
|
|
16734
|
+
return new Date(Math.max(Date.now(), minimum)).toISOString();
|
|
16735
|
+
}
|
|
16736
|
+
function completePlanAtRevision(id, expectedUpdatedAt, db) {
|
|
16737
|
+
const d = db || getDatabase();
|
|
16738
|
+
return d.transaction(() => {
|
|
16739
|
+
guardPlanRowsSqlite([id], d);
|
|
16740
|
+
const plan = getPlan(id, d);
|
|
16741
|
+
if (!plan)
|
|
16742
|
+
throw new PlanNotFoundError(id);
|
|
16743
|
+
if (plan.updated_at !== expectedUpdatedAt) {
|
|
16744
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
|
|
16745
|
+
}
|
|
16746
|
+
if (plan.status === "completed")
|
|
16747
|
+
return { plan, applied: false };
|
|
16748
|
+
const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
|
|
16749
|
+
const result = d.run(`UPDATE plans
|
|
16750
|
+
SET status = 'completed', updated_at = ?
|
|
16751
|
+
WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
|
|
16752
|
+
if (result.changes !== 1) {
|
|
16753
|
+
const current = getPlan(id, d);
|
|
16754
|
+
if (!current)
|
|
16755
|
+
throw new PlanNotFoundError(id);
|
|
16756
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
16757
|
+
}
|
|
16758
|
+
const completed = getPlan(id, d);
|
|
16759
|
+
emitLocalEventHooksQuiet({
|
|
16760
|
+
type: "plan.updated",
|
|
16761
|
+
payload: {
|
|
16762
|
+
id,
|
|
16763
|
+
old_status: plan.status,
|
|
16764
|
+
new_status: completed.status,
|
|
16765
|
+
name: completed.name,
|
|
16766
|
+
project_id: completed.project_id
|
|
16767
|
+
},
|
|
16768
|
+
databasePath: databasePathFromDatabase(d)
|
|
16769
|
+
});
|
|
16770
|
+
return { plan: completed, applied: true };
|
|
16771
|
+
})();
|
|
16772
|
+
}
|
|
16253
16773
|
function deletePlan(id, db) {
|
|
16254
16774
|
const d = db || getDatabase();
|
|
16255
16775
|
const plan = getPlan(id, d);
|
|
@@ -18228,6 +18748,7 @@ __export(exports_tasks, {
|
|
|
18228
18748
|
insertTaskTags: () => insertTaskTags,
|
|
18229
18749
|
importTaskBoardBundle: () => importTaskBoardBundle,
|
|
18230
18750
|
importCalendarIcs: () => importCalendarIcs,
|
|
18751
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
18231
18752
|
getTimeReport: () => getTimeReport,
|
|
18232
18753
|
getTimeLogs: () => getTimeLogs,
|
|
18233
18754
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
@@ -19200,9 +19721,12 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
19200
19721
|
const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
|
|
19201
19722
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
19202
19723
|
}
|
|
19724
|
+
const auditImport = preflightAuditHistoryImport2(d, snapshot.auditHistory, snapshot.tombstones ?? []);
|
|
19725
|
+
result.errors.push(...auditImport.errors);
|
|
19203
19726
|
if (result.errors.length > 0)
|
|
19204
19727
|
return result;
|
|
19205
|
-
|
|
19728
|
+
result.skipped += auditImport.identicalReplayCount;
|
|
19729
|
+
const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
19206
19730
|
for (const row of rows) {
|
|
19207
19731
|
try {
|
|
19208
19732
|
const record = asRecord(row);
|
|
@@ -19211,7 +19735,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
19211
19735
|
result.skipped += 1;
|
|
19212
19736
|
continue;
|
|
19213
19737
|
}
|
|
19214
|
-
const state = upsertById(d, table, columns, record, updateClockColumn);
|
|
19738
|
+
const state = upsertById(d, table, columns, record, updateClockColumn, acceptEqualClock);
|
|
19215
19739
|
if (state === "inserted")
|
|
19216
19740
|
result.inserted += 1;
|
|
19217
19741
|
else if (state === "updated")
|
|
@@ -19228,19 +19752,81 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
19228
19752
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
19229
19753
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
19230
19754
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
19231
|
-
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
19755
|
+
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at", false);
|
|
19232
19756
|
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
19233
19757
|
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
19234
|
-
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
|
|
19758
|
+
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", true, (row, changed) => {
|
|
19235
19759
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
19236
19760
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
19237
19761
|
}
|
|
19238
19762
|
});
|
|
19239
|
-
|
|
19763
|
+
insertAuditHistoryRows(d, auditImport.rowsToInsert, result);
|
|
19240
19764
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
19241
19765
|
return result;
|
|
19242
19766
|
}
|
|
19243
|
-
function
|
|
19767
|
+
function preflightAuditHistoryImport2(db, rows, tombstones) {
|
|
19768
|
+
const errors = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
|
|
19769
|
+
const rowsToInsert = [];
|
|
19770
|
+
const seen = new Map;
|
|
19771
|
+
let identicalReplayCount = 0;
|
|
19772
|
+
for (const rawRow of rows) {
|
|
19773
|
+
try {
|
|
19774
|
+
const row = asRecord(rawRow);
|
|
19775
|
+
if (typeof row.id !== "string" || !row.id) {
|
|
19776
|
+
throw new Error("task_history row is missing id");
|
|
19777
|
+
}
|
|
19778
|
+
const prior = seen.get(row.id);
|
|
19779
|
+
if (prior) {
|
|
19780
|
+
if (auditHistoryRowsAreFieldIdentical(prior, row))
|
|
19781
|
+
identicalReplayCount += 1;
|
|
19782
|
+
else
|
|
19783
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
19784
|
+
continue;
|
|
19785
|
+
}
|
|
19786
|
+
seen.set(row.id, row);
|
|
19787
|
+
const existing = getAuditHistoryById(db, row.id);
|
|
19788
|
+
if (!existing) {
|
|
19789
|
+
rowsToInsert.push(row);
|
|
19790
|
+
} else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
19791
|
+
identicalReplayCount += 1;
|
|
19792
|
+
} else {
|
|
19793
|
+
errors.push(divergentAuditHistoryReplayError(row.id));
|
|
19794
|
+
}
|
|
19795
|
+
} catch (error) {
|
|
19796
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
19797
|
+
}
|
|
19798
|
+
}
|
|
19799
|
+
return { rowsToInsert, identicalReplayCount, errors };
|
|
19800
|
+
}
|
|
19801
|
+
function insertAuditHistoryRows(db, rows, result) {
|
|
19802
|
+
for (const rawRow of rows) {
|
|
19803
|
+
try {
|
|
19804
|
+
const row = asRecord(rawRow);
|
|
19805
|
+
const presentColumns = AUDIT_COLUMNS.filter((column) => (column in row));
|
|
19806
|
+
if (!presentColumns.includes("id"))
|
|
19807
|
+
presentColumns.unshift("id");
|
|
19808
|
+
const placeholders = presentColumns.map(() => "?").join(", ");
|
|
19809
|
+
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
19810
|
+
const changes = db.run(`INSERT OR IGNORE INTO task_history (${presentColumns.join(", ")}) VALUES (${placeholders})`, values).changes;
|
|
19811
|
+
if (changes > 0) {
|
|
19812
|
+
result.inserted += 1;
|
|
19813
|
+
continue;
|
|
19814
|
+
}
|
|
19815
|
+
const existing = getAuditHistoryById(db, String(row["id"]));
|
|
19816
|
+
if (existing && auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
19817
|
+
result.skipped += 1;
|
|
19818
|
+
} else {
|
|
19819
|
+
result.errors.push(divergentAuditHistoryReplayError(String(row["id"])));
|
|
19820
|
+
}
|
|
19821
|
+
} catch (error) {
|
|
19822
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
19823
|
+
}
|
|
19824
|
+
}
|
|
19825
|
+
}
|
|
19826
|
+
function getAuditHistoryById(db, id) {
|
|
19827
|
+
return db.query(`SELECT ${AUDIT_COLUMNS.join(", ")} FROM task_history WHERE id = ? LIMIT 1`).get(id);
|
|
19828
|
+
}
|
|
19829
|
+
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
19244
19830
|
const id = row["id"];
|
|
19245
19831
|
if (typeof id !== "string" || !id)
|
|
19246
19832
|
throw new Error(`${table} row is missing id`);
|
|
@@ -19252,7 +19838,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
|
|
|
19252
19838
|
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
19253
19839
|
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
19254
19840
|
const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
|
|
19255
|
-
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
19841
|
+
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} ${acceptEqualClock ? "<=" : "<"} excluded.${updateClockColumn}` : "";
|
|
19256
19842
|
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
|
|
19257
19843
|
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
|
|
19258
19844
|
const changes = db.run(sql, values).changes;
|
|
@@ -19337,7 +19923,7 @@ function tableForTombstone(objectType) {
|
|
|
19337
19923
|
return "task_templates";
|
|
19338
19924
|
if (objectType === "template_tasks")
|
|
19339
19925
|
return "template_tasks";
|
|
19340
|
-
|
|
19926
|
+
throw new Error(`unsupported storage tombstone object_type: ${String(objectType)}`);
|
|
19341
19927
|
}
|
|
19342
19928
|
function listRows(db, table, columns) {
|
|
19343
19929
|
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
@@ -19384,6 +19970,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
19384
19970
|
init_tasks();
|
|
19385
19971
|
init_templates();
|
|
19386
19972
|
init_storage_tombstones();
|
|
19973
|
+
init_audit_history_import();
|
|
19387
19974
|
PROJECT_COLUMNS = [
|
|
19388
19975
|
"id",
|
|
19389
19976
|
"name",
|
|
@@ -19648,6 +20235,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19648
20235
|
unlockTask2(id, agentId, database());
|
|
19649
20236
|
return true;
|
|
19650
20237
|
},
|
|
20238
|
+
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
19651
20239
|
delete: (id) => deleteTask(id, database()),
|
|
19652
20240
|
start: (id, agentId) => startTask2(id, agentId, database()),
|
|
19653
20241
|
complete: (id, agentId, options2) => completeTask2(id, agentId, database(), options2),
|
|
@@ -19671,6 +20259,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19671
20259
|
get: (id) => getPlan(id, database()),
|
|
19672
20260
|
list: (projectId) => listPlans(projectId, database()),
|
|
19673
20261
|
update: (id, input) => updatePlan2(id, input, database()),
|
|
20262
|
+
completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
|
|
19674
20263
|
delete: (id) => deletePlan(id, database())
|
|
19675
20264
|
},
|
|
19676
20265
|
planProjectLinks: {
|
|
@@ -29025,6 +29614,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29025
29614
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
29026
29615
|
ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
|
|
29027
29616
|
TaskComment: taskCommentSchema,
|
|
29617
|
+
StaleLockHandoffInput: staleLockHandoffInputSchema,
|
|
29618
|
+
StaleLockHandoffReceipt: staleLockHandoffReceiptSchema,
|
|
29028
29619
|
TaskGitRef: taskGitRefSchema,
|
|
29029
29620
|
Plan: planSchema,
|
|
29030
29621
|
PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
|
|
@@ -30232,6 +30823,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30232
30823
|
}
|
|
30233
30824
|
}
|
|
30234
30825
|
},
|
|
30826
|
+
"/v1/tasks/{id}/stale-lock-handoff": {
|
|
30827
|
+
post: {
|
|
30828
|
+
operationId: "handoffStaleTaskLock",
|
|
30829
|
+
summary: "Atomically transfer one exact stale task lock",
|
|
30830
|
+
description: "Compares one full task UUID, current holder, and exact locked_at version, verifies the lock is strictly older than the supplied threshold, then transfers it directly and writes an immutable task-history receipt in the same backend transaction.",
|
|
30831
|
+
parameters: [
|
|
30832
|
+
{
|
|
30833
|
+
name: "id",
|
|
30834
|
+
in: "path",
|
|
30835
|
+
required: true,
|
|
30836
|
+
schema: { type: "string", format: "uuid" },
|
|
30837
|
+
description: "Exact full task UUID. Short ids and prefixes are rejected."
|
|
30838
|
+
}
|
|
30839
|
+
],
|
|
30840
|
+
requestBody: {
|
|
30841
|
+
required: true,
|
|
30842
|
+
content: {
|
|
30843
|
+
"application/json": {
|
|
30844
|
+
schema: { $ref: "#/components/schemas/StaleLockHandoffInput" }
|
|
30845
|
+
}
|
|
30846
|
+
}
|
|
30847
|
+
},
|
|
30848
|
+
responses: {
|
|
30849
|
+
"200": {
|
|
30850
|
+
content: {
|
|
30851
|
+
"application/json": {
|
|
30852
|
+
schema: {
|
|
30853
|
+
type: "object",
|
|
30854
|
+
additionalProperties: false,
|
|
30855
|
+
required: ["receipt"],
|
|
30856
|
+
properties: {
|
|
30857
|
+
receipt: { $ref: "#/components/schemas/StaleLockHandoffReceipt" }
|
|
30858
|
+
}
|
|
30859
|
+
}
|
|
30860
|
+
}
|
|
30861
|
+
}
|
|
30862
|
+
},
|
|
30863
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
30864
|
+
"403": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
30865
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
30866
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
30867
|
+
"501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
30868
|
+
}
|
|
30869
|
+
}
|
|
30870
|
+
},
|
|
30235
30871
|
"/v1/tasks/{id}/refs": {
|
|
30236
30872
|
get: {
|
|
30237
30873
|
operationId: "listTaskGitRefs",
|
|
@@ -30738,8 +31374,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30738
31374
|
"/v1/import": {
|
|
30739
31375
|
post: {
|
|
30740
31376
|
operationId: "importSnapshot",
|
|
30741
|
-
summary: "Bulk-ingest a
|
|
30742
|
-
description: "Upserts every record
|
|
31377
|
+
summary: "Bulk-ingest a snapshot or atomically complete one observed plan",
|
|
31378
|
+
description: "Upserts every snapshot record by primary key, or accepts exactly one planCompletions operation that changes only plan status under an expected_updated_at CAS. Snapshot records and planCompletions are mutually exclusive. Requires the todos:write scope.",
|
|
30743
31379
|
requestBody: {
|
|
30744
31380
|
required: true,
|
|
30745
31381
|
content: {
|
|
@@ -30758,7 +31394,22 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30758
31394
|
templates: { type: "array", items: { type: "object" } },
|
|
30759
31395
|
templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
|
|
30760
31396
|
auditHistory: { type: "array", items: { type: "object" } },
|
|
30761
|
-
tombstones: { type: "array", items: { type: "object" } }
|
|
31397
|
+
tombstones: { type: "array", items: { type: "object" } },
|
|
31398
|
+
planCompletions: {
|
|
31399
|
+
type: "array",
|
|
31400
|
+
minItems: 1,
|
|
31401
|
+
maxItems: 1,
|
|
31402
|
+
items: {
|
|
31403
|
+
type: "object",
|
|
31404
|
+
additionalProperties: false,
|
|
31405
|
+
required: ["id", "expected_updated_at", "status"],
|
|
31406
|
+
properties: {
|
|
31407
|
+
id: { type: "string" },
|
|
31408
|
+
expected_updated_at: { type: "string", format: "date-time" },
|
|
31409
|
+
status: { type: "string", enum: ["completed"] }
|
|
31410
|
+
}
|
|
31411
|
+
}
|
|
31412
|
+
}
|
|
30762
31413
|
}
|
|
30763
31414
|
}
|
|
30764
31415
|
}
|
|
@@ -30781,6 +31432,26 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30781
31432
|
skipped: { type: "number" },
|
|
30782
31433
|
errors: { type: "array", items: { type: "string" } }
|
|
30783
31434
|
}
|
|
31435
|
+
},
|
|
31436
|
+
planCompletions: {
|
|
31437
|
+
type: "array",
|
|
31438
|
+
items: {
|
|
31439
|
+
type: "object",
|
|
31440
|
+
required: [
|
|
31441
|
+
"id",
|
|
31442
|
+
"status",
|
|
31443
|
+
"expected_updated_at",
|
|
31444
|
+
"result_updated_at",
|
|
31445
|
+
"applied"
|
|
31446
|
+
],
|
|
31447
|
+
properties: {
|
|
31448
|
+
id: { type: "string" },
|
|
31449
|
+
status: { type: "string", enum: ["completed"] },
|
|
31450
|
+
expected_updated_at: { type: "string", format: "date-time" },
|
|
31451
|
+
result_updated_at: { type: "string", format: "date-time" },
|
|
31452
|
+
applied: { type: "boolean" }
|
|
31453
|
+
}
|
|
31454
|
+
}
|
|
30784
31455
|
}
|
|
30785
31456
|
}
|
|
30786
31457
|
}
|
|
@@ -30793,7 +31464,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30793
31464
|
}
|
|
30794
31465
|
};
|
|
30795
31466
|
}
|
|
30796
|
-
var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
31467
|
+
var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
30797
31468
|
var init_openapi = __esm(() => {
|
|
30798
31469
|
init_package_version();
|
|
30799
31470
|
init_types();
|
|
@@ -30812,6 +31483,8 @@ var init_openapi = __esm(() => {
|
|
|
30812
31483
|
reason: { type: "string", nullable: true },
|
|
30813
31484
|
tags: { type: "array", items: { type: "string" } },
|
|
30814
31485
|
version: { type: "number" },
|
|
31486
|
+
locked_by: { type: "string", nullable: true },
|
|
31487
|
+
locked_at: { type: "string", format: "date-time", nullable: true },
|
|
30815
31488
|
created_at: { type: "string" },
|
|
30816
31489
|
updated_at: { type: "string" }
|
|
30817
31490
|
}
|
|
@@ -30976,6 +31649,69 @@ var init_openapi = __esm(() => {
|
|
|
30976
31649
|
created_at: { type: "string", format: "date-time" }
|
|
30977
31650
|
}
|
|
30978
31651
|
};
|
|
31652
|
+
staleLockHandoffInputSchema = {
|
|
31653
|
+
type: "object",
|
|
31654
|
+
additionalProperties: false,
|
|
31655
|
+
required: [
|
|
31656
|
+
"expected_holder",
|
|
31657
|
+
"expected_lock_version",
|
|
31658
|
+
"stale_after_seconds",
|
|
31659
|
+
"new_holder",
|
|
31660
|
+
"reason"
|
|
31661
|
+
],
|
|
31662
|
+
properties: {
|
|
31663
|
+
expected_holder: { type: "string", minLength: 1 },
|
|
31664
|
+
expected_lock_version: {
|
|
31665
|
+
type: "string",
|
|
31666
|
+
format: "date-time",
|
|
31667
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
|
|
31668
|
+
description: "Exact authoritative locked_at token read from the task; no default or normalization is applied."
|
|
31669
|
+
},
|
|
31670
|
+
stale_after_seconds: {
|
|
31671
|
+
type: "integer",
|
|
31672
|
+
minimum: 1,
|
|
31673
|
+
description: "Lock age threshold supplied by the caller. The lock must be strictly older at the CAS instant."
|
|
31674
|
+
},
|
|
31675
|
+
new_holder: {
|
|
31676
|
+
type: "string",
|
|
31677
|
+
minLength: 1,
|
|
31678
|
+
description: "Must match the agent bound to the authenticated API key."
|
|
31679
|
+
},
|
|
31680
|
+
reason: { type: "string", minLength: 1, maxLength: 4096 }
|
|
31681
|
+
}
|
|
31682
|
+
};
|
|
31683
|
+
staleLockHandoffReceiptSchema = {
|
|
31684
|
+
type: "object",
|
|
31685
|
+
additionalProperties: false,
|
|
31686
|
+
required: [
|
|
31687
|
+
"schema_version",
|
|
31688
|
+
"receipt_id",
|
|
31689
|
+
"task_id",
|
|
31690
|
+
"actor",
|
|
31691
|
+
"previous_holder",
|
|
31692
|
+
"previous_lock_version",
|
|
31693
|
+
"new_holder",
|
|
31694
|
+
"new_lock_version",
|
|
31695
|
+
"stale_after_seconds",
|
|
31696
|
+
"stale_cutoff",
|
|
31697
|
+
"reason",
|
|
31698
|
+
"created_at"
|
|
31699
|
+
],
|
|
31700
|
+
properties: {
|
|
31701
|
+
schema_version: { type: "string", enum: ["todos.stale-lock-handoff.v1"] },
|
|
31702
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
31703
|
+
task_id: { type: "string", format: "uuid" },
|
|
31704
|
+
actor: { type: "string" },
|
|
31705
|
+
previous_holder: { type: "string" },
|
|
31706
|
+
previous_lock_version: { type: "string", format: "date-time" },
|
|
31707
|
+
new_holder: { type: "string" },
|
|
31708
|
+
new_lock_version: { type: "string", format: "date-time" },
|
|
31709
|
+
stale_after_seconds: { type: "integer", minimum: 1 },
|
|
31710
|
+
stale_cutoff: { type: "string", format: "date-time" },
|
|
31711
|
+
reason: { type: "string" },
|
|
31712
|
+
created_at: { type: "string", format: "date-time" }
|
|
31713
|
+
}
|
|
31714
|
+
};
|
|
30979
31715
|
taskGitRefSchema = {
|
|
30980
31716
|
type: "object",
|
|
30981
31717
|
additionalProperties: false,
|
|
@@ -31977,6 +32713,65 @@ function normalizeImportSnapshot(raw) {
|
|
|
31977
32713
|
function countSnapshotRecords(s) {
|
|
31978
32714
|
return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.templateTasks.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
|
|
31979
32715
|
}
|
|
32716
|
+
function validatePlanCompletionImports(raw) {
|
|
32717
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
32718
|
+
return { present: false, operations: [] };
|
|
32719
|
+
}
|
|
32720
|
+
const body2 = raw;
|
|
32721
|
+
if (!Object.prototype.hasOwnProperty.call(body2, "planCompletions")) {
|
|
32722
|
+
return { present: false, operations: [] };
|
|
32723
|
+
}
|
|
32724
|
+
if (!Array.isArray(body2["planCompletions"]) || body2["planCompletions"].length !== 1) {
|
|
32725
|
+
return {
|
|
32726
|
+
present: true,
|
|
32727
|
+
operations: [],
|
|
32728
|
+
error: "planCompletions must contain exactly one completion operation"
|
|
32729
|
+
};
|
|
32730
|
+
}
|
|
32731
|
+
const operation = body2["planCompletions"][0];
|
|
32732
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
|
|
32733
|
+
return { present: true, operations: [], error: "plan completion must be an object" };
|
|
32734
|
+
}
|
|
32735
|
+
const record = operation;
|
|
32736
|
+
const allowed = new Set(["id", "expected_updated_at", "status"]);
|
|
32737
|
+
const unknown = Object.keys(record).find((key2) => !allowed.has(key2));
|
|
32738
|
+
if (unknown) {
|
|
32739
|
+
return { present: true, operations: [], error: `unknown plan completion field: ${unknown}` };
|
|
32740
|
+
}
|
|
32741
|
+
if (typeof record["id"] !== "string" || !record["id"].trim()) {
|
|
32742
|
+
return { present: true, operations: [], error: "plan completion id must be a non-empty string" };
|
|
32743
|
+
}
|
|
32744
|
+
if (record["status"] !== "completed") {
|
|
32745
|
+
return { present: true, operations: [], error: "plan completion status must be completed" };
|
|
32746
|
+
}
|
|
32747
|
+
const expectedUpdatedAt = typeof record["expected_updated_at"] === "string" ? record["expected_updated_at"] : "";
|
|
32748
|
+
const timestampMatch = RFC3339_DATE_TIME.exec(expectedUpdatedAt);
|
|
32749
|
+
const parsedTimestamp = Date.parse(expectedUpdatedAt);
|
|
32750
|
+
if (!timestampMatch || Number.isNaN(parsedTimestamp)) {
|
|
32751
|
+
return {
|
|
32752
|
+
present: true,
|
|
32753
|
+
operations: [],
|
|
32754
|
+
error: "plan completion expected_updated_at must be an RFC 3339 date-time with an explicit offset"
|
|
32755
|
+
};
|
|
32756
|
+
}
|
|
32757
|
+
const [, year, month, day] = timestampMatch;
|
|
32758
|
+
const calendarProbe = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
32759
|
+
if (calendarProbe.getUTCFullYear() !== Number(year) || calendarProbe.getUTCMonth() !== Number(month) - 1 || calendarProbe.getUTCDate() !== Number(day)) {
|
|
32760
|
+
return {
|
|
32761
|
+
present: true,
|
|
32762
|
+
operations: [],
|
|
32763
|
+
error: "plan completion expected_updated_at names a date that does not exist"
|
|
32764
|
+
};
|
|
32765
|
+
}
|
|
32766
|
+
return {
|
|
32767
|
+
present: true,
|
|
32768
|
+
operations: [{
|
|
32769
|
+
id: record["id"],
|
|
32770
|
+
expected_updated_at: expectedUpdatedAt,
|
|
32771
|
+
status: "completed"
|
|
32772
|
+
}]
|
|
32773
|
+
};
|
|
32774
|
+
}
|
|
31980
32775
|
async function handleV1Request(req, url, dependencies = {}) {
|
|
31981
32776
|
const path = url.pathname;
|
|
31982
32777
|
if (path !== "/v1" && !path.startsWith("/v1/"))
|
|
@@ -32158,6 +32953,45 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
32158
32953
|
return error(405, `method ${method} not allowed on /v1/tasks`);
|
|
32159
32954
|
}
|
|
32160
32955
|
if (action) {
|
|
32956
|
+
if (action === "stale-lock-handoff") {
|
|
32957
|
+
if (method !== "POST") {
|
|
32958
|
+
return error(405, "method must be POST on /v1/tasks/:id/stale-lock-handoff");
|
|
32959
|
+
}
|
|
32960
|
+
const exactId = normalizeExactTaskId(id);
|
|
32961
|
+
if (!principal.agent) {
|
|
32962
|
+
return error(403, "stale-lock handoff requires an authenticated agent-bound key", {
|
|
32963
|
+
code: "STALE_LOCK_HANDOFF_ACTOR_MISMATCH"
|
|
32964
|
+
});
|
|
32965
|
+
}
|
|
32966
|
+
if (typeof store.tasks.handoffStaleLock !== "function") {
|
|
32967
|
+
return error(501, "stale-lock handoff is not supported by this storage backend");
|
|
32968
|
+
}
|
|
32969
|
+
const body3 = await readJson3(req) ?? {};
|
|
32970
|
+
const allowed = new Set([
|
|
32971
|
+
"expected_holder",
|
|
32972
|
+
"expected_lock_version",
|
|
32973
|
+
"stale_after_seconds",
|
|
32974
|
+
"new_holder",
|
|
32975
|
+
"reason"
|
|
32976
|
+
]);
|
|
32977
|
+
const unknown = Object.keys(body3).find((key2) => !allowed.has(key2));
|
|
32978
|
+
if (unknown) {
|
|
32979
|
+
return error(400, `unknown stale-lock handoff field: ${unknown}`, {
|
|
32980
|
+
code: "STALE_LOCK_HANDOFF_INVALID_INPUT",
|
|
32981
|
+
field: unknown
|
|
32982
|
+
});
|
|
32983
|
+
}
|
|
32984
|
+
const receipt = await store.tasks.handoffStaleLock({
|
|
32985
|
+
task_id: exactId,
|
|
32986
|
+
actor: principal.agent,
|
|
32987
|
+
expected_holder: body3.expected_holder,
|
|
32988
|
+
expected_lock_version: body3.expected_lock_version,
|
|
32989
|
+
stale_after_seconds: body3.stale_after_seconds,
|
|
32990
|
+
new_holder: body3.new_holder,
|
|
32991
|
+
reason: body3.reason
|
|
32992
|
+
}, contextFromPrincipal(principal));
|
|
32993
|
+
return json5({ receipt });
|
|
32994
|
+
}
|
|
32161
32995
|
if (action === "comments") {
|
|
32162
32996
|
if (method === "GET") {
|
|
32163
32997
|
if (!await store.tasks.get(id))
|
|
@@ -32913,10 +33747,57 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
32913
33747
|
return error(400, "invalid JSON body");
|
|
32914
33748
|
const snapshot = normalizeImportSnapshot(raw);
|
|
32915
33749
|
const received = countSnapshotRecords(snapshot);
|
|
33750
|
+
const completionImports = validatePlanCompletionImports(raw);
|
|
33751
|
+
if (completionImports.present) {
|
|
33752
|
+
if (completionImports.error)
|
|
33753
|
+
return error(400, completionImports.error);
|
|
33754
|
+
if (received !== 0) {
|
|
33755
|
+
return error(400, "planCompletions cannot be combined with snapshot record arrays");
|
|
33756
|
+
}
|
|
33757
|
+
if (typeof store.plans.completeAtRevision !== "function") {
|
|
33758
|
+
return error(501, "atomic plan completion is not supported by this storage backend");
|
|
33759
|
+
}
|
|
33760
|
+
const operation = completionImports.operations[0];
|
|
33761
|
+
const completed = await store.plans.completeAtRevision(operation.id, operation.expected_updated_at, contextFromPrincipal(principal));
|
|
33762
|
+
return json5({
|
|
33763
|
+
result: {
|
|
33764
|
+
inserted: 0,
|
|
33765
|
+
updated: completed.applied ? 1 : 0,
|
|
33766
|
+
deleted: 0,
|
|
33767
|
+
skipped: completed.applied ? 0 : 1,
|
|
33768
|
+
errors: []
|
|
33769
|
+
},
|
|
33770
|
+
received: 1,
|
|
33771
|
+
planCompletions: [{
|
|
33772
|
+
id: operation.id,
|
|
33773
|
+
status: "completed",
|
|
33774
|
+
expected_updated_at: operation.expected_updated_at,
|
|
33775
|
+
result_updated_at: completed.plan.updated_at,
|
|
33776
|
+
applied: completed.applied
|
|
33777
|
+
}]
|
|
33778
|
+
});
|
|
33779
|
+
}
|
|
32916
33780
|
if (received === 0) {
|
|
32917
33781
|
return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
|
|
32918
33782
|
}
|
|
33783
|
+
const forbiddenAuditTombstone = (snapshot.tombstones ?? []).find((tombstone) => tombstone.object_type === "audit_history");
|
|
33784
|
+
if (forbiddenAuditTombstone) {
|
|
33785
|
+
return error(400, forbiddenAuditHistoryTombstoneError(forbiddenAuditTombstone.object_id), {
|
|
33786
|
+
code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
|
|
33787
|
+
conflict: false,
|
|
33788
|
+
audit_history_id: forbiddenAuditTombstone.object_id
|
|
33789
|
+
});
|
|
33790
|
+
}
|
|
32919
33791
|
const result = await store.sync.importSnapshot(snapshot, contextFromPrincipal(principal));
|
|
33792
|
+
const auditFailureMessage = result.errors.find((message) => parseAuditHistoryImportFailure(message) !== null);
|
|
33793
|
+
if (auditFailureMessage) {
|
|
33794
|
+
const failure = parseAuditHistoryImportFailure(auditFailureMessage);
|
|
33795
|
+
return error(failure.status, auditFailureMessage, {
|
|
33796
|
+
code: failure.code,
|
|
33797
|
+
conflict: failure.conflict,
|
|
33798
|
+
audit_history_id: failure.auditHistoryId
|
|
33799
|
+
});
|
|
33800
|
+
}
|
|
32920
33801
|
return json5({ result, received });
|
|
32921
33802
|
}
|
|
32922
33803
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
@@ -32939,6 +33820,26 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
32939
33820
|
if (e instanceof TaskNotFoundError) {
|
|
32940
33821
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
32941
33822
|
}
|
|
33823
|
+
if (e instanceof StaleLockHandoffError) {
|
|
33824
|
+
const status2 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
|
|
33825
|
+
return error(status2, e.message, {
|
|
33826
|
+
code: e.code,
|
|
33827
|
+
conflict: status2 === 409,
|
|
33828
|
+
...e.details
|
|
33829
|
+
});
|
|
33830
|
+
}
|
|
33831
|
+
if (e instanceof PlanNotFoundError) {
|
|
33832
|
+
return error(404, e.message, { code: PlanNotFoundError.code });
|
|
33833
|
+
}
|
|
33834
|
+
if (e instanceof PlanRevisionConflictError) {
|
|
33835
|
+
return error(409, e.message, {
|
|
33836
|
+
code: PlanRevisionConflictError.code,
|
|
33837
|
+
conflict: true,
|
|
33838
|
+
plan_id: e.planId,
|
|
33839
|
+
expected_updated_at: e.expectedUpdatedAt,
|
|
33840
|
+
current_updated_at: e.currentUpdatedAt
|
|
33841
|
+
});
|
|
33842
|
+
}
|
|
32942
33843
|
if (e instanceof LockError)
|
|
32943
33844
|
return error(409, e.message, { code: LockError.code });
|
|
32944
33845
|
if (e instanceof TaskNotStartableError) {
|
|
@@ -32961,6 +33862,8 @@ var init_v1 = __esm(() => {
|
|
|
32961
33862
|
init_redaction();
|
|
32962
33863
|
init_project_task_list_ensure();
|
|
32963
33864
|
init_plan_project_link();
|
|
33865
|
+
init_stale_lock_handoff();
|
|
33866
|
+
init_audit_history_import();
|
|
32964
33867
|
JSON_HEADERS4 = { "Content-Type": "application/json" };
|
|
32965
33868
|
RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
32966
33869
|
});
|
|
@@ -63726,6 +64629,7 @@ var init_http_client = __esm(() => {
|
|
|
63726
64629
|
});
|
|
63727
64630
|
|
|
63728
64631
|
// src/cli/cloud-router.ts
|
|
64632
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
63729
64633
|
import { resolve as resolvePath } from "path";
|
|
63730
64634
|
function cleanMode(value) {
|
|
63731
64635
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -64128,7 +65032,10 @@ async function cloudGetTask(client, id) {
|
|
|
64128
65032
|
}
|
|
64129
65033
|
async function cloudCreateTask(client, input) {
|
|
64130
65034
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
64131
|
-
const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.
|
|
65035
|
+
const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.transport.post("/tasks", input, {
|
|
65036
|
+
idempotencyKey: randomUUID4(),
|
|
65037
|
+
retry: false
|
|
65038
|
+
}), ["PARENT_TASK_NOT_FOUND"]));
|
|
64132
65039
|
if (!created || typeof created.id !== "string" || !created.id.trim()) {
|
|
64133
65040
|
throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
|
|
64134
65041
|
}
|
|
@@ -64139,7 +65046,7 @@ async function cloudCreateTask(client, input) {
|
|
|
64139
65046
|
return persisted;
|
|
64140
65047
|
}
|
|
64141
65048
|
async function cloudUpdateTask(client, id, patch) {
|
|
64142
|
-
return unwrapTask(await client.
|
|
65049
|
+
return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
|
|
64143
65050
|
}
|
|
64144
65051
|
async function cloudDeleteTask(client, id) {
|
|
64145
65052
|
try {
|
|
@@ -64350,7 +65257,7 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
64350
65257
|
return input.toLowerCase();
|
|
64351
65258
|
return (await cloudResolveTaskList(client, ref, projectId)).id;
|
|
64352
65259
|
}
|
|
64353
|
-
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache;
|
|
65260
|
+
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, retryCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache;
|
|
64354
65261
|
var init_cloud_router = __esm(() => {
|
|
64355
65262
|
init_storage();
|
|
64356
65263
|
init_mode();
|
|
@@ -64369,6 +65276,7 @@ var init_cloud_router = __esm(() => {
|
|
|
64369
65276
|
hybrid: "http"
|
|
64370
65277
|
};
|
|
64371
65278
|
completionCapabilityCache = new Map;
|
|
65279
|
+
retryCapabilityCache = new Map;
|
|
64372
65280
|
gitRefCapabilityCache = new Map;
|
|
64373
65281
|
SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
|
|
64374
65282
|
PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|