@hasna/todos 0.15.19 → 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/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +851 -47
- package/dist/cli/stage-a.d.ts +21 -10
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +228 -16
- package/dist/db/audit.d.ts +8 -0
- package/dist/db/audit.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 +479 -20
- 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 +702 -36
- package/dist/mcp.js +3 -1
- package/dist/project-registration.js +1147 -686
- package/dist/registry.js +228 -16
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/v1.generated.d.ts +27 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1035 -369
- package/dist/server/openapi.d.ts +182 -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 +12 -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/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +476 -19
- package/dist/task-manifest.js +11 -1
- package/dist/types/index.d.ts +43 -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, PlanRevisionConflictError, 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",
|
|
@@ -983,6 +985,16 @@ var init_types = __esm(() => {
|
|
|
983
985
|
this.name = "LockError";
|
|
984
986
|
}
|
|
985
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
|
+
};
|
|
986
998
|
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
987
999
|
agentId;
|
|
988
1000
|
static code = "AGENT_NOT_FOUND";
|
|
@@ -1196,6 +1208,501 @@ var init_plan_project_link_contract = __esm(() => {
|
|
|
1196
1208
|
};
|
|
1197
1209
|
});
|
|
1198
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
|
+
|
|
1199
1706
|
// src/lib/slugs.ts
|
|
1200
1707
|
function normalizeSlug(value) {
|
|
1201
1708
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -1862,181 +2369,56 @@ var init_integrity = __esm(() => {
|
|
|
1862
2369
|
};
|
|
1863
2370
|
});
|
|
1864
2371
|
|
|
1865
|
-
// src/
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
function loadConfig() {
|
|
1872
|
-
if (cached)
|
|
1873
|
-
return cached;
|
|
1874
|
-
if (!existsSync3(getConfigPath())) {
|
|
1875
|
-
cached = {};
|
|
1876
|
-
return cached;
|
|
1877
|
-
}
|
|
1878
|
-
const config = readJsonFile(getConfigPath()) || {};
|
|
1879
|
-
if (typeof config.sync_agents === "string") {
|
|
1880
|
-
config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1881
|
-
}
|
|
1882
|
-
cached = config;
|
|
1883
|
-
return cached;
|
|
1884
|
-
}
|
|
1885
|
-
function saveConfig(config) {
|
|
1886
|
-
const configPath = getConfigPath();
|
|
1887
|
-
ensureDir(dirname(configPath));
|
|
1888
|
-
writeJsonFile(configPath, config);
|
|
1889
|
-
cached = config;
|
|
1890
|
-
return config;
|
|
1891
|
-
}
|
|
1892
|
-
function getAgentPoolForProject(workingDir) {
|
|
1893
|
-
const config = loadConfig();
|
|
1894
|
-
if (workingDir && config.project_pools) {
|
|
1895
|
-
let bestKey = null;
|
|
1896
|
-
let bestLen = 0;
|
|
1897
|
-
for (const key of Object.keys(config.project_pools)) {
|
|
1898
|
-
if (workingDir.startsWith(key) && key.length > bestLen) {
|
|
1899
|
-
bestKey = key;
|
|
1900
|
-
bestLen = key.length;
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
if (bestKey && config.project_pools[bestKey]) {
|
|
1904
|
-
return config.project_pools[bestKey];
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
return config.agent_pool || null;
|
|
1908
|
-
}
|
|
1909
|
-
function getCompletionGuardConfig(projectPath) {
|
|
1910
|
-
const config = loadConfig();
|
|
1911
|
-
const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
|
|
1912
|
-
if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
|
|
1913
|
-
return { ...global, ...config.project_overrides[projectPath].completion_guard };
|
|
1914
|
-
}
|
|
1915
|
-
return global;
|
|
1916
|
-
}
|
|
1917
|
-
var cached = null, GUARD_DEFAULTS;
|
|
1918
|
-
var init_config2 = __esm(() => {
|
|
1919
|
-
init_sync_utils();
|
|
1920
|
-
GUARD_DEFAULTS = {
|
|
1921
|
-
enabled: false,
|
|
1922
|
-
min_work_seconds: 30,
|
|
1923
|
-
max_completions_per_window: 5,
|
|
1924
|
-
window_minutes: 10,
|
|
1925
|
-
cooldown_seconds: 60
|
|
1926
|
-
};
|
|
1927
|
-
});
|
|
1928
|
-
|
|
1929
|
-
// src/lib/redaction.ts
|
|
1930
|
-
function unique(values) {
|
|
1931
|
-
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
1932
|
-
}
|
|
1933
|
-
function cloneRegex(regex) {
|
|
1934
|
-
return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
|
|
1935
|
-
}
|
|
1936
|
-
function customPatterns() {
|
|
1937
|
-
return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
|
|
1938
|
-
try {
|
|
1939
|
-
return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
|
|
1940
|
-
} catch {
|
|
1941
|
-
return [];
|
|
1942
|
-
}
|
|
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;
|
|
1943
2378
|
});
|
|
1944
2379
|
}
|
|
1945
|
-
function
|
|
1946
|
-
return
|
|
1947
|
-
}
|
|
1948
|
-
function isRedactionPlaceholderMatch(match) {
|
|
1949
|
-
const trimmed = match.trim();
|
|
1950
|
-
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(trimmed) || new RegExp(`=\\s*['"]?${REDACTION_PLACEHOLDER}['"]?$`).test(trimmed);
|
|
1951
|
-
}
|
|
1952
|
-
function isRedactionPlaceholderKey(key) {
|
|
1953
|
-
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(key.trim());
|
|
1954
|
-
}
|
|
1955
|
-
function isSecretKey(key) {
|
|
1956
|
-
if (isRedactionPlaceholderKey(key))
|
|
1957
|
-
return false;
|
|
1958
|
-
if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
|
|
1959
|
-
return false;
|
|
1960
|
-
if (DEFAULT_SECRET_KEY_PATTERN.test(key))
|
|
1961
|
-
return true;
|
|
1962
|
-
return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
|
|
2380
|
+
function divergentAuditHistoryReplayError(id) {
|
|
2381
|
+
return `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row ${id} differs from stored row`;
|
|
1963
2382
|
}
|
|
1964
|
-
function
|
|
1965
|
-
|
|
1966
|
-
for (const pattern of secretPatterns()) {
|
|
1967
|
-
const regex = cloneRegex(pattern.regex);
|
|
1968
|
-
const replacement = pattern.replacement ?? "[REDACTED]";
|
|
1969
|
-
redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
|
|
1970
|
-
}
|
|
1971
|
-
return redacted;
|
|
2383
|
+
function forbiddenAuditHistoryTombstoneError(id) {
|
|
2384
|
+
return `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone ${id} is not allowed`;
|
|
1972
2385
|
}
|
|
1973
|
-
function
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
if (
|
|
1977
|
-
return
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
} else {
|
|
1984
|
-
redacted[key] = redactValue(child);
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
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
|
+
};
|
|
1988
2396
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
+
};
|
|
1997
2406
|
}
|
|
1998
|
-
return
|
|
1999
|
-
}
|
|
2000
|
-
function getSecretSafetyConfig() {
|
|
2001
|
-
return {
|
|
2002
|
-
redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
|
|
2003
|
-
redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
|
|
2004
|
-
};
|
|
2005
|
-
}
|
|
2006
|
-
function upsertSecretSafetyConfig(input) {
|
|
2007
|
-
const config = loadConfig();
|
|
2008
|
-
const next = {
|
|
2009
|
-
redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
|
|
2010
|
-
redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
|
|
2011
|
-
};
|
|
2012
|
-
saveConfig({ ...config, secret_safety: next });
|
|
2013
|
-
return next;
|
|
2407
|
+
return null;
|
|
2014
2408
|
}
|
|
2015
|
-
var
|
|
2016
|
-
var
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
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"
|
|
2027
2421
|
];
|
|
2028
|
-
DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
|
|
2029
|
-
NON_SECRET_USAGE_KEYS = new Set([
|
|
2030
|
-
"tokens",
|
|
2031
|
-
"total_tokens",
|
|
2032
|
-
"token_count",
|
|
2033
|
-
"input_tokens",
|
|
2034
|
-
"output_tokens",
|
|
2035
|
-
"prompt_tokens",
|
|
2036
|
-
"completion_tokens",
|
|
2037
|
-
"cost_tokens"
|
|
2038
|
-
]);
|
|
2039
|
-
REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
2040
2422
|
});
|
|
2041
2423
|
|
|
2042
2424
|
// src/storage/postgres-adapter.ts
|
|
@@ -2069,6 +2451,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
2069
2451
|
getChangedSince: (since, filters) => getChangedSince(since, filters, store),
|
|
2070
2452
|
lock: (id, agentId) => lockTask(id, agentId, store),
|
|
2071
2453
|
unlock: (id, agentId) => unlockTask(id, agentId, store),
|
|
2454
|
+
handoffStaleLock: (input, context) => store.handoffStaleLock(input, context),
|
|
2072
2455
|
getByFingerprint: (fingerprint) => store.getTaskByFingerprint(fingerprint)
|
|
2073
2456
|
},
|
|
2074
2457
|
dependencies: {
|
|
@@ -2215,6 +2598,91 @@ class PostgresJsonRecordStore {
|
|
|
2215
2598
|
LIMIT 1`, [this.service, type, id]);
|
|
2216
2599
|
return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
|
|
2217
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
|
+
}
|
|
2218
2686
|
async list(type) {
|
|
2219
2687
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
2220
2688
|
}
|
|
@@ -2484,6 +2952,28 @@ class PostgresJsonRecordStore {
|
|
|
2484
2952
|
}
|
|
2485
2953
|
return value;
|
|
2486
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
|
+
}
|
|
2487
2977
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
|
|
2488
2978
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
2489
2979
|
if (planIds.length === 0)
|
|
@@ -3972,6 +4462,11 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
3972
4462
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
3973
4463
|
if (result.errors.length > 0)
|
|
3974
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;
|
|
3975
4470
|
const entries = [
|
|
3976
4471
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
3977
4472
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
@@ -3980,9 +4475,20 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
3980
4475
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
3981
4476
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
3982
4477
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
3983
|
-
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
3984
|
-
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
4478
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
3985
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
|
+
}
|
|
3986
4492
|
for (const [type, row] of entries) {
|
|
3987
4493
|
try {
|
|
3988
4494
|
const existing = await store.get(type, row.id);
|
|
@@ -4016,6 +4522,32 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
4016
4522
|
}
|
|
4017
4523
|
return result;
|
|
4018
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
|
+
}
|
|
4019
4551
|
async function requireRecord(type, id, store) {
|
|
4020
4552
|
const record = await store.get(type, id);
|
|
4021
4553
|
if (!record)
|
|
@@ -4122,9 +4654,11 @@ var init_postgres_adapter = __esm(() => {
|
|
|
4122
4654
|
init_types();
|
|
4123
4655
|
init_creator_identity();
|
|
4124
4656
|
init_plan_project_link_contract();
|
|
4657
|
+
init_stale_lock_handoff();
|
|
4125
4658
|
init_postgres_sync();
|
|
4126
4659
|
init_integrity();
|
|
4127
4660
|
init_redaction();
|
|
4661
|
+
init_audit_history_import();
|
|
4128
4662
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
4129
4663
|
});
|
|
4130
4664
|
|
|
@@ -12833,185 +13367,6 @@ var init_shared_events = __esm(() => {
|
|
|
12833
13367
|
init_event_emission_safety();
|
|
12834
13368
|
});
|
|
12835
13369
|
|
|
12836
|
-
// src/lib/secret-redaction.ts
|
|
12837
|
-
function isAllowlisted(text, match, allowlist) {
|
|
12838
|
-
const context = text.slice(Math.max(0, text.indexOf(match) - 20), text.indexOf(match) + match.length + 20);
|
|
12839
|
-
return allowlist.some((re) => re.test(context) || re.test(match));
|
|
12840
|
-
}
|
|
12841
|
-
function scanTextForSecrets(text, options = {}) {
|
|
12842
|
-
const allowlist = [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []];
|
|
12843
|
-
const matches = [];
|
|
12844
|
-
const patterns = [
|
|
12845
|
-
...DEFAULT_PATTERNS,
|
|
12846
|
-
...options.custom_patterns?.map((p, i) => ({ name: `custom_${i}`, pattern: p })) ?? []
|
|
12847
|
-
];
|
|
12848
|
-
for (const { name, pattern, allowlist_ok } of patterns) {
|
|
12849
|
-
const re = new RegExp(pattern.source, pattern.flags);
|
|
12850
|
-
let m;
|
|
12851
|
-
while ((m = re.exec(text)) !== null) {
|
|
12852
|
-
const match = m[0];
|
|
12853
|
-
if (allowlist_ok && isAllowlisted(text, match, allowlist))
|
|
12854
|
-
continue;
|
|
12855
|
-
if (isAllowlisted(text, match, allowlist))
|
|
12856
|
-
continue;
|
|
12857
|
-
const line = text.slice(0, m.index).split(`
|
|
12858
|
-
`).length;
|
|
12859
|
-
matches.push({ pattern: name, match: match.slice(0, 12) + (match.length > 12 ? "\u2026" : ""), index: m.index, line });
|
|
12860
|
-
}
|
|
12861
|
-
}
|
|
12862
|
-
return {
|
|
12863
|
-
schema_version: SECRET_REDACTION_SCHEMA,
|
|
12864
|
-
clean: matches.length === 0,
|
|
12865
|
-
matches
|
|
12866
|
-
};
|
|
12867
|
-
}
|
|
12868
|
-
function redactText(text, options = {}) {
|
|
12869
|
-
const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
|
|
12870
|
-
let out = text;
|
|
12871
|
-
for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
|
|
12872
|
-
out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
|
|
12873
|
-
if (allowlist_ok && isAllowlisted(out, match, [...DEFAULT_ALLOWLIST, ...options.allowlist ?? []])) {
|
|
12874
|
-
return match;
|
|
12875
|
-
}
|
|
12876
|
-
return placeholder;
|
|
12877
|
-
});
|
|
12878
|
-
}
|
|
12879
|
-
for (const custom of options.custom_patterns ?? []) {
|
|
12880
|
-
out = out.replace(custom, placeholder);
|
|
12881
|
-
}
|
|
12882
|
-
for (const fn of customRedactors) {
|
|
12883
|
-
out = fn(out);
|
|
12884
|
-
}
|
|
12885
|
-
return out;
|
|
12886
|
-
}
|
|
12887
|
-
function redactExportRecord(record) {
|
|
12888
|
-
const base = redactValue(record);
|
|
12889
|
-
for (const [key, value] of Object.entries(base)) {
|
|
12890
|
-
if (typeof value === "string") {
|
|
12891
|
-
base[key] = redactText(value);
|
|
12892
|
-
}
|
|
12893
|
-
}
|
|
12894
|
-
return base;
|
|
12895
|
-
}
|
|
12896
|
-
var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
|
|
12897
|
-
var init_secret_redaction = __esm(() => {
|
|
12898
|
-
init_redaction();
|
|
12899
|
-
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
12900
|
-
DEFAULT_PATTERNS = [
|
|
12901
|
-
{ name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
|
|
12902
|
-
{ name: "github_pat", pattern: /\bghp_[a-zA-Z0-9]{20,}\b/g },
|
|
12903
|
-
{ name: "github_oauth", pattern: /\bgho_[a-zA-Z0-9]{20,}\b/g },
|
|
12904
|
-
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
12905
|
-
{ name: "bearer_token", pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+=*\b/gi },
|
|
12906
|
-
{ name: "jwt", pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
12907
|
-
{ name: "private_key_block", pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
12908
|
-
{ name: "generic_api_key", pattern: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[a-zA-Z0-9\-._]{8,}['"]?/gi, allowlist_ok: true }
|
|
12909
|
-
];
|
|
12910
|
-
DEFAULT_ALLOWLIST = [
|
|
12911
|
-
/\[REDACTED\]/,
|
|
12912
|
-
/example\.com/i,
|
|
12913
|
-
/your-api-key-here/i,
|
|
12914
|
-
/sk-test/i,
|
|
12915
|
-
/ghp_xxx/i
|
|
12916
|
-
];
|
|
12917
|
-
customRedactors = [];
|
|
12918
|
-
});
|
|
12919
|
-
|
|
12920
|
-
// src/lib/prewrite-secrets.ts
|
|
12921
|
-
function mergeFindings(findings) {
|
|
12922
|
-
const byPattern = new Map;
|
|
12923
|
-
for (const finding of findings) {
|
|
12924
|
-
const existing = byPattern.get(finding.pattern);
|
|
12925
|
-
if (!existing) {
|
|
12926
|
-
byPattern.set(finding.pattern, {
|
|
12927
|
-
pattern: finding.pattern,
|
|
12928
|
-
count: finding.count,
|
|
12929
|
-
lines: Array.from(new Set(finding.lines)).sort((a, b) => a - b)
|
|
12930
|
-
});
|
|
12931
|
-
continue;
|
|
12932
|
-
}
|
|
12933
|
-
existing.count += finding.count;
|
|
12934
|
-
existing.lines = Array.from(new Set([...existing.lines, ...finding.lines])).sort((a, b) => a - b);
|
|
12935
|
-
}
|
|
12936
|
-
return [...byPattern.values()].sort((left, right) => left.pattern.localeCompare(right.pattern));
|
|
12937
|
-
}
|
|
12938
|
-
function redactPreWriteText(value) {
|
|
12939
|
-
return redactText(redactEvidenceText(value));
|
|
12940
|
-
}
|
|
12941
|
-
function scanPreWriteText(value, context = "text") {
|
|
12942
|
-
const redacted = redactPreWriteText(value);
|
|
12943
|
-
if (redacted === value) {
|
|
12944
|
-
return {
|
|
12945
|
-
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
12946
|
-
clean: true,
|
|
12947
|
-
context,
|
|
12948
|
-
findings: []
|
|
12949
|
-
};
|
|
12950
|
-
}
|
|
12951
|
-
const structural = scanTextForSecrets(value).matches.map((match) => ({
|
|
12952
|
-
pattern: match.pattern,
|
|
12953
|
-
count: 1,
|
|
12954
|
-
lines: match.line ? [match.line] : []
|
|
12955
|
-
}));
|
|
12956
|
-
const configured = listSecretFindings(value).map((finding) => ({
|
|
12957
|
-
pattern: finding.pattern,
|
|
12958
|
-
count: finding.count,
|
|
12959
|
-
lines: []
|
|
12960
|
-
}));
|
|
12961
|
-
return {
|
|
12962
|
-
schema_version: PREWRITE_SECRET_SCAN_SCHEMA,
|
|
12963
|
-
clean: false,
|
|
12964
|
-
context,
|
|
12965
|
-
findings: mergeFindings([...structural, ...configured])
|
|
12966
|
-
};
|
|
12967
|
-
}
|
|
12968
|
-
function assertPreWriteTextClean(value, context = "text") {
|
|
12969
|
-
const scan = scanPreWriteText(value, context);
|
|
12970
|
-
if (!scan.clean)
|
|
12971
|
-
throw new PreWriteSecretError(context, scan.findings);
|
|
12972
|
-
}
|
|
12973
|
-
function sanitizePreWriteText(value, context = "text", options = {}) {
|
|
12974
|
-
const mode = options.mode ?? "redact";
|
|
12975
|
-
if (mode === "block") {
|
|
12976
|
-
assertPreWriteTextClean(value, context);
|
|
12977
|
-
return value;
|
|
12978
|
-
}
|
|
12979
|
-
return redactPreWriteText(value);
|
|
12980
|
-
}
|
|
12981
|
-
function sanitizePreWriteValue(value, context = "value", options = {}) {
|
|
12982
|
-
if (typeof value === "string")
|
|
12983
|
-
return sanitizePreWriteText(value, context, options);
|
|
12984
|
-
if (Array.isArray(value)) {
|
|
12985
|
-
return value.map((item, index) => sanitizePreWriteValue(item, `${context}[${index}]`, options));
|
|
12986
|
-
}
|
|
12987
|
-
if (value && typeof value === "object") {
|
|
12988
|
-
const sanitized = {};
|
|
12989
|
-
for (const [key, child] of Object.entries(value)) {
|
|
12990
|
-
const safeKey = sanitizePreWriteText(key, `${context}.$key`, options);
|
|
12991
|
-
sanitized[safeKey] = sanitizePreWriteValue(child, `${context}.${safeKey}`, options);
|
|
12992
|
-
}
|
|
12993
|
-
return redactValue(sanitized);
|
|
12994
|
-
}
|
|
12995
|
-
return value;
|
|
12996
|
-
}
|
|
12997
|
-
var PREWRITE_SECRET_SCAN_SCHEMA, PreWriteSecretError;
|
|
12998
|
-
var init_prewrite_secrets = __esm(() => {
|
|
12999
|
-
init_redaction();
|
|
13000
|
-
init_secret_redaction();
|
|
13001
|
-
PREWRITE_SECRET_SCAN_SCHEMA = ["todos", "prewrite_secret_scan", "v1"].join(".");
|
|
13002
|
-
PreWriteSecretError = class PreWriteSecretError extends Error {
|
|
13003
|
-
context;
|
|
13004
|
-
findings;
|
|
13005
|
-
constructor(context, findings) {
|
|
13006
|
-
const summary = findings.map((finding) => `${finding.pattern}:${finding.count}`).join(", ");
|
|
13007
|
-
super(`Secret pattern detected before persistence in ${context}: ${summary}`);
|
|
13008
|
-
this.name = "PreWriteSecretError";
|
|
13009
|
-
this.context = context;
|
|
13010
|
-
this.findings = findings;
|
|
13011
|
-
}
|
|
13012
|
-
};
|
|
13013
|
-
});
|
|
13014
|
-
|
|
13015
13370
|
// src/lib/activity-audit.ts
|
|
13016
13371
|
var exports_activity_audit = {};
|
|
13017
13372
|
__export(exports_activity_audit, {
|
|
@@ -13204,28 +13559,55 @@ var init_activity_audit = __esm(() => {
|
|
|
13204
13559
|
function sanitizeHistoryValue(value, context) {
|
|
13205
13560
|
return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
|
|
13206
13561
|
}
|
|
13207
|
-
function
|
|
13562
|
+
function insertTaskHistory(entry2, db) {
|
|
13208
13563
|
const d = db || getDatabase();
|
|
13209
|
-
const
|
|
13210
|
-
|
|
13211
|
-
|
|
13212
|
-
|
|
13213
|
-
|
|
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
|
+
};
|
|
13214
13572
|
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
|
|
13215
|
-
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
|
+
]);
|
|
13216
13584
|
try {
|
|
13217
13585
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
13218
13586
|
logActivity2({
|
|
13219
13587
|
entity_type: "task",
|
|
13220
|
-
entity_id:
|
|
13221
|
-
action,
|
|
13222
|
-
field,
|
|
13223
|
-
old_value:
|
|
13224
|
-
new_value:
|
|
13225
|
-
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
|
|
13226
13594
|
}, d);
|
|
13227
13595
|
} catch {}
|
|
13228
|
-
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);
|
|
13229
13611
|
}
|
|
13230
13612
|
function getTaskHistory(taskId, db) {
|
|
13231
13613
|
const d = db || getDatabase();
|
|
@@ -14192,6 +14574,7 @@ __export(exports_task_lifecycle, {
|
|
|
14192
14574
|
startTask: () => startTask2,
|
|
14193
14575
|
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
14194
14576
|
lockTask: () => lockTask2,
|
|
14577
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
14195
14578
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
14196
14579
|
getTaskLockStatus: () => getTaskLockStatus,
|
|
14197
14580
|
getStaleTasks: () => getStaleTasks,
|
|
@@ -14460,6 +14843,38 @@ function unlockTask2(id, agentId, db) {
|
|
|
14460
14843
|
WHERE id = ?`, [timestamp2, id]);
|
|
14461
14844
|
return true;
|
|
14462
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
|
+
}
|
|
14463
14878
|
function getTaskLockStatus(id, db) {
|
|
14464
14879
|
const d = db || getDatabase();
|
|
14465
14880
|
const task = getTask(id, d);
|
|
@@ -14760,6 +15175,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
14760
15175
|
init_task_crud();
|
|
14761
15176
|
init_task_graph();
|
|
14762
15177
|
init_prewrite_secrets();
|
|
15178
|
+
init_stale_lock_handoff();
|
|
14763
15179
|
});
|
|
14764
15180
|
|
|
14765
15181
|
// src/db/task-crud.ts
|
|
@@ -18332,6 +18748,7 @@ __export(exports_tasks, {
|
|
|
18332
18748
|
insertTaskTags: () => insertTaskTags,
|
|
18333
18749
|
importTaskBoardBundle: () => importTaskBoardBundle,
|
|
18334
18750
|
importCalendarIcs: () => importCalendarIcs,
|
|
18751
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
18335
18752
|
getTimeReport: () => getTimeReport,
|
|
18336
18753
|
getTimeLogs: () => getTimeLogs,
|
|
18337
18754
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
@@ -19304,8 +19721,11 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
19304
19721
|
const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
|
|
19305
19722
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
19306
19723
|
}
|
|
19724
|
+
const auditImport = preflightAuditHistoryImport2(d, snapshot.auditHistory, snapshot.tombstones ?? []);
|
|
19725
|
+
result.errors.push(...auditImport.errors);
|
|
19307
19726
|
if (result.errors.length > 0)
|
|
19308
19727
|
return result;
|
|
19728
|
+
result.skipped += auditImport.identicalReplayCount;
|
|
19309
19729
|
const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
19310
19730
|
for (const row of rows) {
|
|
19311
19731
|
try {
|
|
@@ -19340,10 +19760,72 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
19340
19760
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
19341
19761
|
}
|
|
19342
19762
|
});
|
|
19343
|
-
|
|
19763
|
+
insertAuditHistoryRows(d, auditImport.rowsToInsert, result);
|
|
19344
19764
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
19345
19765
|
return result;
|
|
19346
19766
|
}
|
|
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
|
+
}
|
|
19347
19829
|
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
19348
19830
|
const id = row["id"];
|
|
19349
19831
|
if (typeof id !== "string" || !id)
|
|
@@ -19441,7 +19923,7 @@ function tableForTombstone(objectType) {
|
|
|
19441
19923
|
return "task_templates";
|
|
19442
19924
|
if (objectType === "template_tasks")
|
|
19443
19925
|
return "template_tasks";
|
|
19444
|
-
|
|
19926
|
+
throw new Error(`unsupported storage tombstone object_type: ${String(objectType)}`);
|
|
19445
19927
|
}
|
|
19446
19928
|
function listRows(db, table, columns) {
|
|
19447
19929
|
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
@@ -19488,6 +19970,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
19488
19970
|
init_tasks();
|
|
19489
19971
|
init_templates();
|
|
19490
19972
|
init_storage_tombstones();
|
|
19973
|
+
init_audit_history_import();
|
|
19491
19974
|
PROJECT_COLUMNS = [
|
|
19492
19975
|
"id",
|
|
19493
19976
|
"name",
|
|
@@ -19752,6 +20235,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19752
20235
|
unlockTask2(id, agentId, database());
|
|
19753
20236
|
return true;
|
|
19754
20237
|
},
|
|
20238
|
+
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
19755
20239
|
delete: (id) => deleteTask(id, database()),
|
|
19756
20240
|
start: (id, agentId) => startTask2(id, agentId, database()),
|
|
19757
20241
|
complete: (id, agentId, options2) => completeTask2(id, agentId, database(), options2),
|
|
@@ -29130,6 +29614,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29130
29614
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
29131
29615
|
ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
|
|
29132
29616
|
TaskComment: taskCommentSchema,
|
|
29617
|
+
StaleLockHandoffInput: staleLockHandoffInputSchema,
|
|
29618
|
+
StaleLockHandoffReceipt: staleLockHandoffReceiptSchema,
|
|
29133
29619
|
TaskGitRef: taskGitRefSchema,
|
|
29134
29620
|
Plan: planSchema,
|
|
29135
29621
|
PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
|
|
@@ -30337,6 +30823,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30337
30823
|
}
|
|
30338
30824
|
}
|
|
30339
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
|
+
},
|
|
30340
30871
|
"/v1/tasks/{id}/refs": {
|
|
30341
30872
|
get: {
|
|
30342
30873
|
operationId: "listTaskGitRefs",
|
|
@@ -30933,7 +31464,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
30933
31464
|
}
|
|
30934
31465
|
};
|
|
30935
31466
|
}
|
|
30936
|
-
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;
|
|
30937
31468
|
var init_openapi = __esm(() => {
|
|
30938
31469
|
init_package_version();
|
|
30939
31470
|
init_types();
|
|
@@ -30952,6 +31483,8 @@ var init_openapi = __esm(() => {
|
|
|
30952
31483
|
reason: { type: "string", nullable: true },
|
|
30953
31484
|
tags: { type: "array", items: { type: "string" } },
|
|
30954
31485
|
version: { type: "number" },
|
|
31486
|
+
locked_by: { type: "string", nullable: true },
|
|
31487
|
+
locked_at: { type: "string", format: "date-time", nullable: true },
|
|
30955
31488
|
created_at: { type: "string" },
|
|
30956
31489
|
updated_at: { type: "string" }
|
|
30957
31490
|
}
|
|
@@ -31116,6 +31649,69 @@ var init_openapi = __esm(() => {
|
|
|
31116
31649
|
created_at: { type: "string", format: "date-time" }
|
|
31117
31650
|
}
|
|
31118
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
|
+
};
|
|
31119
31715
|
taskGitRefSchema = {
|
|
31120
31716
|
type: "object",
|
|
31121
31717
|
additionalProperties: false,
|
|
@@ -32357,6 +32953,45 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
32357
32953
|
return error(405, `method ${method} not allowed on /v1/tasks`);
|
|
32358
32954
|
}
|
|
32359
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
|
+
}
|
|
32360
32995
|
if (action === "comments") {
|
|
32361
32996
|
if (method === "GET") {
|
|
32362
32997
|
if (!await store.tasks.get(id))
|
|
@@ -33145,7 +33780,24 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
33145
33780
|
if (received === 0) {
|
|
33146
33781
|
return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
|
|
33147
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
|
+
}
|
|
33148
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
|
+
}
|
|
33149
33801
|
return json5({ result, received });
|
|
33150
33802
|
}
|
|
33151
33803
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
@@ -33168,6 +33820,14 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
33168
33820
|
if (e instanceof TaskNotFoundError) {
|
|
33169
33821
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
33170
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
|
+
}
|
|
33171
33831
|
if (e instanceof PlanNotFoundError) {
|
|
33172
33832
|
return error(404, e.message, { code: PlanNotFoundError.code });
|
|
33173
33833
|
}
|
|
@@ -33202,6 +33862,8 @@ var init_v1 = __esm(() => {
|
|
|
33202
33862
|
init_redaction();
|
|
33203
33863
|
init_project_task_list_ensure();
|
|
33204
33864
|
init_plan_project_link();
|
|
33865
|
+
init_stale_lock_handoff();
|
|
33866
|
+
init_audit_history_import();
|
|
33205
33867
|
JSON_HEADERS4 = { "Content-Type": "application/json" };
|
|
33206
33868
|
RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
33207
33869
|
});
|
|
@@ -63967,6 +64629,7 @@ var init_http_client = __esm(() => {
|
|
|
63967
64629
|
});
|
|
63968
64630
|
|
|
63969
64631
|
// src/cli/cloud-router.ts
|
|
64632
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
63970
64633
|
import { resolve as resolvePath } from "path";
|
|
63971
64634
|
function cleanMode(value) {
|
|
63972
64635
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -64369,7 +65032,10 @@ async function cloudGetTask(client, id) {
|
|
|
64369
65032
|
}
|
|
64370
65033
|
async function cloudCreateTask(client, input) {
|
|
64371
65034
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
64372
|
-
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"]));
|
|
64373
65039
|
if (!created || typeof created.id !== "string" || !created.id.trim()) {
|
|
64374
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");
|
|
64375
65041
|
}
|
|
@@ -64380,7 +65046,7 @@ async function cloudCreateTask(client, input) {
|
|
|
64380
65046
|
return persisted;
|
|
64381
65047
|
}
|
|
64382
65048
|
async function cloudUpdateTask(client, id, patch) {
|
|
64383
|
-
return unwrapTask(await client.
|
|
65049
|
+
return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
|
|
64384
65050
|
}
|
|
64385
65051
|
async function cloudDeleteTask(client, id) {
|
|
64386
65052
|
try {
|