@gajae-code/coding-agent 0.18.6 → 0.18.7
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/CHANGELOG.md +39 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/config/settings.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
- package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
- package/dist/types/sdk/host/host.d.ts +6 -0
- package/dist/types/sdk/router/session-router.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
- package/dist/types/session/session-retirement.d.ts +47 -0
- package/dist/types/session/session-storage.d.ts +0 -46
- package/dist/types/session/session-transcript-header.d.ts +2 -0
- package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
- package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
- package/dist/types/session/task-artifact-owner.d.ts +10 -0
- package/package.json +7 -7
- package/scripts/resident-memory-bench.ts +174 -20
- package/src/capability/index.ts +1 -0
- package/src/capability/types.ts +4 -0
- package/src/cli/config-cli.ts +8 -18
- package/src/cli-main.ts +7 -8
- package/src/commands/sdk.ts +112 -21
- package/src/config/settings.ts +44 -4
- package/src/discovery/helpers.ts +9 -3
- package/src/discovery/ssh.ts +8 -3
- package/src/gjc-runtime/gc-runtime.ts +2 -1
- package/src/hindsight/config.ts +9 -3
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/acp/acp-agent.ts +75 -3
- package/src/runtime-mcp/client.ts +13 -4
- package/src/runtime-mcp/elicitation.ts +142 -0
- package/src/sdk/broker/broker.ts +8 -1
- package/src/sdk/broker/lifecycle.ts +109 -7
- package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
- package/src/sdk/bus/index.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +4 -2
- package/src/sdk/host/host.ts +6 -0
- package/src/sdk/host/session-runtime.test.ts +43 -2
- package/src/sdk/host/session-runtime.ts +2 -2
- package/src/sdk/mcp/server.ts +15 -3
- package/src/sdk/router/session-router.ts +19 -2
- package/src/sdk/session.ts +14 -0
- package/src/session/agent-session.ts +57 -6
- package/src/session/internal/managed-session-storage.ts +107 -1
- package/src/session/internal/task-artifact-owner-access.ts +229 -0
- package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
- package/src/session/session-retirement.ts +155 -0
- package/src/session/session-storage.ts +1 -160
- package/src/session/session-transcript-header.ts +17 -0
- package/src/session/task-artifact-owner-codec.ts +578 -0
- package/src/session/task-artifact-owner-retirement.ts +475 -0
- package/src/session/task-artifact-owner.ts +289 -0
|
@@ -127,6 +127,7 @@ import {
|
|
|
127
127
|
streamSimple,
|
|
128
128
|
} from "@gajae-code/ai/core";
|
|
129
129
|
import { normalizeAnthropicBaseUrl } from "@gajae-code/ai/providers/anthropic";
|
|
130
|
+
import { isExplicitCodexTerminalVeto } from "@gajae-code/ai/providers/openai-codex-responses";
|
|
130
131
|
import {
|
|
131
132
|
type AuthDisposition,
|
|
132
133
|
beginAttempt,
|
|
@@ -1426,6 +1427,8 @@ const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream time
|
|
|
1426
1427
|
const BARE_DEFAULT_CODEX_RETRYABLE_ERROR =
|
|
1427
1428
|
/^Codex error event(?:: .*)? \(code=(?:server_is_overloaded|server_error|internal_error)(?:, [^)]+)*\)$/;
|
|
1428
1429
|
const BARE_DEFAULT_CODEX_RETRYABLE_CODES = new Set(["server_is_overloaded", "server_error", "internal_error"]);
|
|
1430
|
+
const BARE_DEFAULT_CODEX_OVERLOAD_ERROR =
|
|
1431
|
+
/^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
|
|
1429
1432
|
const BARE_DEFAULT_OVERLOAD_CODES = new Set([SERVER_OVERLOADED_PROVIDER_CODE]);
|
|
1430
1433
|
/** Anthropic's typed capacity-overload `error.type`, the only Anthropic overload code admitted below. */
|
|
1431
1434
|
const ANTHROPIC_OVERLOADED_ERROR_TYPE = "overloaded_error";
|
|
@@ -1555,9 +1558,38 @@ function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined
|
|
|
1555
1558
|
);
|
|
1556
1559
|
}
|
|
1557
1560
|
|
|
1561
|
+
// Deterministic auth/request/model diagnostics must surface even when a provider
|
|
1562
|
+
// labels the failure with a transient code.
|
|
1563
|
+
const TERMINAL_ERROR_MESSAGE =
|
|
1564
|
+
/unauthorized|forbidden|authentication_error|permission_error|permission denied|invalid api key|invalid_request_error|invalid request|bad request|bad_request|validation_error|unprocessable|payload too large|payment required|insufficient_quota|insufficient credits|missing required (parameter|field)|invalid schema|invalid tool_choice|unsupported (parameter|value|model)|model_not_found|no such model|unknown model|does not (exist|support)|request was aborted|request aborted|the user aborted/i;
|
|
1565
|
+
|
|
1566
|
+
function isTerminalCodexProviderFailure(message: AssistantMessage): boolean {
|
|
1567
|
+
if (message.api !== "openai-codex-responses") return false;
|
|
1568
|
+
if (isExplicitCodexTerminalVeto(message.transportFailure?.providerCode, message.errorMessage)) return true;
|
|
1569
|
+
return (
|
|
1570
|
+
(BARE_DEFAULT_CODEX_RETRYABLE_CODES.has(message.transportFailure?.providerCode ?? "") ||
|
|
1571
|
+
BARE_DEFAULT_CODEX_RETRYABLE_ERROR.test(message.errorMessage ?? "")) &&
|
|
1572
|
+
TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "")
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1558
1576
|
function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
|
|
1559
1577
|
return (
|
|
1560
1578
|
message.api === "openai-codex-responses" &&
|
|
1579
|
+
!TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "") &&
|
|
1580
|
+
!isExplicitCodexTerminalVeto(undefined, message.errorMessage) &&
|
|
1581
|
+
(isExactTypedOverloadFacts(message, BARE_DEFAULT_OVERLOAD_CODES) ||
|
|
1582
|
+
(BARE_DEFAULT_CODEX_OVERLOAD_ERROR.test(message.errorMessage ?? "") &&
|
|
1583
|
+
!hasBareDefaultRetryDisqualifyingFacts(message))) &&
|
|
1584
|
+
!assistantMessageHasVisibleOrToolContent(message)
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function isBareDefaultCodexProviderFailure(message: AssistantMessage): boolean {
|
|
1589
|
+
return (
|
|
1590
|
+
message.api === "openai-codex-responses" &&
|
|
1591
|
+
!TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "") &&
|
|
1592
|
+
!isExplicitCodexTerminalVeto(message.transportFailure?.providerCode, message.errorMessage) &&
|
|
1561
1593
|
(isExactTypedOverloadFacts(message, BARE_DEFAULT_CODEX_RETRYABLE_CODES) ||
|
|
1562
1594
|
(BARE_DEFAULT_CODEX_RETRYABLE_ERROR.test(message.errorMessage ?? "") &&
|
|
1563
1595
|
!hasBareDefaultRetryDisqualifyingFacts(message))) &&
|
|
@@ -1620,7 +1652,10 @@ function isBareDefaultAnthropicOverload(message: AssistantMessage): boolean {
|
|
|
1620
1652
|
function assistantMessageHasVisibleOrToolContent(message: AssistantMessage): boolean {
|
|
1621
1653
|
return message.content.some(content => {
|
|
1622
1654
|
if (content.type === "text") return content.text.length > 0;
|
|
1623
|
-
|
|
1655
|
+
if (content.type === "thinking") {
|
|
1656
|
+
return content.thinking.length > 0 || (content.thinkingSignature?.length ?? 0) > 0;
|
|
1657
|
+
}
|
|
1658
|
+
return content.type === "redactedThinking" || content.type === "toolCall";
|
|
1624
1659
|
});
|
|
1625
1660
|
}
|
|
1626
1661
|
|
|
@@ -22994,6 +23029,7 @@ export class AgentSession {
|
|
|
22994
23029
|
}
|
|
22995
23030
|
|
|
22996
23031
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
23032
|
+
if (isTerminalCodexProviderFailure(message)) return false;
|
|
22997
23033
|
if (this.#isTerminalProviderFirstEventTimeout(message)) return false;
|
|
22998
23034
|
if (message.errorMessage?.startsWith("Model fallback chain exhausted;")) return false;
|
|
22999
23035
|
if (message.errorMessage?.startsWith("Managed fallback retried the escaped non-ASCII")) return false;
|
|
@@ -23004,7 +23040,11 @@ export class AgentSession {
|
|
|
23004
23040
|
return false;
|
|
23005
23041
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
23006
23042
|
if (classifyContextOverflow(message, transportFailure, contextWindow)) return false;
|
|
23007
|
-
if (
|
|
23043
|
+
if (
|
|
23044
|
+
(isBareDefaultCodexOverload(message) || isBareDefaultCodexProviderFailure(message)) &&
|
|
23045
|
+
this.settings.get("retry.enabled") !== false
|
|
23046
|
+
)
|
|
23047
|
+
return true;
|
|
23008
23048
|
const managedFallback = this.#defaultFallbackChain().chain.entries.length > 1;
|
|
23009
23049
|
// An account-specific model rejection that cannot rotate to another
|
|
23010
23050
|
// credential stays terminal only on the session's own retry path; managed
|
|
@@ -23117,9 +23157,7 @@ export class AgentSession {
|
|
|
23117
23157
|
// Errors that will never succeed on retry (auth/permission, malformed
|
|
23118
23158
|
// request, unknown/unsupported model). These surface immediately rather
|
|
23119
23159
|
// than retry forever.
|
|
23120
|
-
return
|
|
23121
|
-
errorMessage,
|
|
23122
|
-
);
|
|
23160
|
+
return TERMINAL_ERROR_MESSAGE.test(errorMessage);
|
|
23123
23161
|
}
|
|
23124
23162
|
|
|
23125
23163
|
#extractExplicitHttpStatusFromErrorMessage(errorMessage: string): number | undefined {
|
|
@@ -24308,6 +24346,13 @@ export class AgentSession {
|
|
|
24308
24346
|
const classification = this.#classifyErrorForRetry(message);
|
|
24309
24347
|
const localSnapshot = classification === "local_snapshot";
|
|
24310
24348
|
const localBufferOverflow = classification === "local_buffer_overflow";
|
|
24349
|
+
// Managed outcomes can reach retry handling without the ordinary admission
|
|
24350
|
+
// check. Do not let retained transport facts bypass the same terminal veto.
|
|
24351
|
+
if (isTerminalCodexProviderFailure(message)) {
|
|
24352
|
+
return managedOutcome
|
|
24353
|
+
? { type: "terminal", terminal: { stopReason: "error", messages: [message] } }
|
|
24354
|
+
: false;
|
|
24355
|
+
}
|
|
24311
24356
|
if (retryCancelled()) {
|
|
24312
24357
|
return managedOutcome ? { type: "terminal", terminal: { stopReason: "cancelled" } } : false;
|
|
24313
24358
|
}
|
|
@@ -24369,6 +24414,8 @@ export class AgentSession {
|
|
|
24369
24414
|
isBareDefaultCodexOverload(message) ||
|
|
24370
24415
|
isBareDefaultAnthropicOverload(message) ||
|
|
24371
24416
|
isBareDefaultOpenAIResponsesOverload(message);
|
|
24417
|
+
const canReplayBareDefaultCodexFailure = isBareDefaultCodexProviderFailure(message);
|
|
24418
|
+
const boundBareDefaultCodexFailure = canReplayBareDefaultCodexFailure && !legacyRetryConfigured;
|
|
24372
24419
|
const canReplayCodexProviderOverload = isBareDefaultCodexOverload(message);
|
|
24373
24420
|
const reportedRetryMaxAttempts = transportFailure?.retryMaxAttempts;
|
|
24374
24421
|
if (reportedRetryMaxAttempts !== undefined) {
|
|
@@ -24412,7 +24459,9 @@ export class AgentSession {
|
|
|
24412
24459
|
const fallbackTrigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
24413
24460
|
const trigger:
|
|
24414
24461
|
| { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition }
|
|
24415
|
-
| undefined = canReplayCodexProviderOverload
|
|
24462
|
+
| undefined = canReplayCodexProviderOverload || canReplayBareDefaultCodexFailure
|
|
24463
|
+
? { class: "server" }
|
|
24464
|
+
: fallbackTrigger;
|
|
24416
24465
|
// OpenAI's typed statusless capacity-overload code (issue #5018) must not
|
|
24417
24466
|
// gain managed-chain retry/advance authority from its new facts. Before
|
|
24418
24467
|
// the code survived transport, this failure reached the session as an
|
|
@@ -24516,6 +24565,7 @@ export class AgentSession {
|
|
|
24516
24565
|
if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential && !canReplayEmptyResponse) {
|
|
24517
24566
|
if (
|
|
24518
24567
|
(!canReplayProviderOverload &&
|
|
24568
|
+
!canReplayBareDefaultCodexFailure &&
|
|
24519
24569
|
!canReplayUnexpectedSocketClose &&
|
|
24520
24570
|
!this.#isTypedFirstEventTimeout(message) &&
|
|
24521
24571
|
!messageOnlyWatchdogTimeout &&
|
|
@@ -24534,6 +24584,7 @@ export class AgentSession {
|
|
|
24534
24584
|
!managedFallback &&
|
|
24535
24585
|
classification === "transient" &&
|
|
24536
24586
|
!canReplayProviderOverload &&
|
|
24587
|
+
!boundBareDefaultCodexFailure &&
|
|
24537
24588
|
!canReplayUnexpectedSocketClose &&
|
|
24538
24589
|
!this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
|
|
24539
24590
|
|
|
@@ -1474,6 +1474,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1474
1474
|
retained?: { authority: RecoveryFsRoot; authorityBaseDir: string },
|
|
1475
1475
|
policy?: ManagedSessionSecurityPolicy,
|
|
1476
1476
|
profileAgentDir?: string,
|
|
1477
|
+
expectedSubtreeRoot?: ManagedDirectoryRoot,
|
|
1477
1478
|
) {
|
|
1478
1479
|
managedRelativePath(root, baseDir);
|
|
1479
1480
|
|
|
@@ -1512,13 +1513,46 @@ export class ManagedSessionDescendantStore {
|
|
|
1512
1513
|
});
|
|
1513
1514
|
}
|
|
1514
1515
|
this.#authority = retained.authority;
|
|
1516
|
+
if (
|
|
1517
|
+
expectedSubtreeRoot &&
|
|
1518
|
+
(expectedSubtreeRoot.canonicalPath !== this.#baseDir ||
|
|
1519
|
+
expectedSubtreeRoot.dev !== this.#subtreeRoot.dev ||
|
|
1520
|
+
expectedSubtreeRoot.ino !== this.#subtreeRoot.ino)
|
|
1521
|
+
)
|
|
1522
|
+
throw new Error("Managed subtree authority changed during establishment");
|
|
1515
1523
|
this.#assertBound();
|
|
1516
1524
|
|
|
1517
1525
|
return;
|
|
1518
1526
|
}
|
|
1519
1527
|
assertManagedDirectoryRoot(root);
|
|
1520
|
-
|
|
1528
|
+
if (expectedSubtreeRoot) {
|
|
1529
|
+
if (expectedSubtreeRoot.canonicalPath !== this.#baseDir)
|
|
1530
|
+
throw new Error("Managed subtree authority path mismatch");
|
|
1531
|
+
assertManagedDirectoryRoot(expectedSubtreeRoot);
|
|
1532
|
+
const verified = validateNativeSecurityResult(
|
|
1533
|
+
process.platform === "win32"
|
|
1534
|
+
? nativeSessionStorage().verifyOwnerOnlyPathSecurityExpected(
|
|
1535
|
+
this.#baseDir,
|
|
1536
|
+
"directory",
|
|
1537
|
+
expectedSubtreeRoot.dev,
|
|
1538
|
+
expectedSubtreeRoot.ino,
|
|
1539
|
+
)
|
|
1540
|
+
: nativeSessionStorage().verifyOwnerOnlyPathSecurity(this.#baseDir, "directory"),
|
|
1541
|
+
"verify",
|
|
1542
|
+
"directory",
|
|
1543
|
+
);
|
|
1544
|
+
if (!verified.ok) throw securityError(this.#baseDir, verified);
|
|
1545
|
+
assertManagedDirectoryRoot(expectedSubtreeRoot);
|
|
1546
|
+
} else {
|
|
1547
|
+
ensureManagedDirectory(this.#baseDir, root, this.#policy);
|
|
1548
|
+
}
|
|
1521
1549
|
const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
1550
|
+
if (
|
|
1551
|
+
expectedSubtreeRoot &&
|
|
1552
|
+
(canonicalFileId(subtreeStat.dev) !== expectedSubtreeRoot.dev ||
|
|
1553
|
+
canonicalFileId(subtreeStat.ino) !== expectedSubtreeRoot.ino)
|
|
1554
|
+
)
|
|
1555
|
+
throw new Error("Managed subtree authority changed during establishment");
|
|
1522
1556
|
this.#subtreeRoot = Object.freeze({
|
|
1523
1557
|
canonicalPath: this.#baseDir,
|
|
1524
1558
|
dev: canonicalFileId(subtreeStat.dev),
|
|
@@ -1634,6 +1668,50 @@ export class ManagedSessionDescendantStore {
|
|
|
1634
1668
|
}
|
|
1635
1669
|
this.#assertBound();
|
|
1636
1670
|
}
|
|
1671
|
+
|
|
1672
|
+
/** Capture a directory identity through this store's retained managed root without repairing it. */
|
|
1673
|
+
captureDirectoryIdentity(relativePath: string): { dev: string; ino: string } {
|
|
1674
|
+
this.#assertBound();
|
|
1675
|
+
const resolved = this.#resolve(relativePath);
|
|
1676
|
+
if (!this.#authority) this.#assertPathBackedDirectoryChain(resolved);
|
|
1677
|
+
const named = fs.lstatSync(resolved, { bigint: true });
|
|
1678
|
+
if (!named.isDirectory() || named.isSymbolicLink()) throw new Error("managed_directory_identity_unavailable");
|
|
1679
|
+
const dev = canonicalFileId(named.dev);
|
|
1680
|
+
const ino = canonicalFileId(named.ino);
|
|
1681
|
+
if (dev !== this.#subtreeRoot.dev) throw new Error("managed_directory_identity_unavailable");
|
|
1682
|
+
if (this.#authority) {
|
|
1683
|
+
const retainedRelative = this.#relative(resolved);
|
|
1684
|
+
if (retainedRelative === "") {
|
|
1685
|
+
if (dev !== this.#subtreeRoot.dev || ino !== this.#subtreeRoot.ino)
|
|
1686
|
+
throw new Error("Managed descendant root binding changed");
|
|
1687
|
+
} else {
|
|
1688
|
+
const retained = this.#authority.retainManagedDirectory(retainedRelative, dev.toString(), ino.toString());
|
|
1689
|
+
try {
|
|
1690
|
+
const identity = retained.identity();
|
|
1691
|
+
if (
|
|
1692
|
+
!identity.ok ||
|
|
1693
|
+
!identity.identity ||
|
|
1694
|
+
identity.identity.dev !== dev.toString() ||
|
|
1695
|
+
identity.identity.ino !== ino.toString()
|
|
1696
|
+
)
|
|
1697
|
+
throw new Error(identity.code ?? "managed_directory_identity_unavailable");
|
|
1698
|
+
} finally {
|
|
1699
|
+
retained.close();
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
const after = fs.lstatSync(resolved, { bigint: true });
|
|
1704
|
+
if (
|
|
1705
|
+
!after.isDirectory() ||
|
|
1706
|
+
after.isSymbolicLink() ||
|
|
1707
|
+
canonicalFileId(after.dev) !== dev ||
|
|
1708
|
+
canonicalFileId(after.ino) !== ino
|
|
1709
|
+
)
|
|
1710
|
+
throw new Error("managed_directory_identity_changed");
|
|
1711
|
+
this.#assertBound();
|
|
1712
|
+
return { dev: dev.toString(), ino: ino.toString() };
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1637
1715
|
#assertBound(): void {
|
|
1638
1716
|
if (!this.#authority) {
|
|
1639
1717
|
const named = fs.statSync(this.#baseDir, { bigint: true });
|
|
@@ -3019,6 +3097,34 @@ export class ManagedSessionDescendantStore {
|
|
|
3019
3097
|
if (!removed.ok) throw new Error(removed.code ?? "managed_remove_failed");
|
|
3020
3098
|
this.#assertBound();
|
|
3021
3099
|
}
|
|
3100
|
+
|
|
3101
|
+
/** Remove one exact managed tree through the parent-identity-bound native protocol. */
|
|
3102
|
+
removeTreeExpectedWithParentIdentity(
|
|
3103
|
+
relativePath: string,
|
|
3104
|
+
expected: NativeDirectoryTreeSnapshot,
|
|
3105
|
+
parentIdentity: { dev: bigint; ino: bigint },
|
|
3106
|
+
): NativeExactUnlinkResult {
|
|
3107
|
+
this.#beforeMutation();
|
|
3108
|
+
this.#assertBound();
|
|
3109
|
+
const resolved = this.#resolve(relativePath);
|
|
3110
|
+
const parentPath = path.dirname(resolved);
|
|
3111
|
+
const parentRelative = path.relative(this.#baseDir, parentPath);
|
|
3112
|
+
if (path.isAbsolute(parentRelative) || parentRelative === ".." || parentRelative.startsWith(`..${path.sep}`))
|
|
3113
|
+
throw new Error("managed_remove_parent_outside_store");
|
|
3114
|
+
const currentParent = this.captureDirectoryIdentity(parentRelative);
|
|
3115
|
+
if (
|
|
3116
|
+
currentParent.dev !== canonicalFileId(parentIdentity.dev).toString() ||
|
|
3117
|
+
currentParent.ino !== canonicalFileId(parentIdentity.ino).toString()
|
|
3118
|
+
)
|
|
3119
|
+
throw new Error("managed_remove_parent_identity_mismatch");
|
|
3120
|
+
const removed = nativeSessionStorage().exactRemoveDirectoryTree(resolved, expected, {
|
|
3121
|
+
dev: BigInt(currentParent.dev),
|
|
3122
|
+
ino: BigInt(currentParent.ino),
|
|
3123
|
+
});
|
|
3124
|
+
this.#assertBound();
|
|
3125
|
+
return removed;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3022
3128
|
fsyncTree(): NativeDirectoryTreeSnapshot {
|
|
3023
3129
|
this.#beforeMutation();
|
|
3024
3130
|
this.#assertBound();
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
|
|
4
|
+
import {
|
|
5
|
+
assertSafeRelativePath,
|
|
6
|
+
assertSessionRoot,
|
|
7
|
+
freezeTreeSnapshot,
|
|
8
|
+
immutableDeletionEvidence,
|
|
9
|
+
isOwnerRetainedRoot,
|
|
10
|
+
manifestRelativePath,
|
|
11
|
+
OWNER_DIRECTORY,
|
|
12
|
+
OWNER_MANIFEST,
|
|
13
|
+
OWNER_SCHEMA_VERSION,
|
|
14
|
+
ownerIdForSession,
|
|
15
|
+
ownerRelativePath,
|
|
16
|
+
parseTaskArtifactOwnerLocator,
|
|
17
|
+
sameOwnerParentIdentity,
|
|
18
|
+
type TaskArtifactOwnerDeletionEvidence,
|
|
19
|
+
type TaskArtifactOwnerLocator,
|
|
20
|
+
type TaskArtifactOwnerParentIdentity,
|
|
21
|
+
type TaskArtifactOwnerStorageContext,
|
|
22
|
+
} from "../task-artifact-owner-codec";
|
|
23
|
+
import { ManagedSessionDescendantStore } from "./managed-session-storage";
|
|
24
|
+
|
|
25
|
+
export interface OwnerManifest extends TaskArtifactOwnerLocator {
|
|
26
|
+
readonly sessionId: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function newSessionRootStore(context: TaskArtifactOwnerStorageContext): ManagedSessionDescendantStore {
|
|
30
|
+
assertSessionRoot(context);
|
|
31
|
+
if (path.resolve(context.profileAgentDir) !== context.profileAgentDir)
|
|
32
|
+
throw new Error("task_artifact_owner_profile_invalid");
|
|
33
|
+
const profile = fs.lstatSync(context.profileAgentDir, { bigint: true });
|
|
34
|
+
if (!profile.isDirectory() || profile.isSymbolicLink()) throw new Error("task_artifact_owner_profile_invalid");
|
|
35
|
+
const stat = fs.lstatSync(context.sessionsRoot, { bigint: true });
|
|
36
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("task_artifact_owner_root_invalid");
|
|
37
|
+
return new ManagedSessionDescendantStore(
|
|
38
|
+
context.rootAuthority,
|
|
39
|
+
context.sessionsRoot,
|
|
40
|
+
undefined,
|
|
41
|
+
context.securityPolicy,
|
|
42
|
+
context.profileAgentDir,
|
|
43
|
+
{
|
|
44
|
+
canonicalPath: context.sessionsRoot,
|
|
45
|
+
dev: BigInt.asUintN(64, stat.dev),
|
|
46
|
+
ino: BigInt.asUintN(64, stat.ino),
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseOwnerManifest(bytes: Uint8Array): OwnerManifest {
|
|
52
|
+
let parsed: unknown;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error("task_artifact_owner_manifest_invalid");
|
|
57
|
+
}
|
|
58
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || Object.keys(parsed).length !== 5)
|
|
59
|
+
throw new Error("task_artifact_owner_manifest_invalid");
|
|
60
|
+
const record = parsed as Record<string, unknown>;
|
|
61
|
+
if (typeof record.sessionId !== "string" || record.sessionId.length === 0)
|
|
62
|
+
throw new Error("task_artifact_owner_manifest_invalid");
|
|
63
|
+
let locator: TaskArtifactOwnerLocator | undefined;
|
|
64
|
+
try {
|
|
65
|
+
locator = parseTaskArtifactOwnerLocator({
|
|
66
|
+
schemaVersion: record.schemaVersion,
|
|
67
|
+
ownerId: record.ownerId,
|
|
68
|
+
directoryDev: record.directoryDev,
|
|
69
|
+
directoryIno: record.directoryIno,
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
throw new Error("task_artifact_owner_manifest_invalid");
|
|
73
|
+
}
|
|
74
|
+
if (!locator) throw new Error("task_artifact_owner_manifest_invalid");
|
|
75
|
+
return { ...locator, sessionId: record.sessionId };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function readOwnerManifest(
|
|
79
|
+
store: ManagedSessionDescendantStore,
|
|
80
|
+
locator: TaskArtifactOwnerLocator,
|
|
81
|
+
relativePath = manifestRelativePath(locator.ownerId),
|
|
82
|
+
): OwnerManifest {
|
|
83
|
+
const manifest = store.readExpected(relativePath);
|
|
84
|
+
if (!manifest) throw new Error("task_artifact_owner_manifest_missing");
|
|
85
|
+
const parsed = parseOwnerManifest(manifest.bytes);
|
|
86
|
+
if (
|
|
87
|
+
parsed.ownerId !== locator.ownerId ||
|
|
88
|
+
parsed.directoryDev !== locator.directoryDev ||
|
|
89
|
+
parsed.directoryIno !== locator.directoryIno
|
|
90
|
+
)
|
|
91
|
+
throw new Error("task_artifact_owner_manifest_mismatch");
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function assertOwnerDirectoryExists(context: TaskArtifactOwnerStorageContext, ownerId: string): void {
|
|
96
|
+
const parent = path.join(context.sessionsRoot, OWNER_DIRECTORY);
|
|
97
|
+
const owner = path.join(parent, ownerId);
|
|
98
|
+
for (const candidate of [parent, owner]) {
|
|
99
|
+
let stat: fs.BigIntStats;
|
|
100
|
+
try {
|
|
101
|
+
stat = fs.lstatSync(candidate, { bigint: true });
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new Error("task_artifact_owner_missing");
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("task_artifact_owner_replaced");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function assertOwnerIdentity(store: ManagedSessionDescendantStore, locator: TaskArtifactOwnerLocator): void {
|
|
111
|
+
const identity = store.subtreeRootAuthority;
|
|
112
|
+
if (identity.dev.toString() !== locator.directoryDev || identity.ino.toString() !== locator.directoryIno)
|
|
113
|
+
throw new Error("task_artifact_owner_identity_mismatch");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function openOwnerStore(
|
|
117
|
+
context: TaskArtifactOwnerStorageContext,
|
|
118
|
+
rootStore: ManagedSessionDescendantStore,
|
|
119
|
+
locator: TaskArtifactOwnerLocator,
|
|
120
|
+
sessionId: string,
|
|
121
|
+
): ManagedSessionDescendantStore {
|
|
122
|
+
if (locator.ownerId !== ownerIdForSession(sessionId)) throw new Error("task_artifact_owner_session_mismatch");
|
|
123
|
+
assertOwnerDirectoryExists(context, locator.ownerId);
|
|
124
|
+
const manifest = readOwnerManifest(rootStore, locator);
|
|
125
|
+
if (manifest.sessionId !== sessionId) throw new Error("task_artifact_owner_session_mismatch");
|
|
126
|
+
const identity = rootStore.captureDirectoryIdentity(ownerRelativePath(locator.ownerId));
|
|
127
|
+
if (identity.dev !== locator.directoryDev || identity.ino !== locator.directoryIno)
|
|
128
|
+
throw new Error("task_artifact_owner_identity_mismatch");
|
|
129
|
+
const ownerPath = path.join(context.sessionsRoot, ownerRelativePath(locator.ownerId));
|
|
130
|
+
const ownerStore = new ManagedSessionDescendantStore(
|
|
131
|
+
context.rootAuthority,
|
|
132
|
+
ownerPath,
|
|
133
|
+
undefined,
|
|
134
|
+
context.securityPolicy,
|
|
135
|
+
context.profileAgentDir,
|
|
136
|
+
{
|
|
137
|
+
canonicalPath: ownerPath,
|
|
138
|
+
dev: BigInt(locator.directoryDev),
|
|
139
|
+
ino: BigInt(locator.directoryIno),
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
try {
|
|
143
|
+
assertOwnerIdentity(ownerStore, locator);
|
|
144
|
+
readOwnerManifest(ownerStore, locator, OWNER_MANIFEST);
|
|
145
|
+
return ownerStore;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
ownerStore.close();
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function locatorFromManifest(manifest: OwnerManifest): TaskArtifactOwnerLocator {
|
|
153
|
+
return {
|
|
154
|
+
schemaVersion: OWNER_SCHEMA_VERSION,
|
|
155
|
+
ownerId: manifest.ownerId,
|
|
156
|
+
directoryDev: manifest.directoryDev,
|
|
157
|
+
directoryIno: manifest.directoryIno,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function captureValidatedOwnerTree(
|
|
162
|
+
context: TaskArtifactOwnerStorageContext,
|
|
163
|
+
rootStore: ManagedSessionDescendantStore,
|
|
164
|
+
sessionId: string,
|
|
165
|
+
locator: TaskArtifactOwnerLocator,
|
|
166
|
+
): NativeDirectoryTreeSnapshot {
|
|
167
|
+
if (locator.ownerId !== ownerIdForSession(sessionId)) throw new Error("task_artifact_owner_session_mismatch");
|
|
168
|
+
assertOwnerDirectoryExists(context, locator.ownerId);
|
|
169
|
+
const manifest = readOwnerManifest(rootStore, locator);
|
|
170
|
+
if (manifest.sessionId !== sessionId) throw new Error("task_artifact_owner_session_mismatch");
|
|
171
|
+
const tree = rootStore.captureTree(ownerRelativePath(locator.ownerId));
|
|
172
|
+
if (tree.rootDev !== locator.directoryDev || tree.rootIno !== locator.directoryIno)
|
|
173
|
+
throw new Error("task_artifact_owner_identity_mismatch");
|
|
174
|
+
return tree;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function captureOwnerTreeIfPresent(
|
|
178
|
+
context: TaskArtifactOwnerStorageContext,
|
|
179
|
+
rootStore: ManagedSessionDescendantStore,
|
|
180
|
+
ownerId: string,
|
|
181
|
+
pathname: string,
|
|
182
|
+
): NativeDirectoryTreeSnapshot | undefined {
|
|
183
|
+
if (!isOwnerRetainedRoot(context, ownerId, pathname))
|
|
184
|
+
throw new Error("task_artifact_owner_retained_root_unrecognized");
|
|
185
|
+
let named: fs.BigIntStats;
|
|
186
|
+
try {
|
|
187
|
+
named = fs.lstatSync(pathname, { bigint: true });
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
if (!named.isDirectory() || named.isSymbolicLink()) throw new Error("task_artifact_owner_retained_root_replaced");
|
|
193
|
+
const relative = path.relative(context.sessionsRoot, pathname).split(path.sep).join("/");
|
|
194
|
+
assertSafeRelativePath(relative);
|
|
195
|
+
return freezeTreeSnapshot(rootStore.captureTree(relative));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function ownerTreeHasPendingManagedPublication(snapshot: NativeDirectoryTreeSnapshot): boolean {
|
|
199
|
+
return snapshot.entries.some(entry =>
|
|
200
|
+
/^\..+\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(?:staging|replacement)$/u.test(
|
|
201
|
+
path.posix.basename(entry.relativePath),
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Capture exact owner evidence without repairing its security, timestamps, or namespace. */
|
|
207
|
+
export function captureTaskArtifactOwnerDeletionEvidence(
|
|
208
|
+
context: TaskArtifactOwnerStorageContext,
|
|
209
|
+
sessionId: string,
|
|
210
|
+
locatorValue: unknown,
|
|
211
|
+
): TaskArtifactOwnerDeletionEvidence | undefined {
|
|
212
|
+
const locator = parseTaskArtifactOwnerLocator(locatorValue);
|
|
213
|
+
if (!locator) return undefined;
|
|
214
|
+
const rootStore = newSessionRootStore(context);
|
|
215
|
+
try {
|
|
216
|
+
rootStore.verifyRootSecurity();
|
|
217
|
+
const parentIdentity: TaskArtifactOwnerParentIdentity = rootStore.captureDirectoryIdentity(OWNER_DIRECTORY);
|
|
218
|
+
const treeSnapshot = captureValidatedOwnerTree(context, rootStore, sessionId, locator);
|
|
219
|
+
// This refuses to capture during publication; it does not revoke independent open descriptors.
|
|
220
|
+
if (ownerTreeHasPendingManagedPublication(treeSnapshot))
|
|
221
|
+
throw new Error("task_artifact_owner_writer_not_quiescent");
|
|
222
|
+
const parentAfter = rootStore.captureDirectoryIdentity(OWNER_DIRECTORY);
|
|
223
|
+
if (!sameOwnerParentIdentity(parentIdentity, parentAfter))
|
|
224
|
+
throw new Error("task_artifact_owner_parent_changed_during_capture");
|
|
225
|
+
return immutableDeletionEvidence(sessionId, locator, parentIdentity, treeSnapshot);
|
|
226
|
+
} finally {
|
|
227
|
+
rootStore.close();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { parseFirstJsonlLine } from "../session-transcript-header";
|
|
2
|
+
import { parseTaskArtifactOwnerLocator, type TaskArtifactOwnerLocator } from "../task-artifact-owner-codec";
|
|
3
|
+
|
|
4
|
+
const transcriptDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
5
|
+
|
|
6
|
+
/** Resolve the path-free owner locator using transcript replay's header-patch semantics. */
|
|
7
|
+
export function taskArtifactOwnerLocatorFromTranscriptBytes(
|
|
8
|
+
bytes: Uint8Array,
|
|
9
|
+
expectedSessionId: string,
|
|
10
|
+
): TaskArtifactOwnerLocator | undefined {
|
|
11
|
+
const header = parseFirstJsonlLine(bytes);
|
|
12
|
+
if (header?.type !== "session" || header.id !== expectedSessionId)
|
|
13
|
+
throw new Error("task_artifact_owner_transcript_header_invalid");
|
|
14
|
+
let locator = parseTaskArtifactOwnerLocator(header.taskArtifactOwner);
|
|
15
|
+
if (typeof header.version !== "number" || header.version < 4) return locator;
|
|
16
|
+
const firstEnd = bytes.indexOf(0x0a);
|
|
17
|
+
let start = firstEnd < 0 ? bytes.byteLength : firstEnd + 1;
|
|
18
|
+
while (start < bytes.byteLength) {
|
|
19
|
+
const newline = bytes.indexOf(0x0a, start);
|
|
20
|
+
const end = newline < 0 ? bytes.byteLength : newline;
|
|
21
|
+
const line = bytes.subarray(start, end);
|
|
22
|
+
let record: Record<string, unknown> | undefined;
|
|
23
|
+
try {
|
|
24
|
+
if (line.byteLength > 0) {
|
|
25
|
+
const parsed: unknown = JSON.parse(transcriptDecoder.decode(line));
|
|
26
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
|
|
27
|
+
record = parsed as Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
const text = Buffer.from(line).toString("utf8");
|
|
31
|
+
if (text.includes("header_patch") && text.includes("taskArtifactOwner"))
|
|
32
|
+
throw new Error("task_artifact_owner_patch_invalid");
|
|
33
|
+
}
|
|
34
|
+
if (
|
|
35
|
+
record?.type === "header_patch" &&
|
|
36
|
+
typeof record.patch === "object" &&
|
|
37
|
+
record.patch !== null &&
|
|
38
|
+
!Array.isArray(record.patch) &&
|
|
39
|
+
Object.hasOwn(record.patch, "taskArtifactOwner")
|
|
40
|
+
) {
|
|
41
|
+
const patch = record.patch as Record<string, unknown>;
|
|
42
|
+
if (
|
|
43
|
+
!Object.keys(record).every(key => key === "type" || key === "patch") ||
|
|
44
|
+
!Object.keys(patch).every(
|
|
45
|
+
key =>
|
|
46
|
+
key === "cwd" ||
|
|
47
|
+
key === "title" ||
|
|
48
|
+
key === "titleSource" ||
|
|
49
|
+
key === "starred" ||
|
|
50
|
+
key === "taskArtifactOwner",
|
|
51
|
+
) ||
|
|
52
|
+
(patch.cwd !== undefined && typeof patch.cwd !== "string") ||
|
|
53
|
+
(patch.title !== undefined && typeof patch.title !== "string") ||
|
|
54
|
+
(patch.titleSource !== undefined && patch.titleSource !== "auto" && patch.titleSource !== "user") ||
|
|
55
|
+
(patch.starred !== undefined && typeof patch.starred !== "boolean")
|
|
56
|
+
)
|
|
57
|
+
throw new Error("task_artifact_owner_patch_invalid");
|
|
58
|
+
try {
|
|
59
|
+
locator = parseTaskArtifactOwnerLocator(patch.taskArtifactOwner);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error("task_artifact_owner_patch_invalid");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (newline < 0) break;
|
|
65
|
+
start = newline + 1;
|
|
66
|
+
}
|
|
67
|
+
return locator;
|
|
68
|
+
}
|