@opengeni/db 5.0.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/automations.d.ts +0 -2
- package/dist/canonical-human-identities.d.ts +4 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-components.d.ts +5 -13
- package/dist/capability-integrations.d.ts +23 -8
- package/dist/capability-reconciliation.d.ts +5 -0
- package/dist/capability-reconciliation.js +21 -0
- package/dist/capability-reconciliation.js.map +1 -0
- package/dist/{chunk-6T2GGYWE.js → chunk-4UJCXBR2.js} +4 -5800
- package/dist/chunk-4UJCXBR2.js.map +1 -0
- package/dist/{chunk-MWMXWHJD.js → chunk-7JCDSMX2.js} +2 -2
- package/dist/chunk-AJA2ZIUW.js +219 -0
- package/dist/chunk-AJA2ZIUW.js.map +1 -0
- package/dist/chunk-CDAT6EKW.js +247 -0
- package/dist/chunk-CDAT6EKW.js.map +1 -0
- package/dist/{chunk-DPONPYJD.js → chunk-CJI6G254.js} +70 -18
- package/dist/chunk-CJI6G254.js.map +1 -0
- package/dist/chunk-G32NVMRJ.js +276 -0
- package/dist/chunk-G32NVMRJ.js.map +1 -0
- package/dist/chunk-GABXCG3N.js +5811 -0
- package/dist/chunk-GABXCG3N.js.map +1 -0
- package/dist/{chunk-ERKVSPDX.js → chunk-LVY33EWL.js} +3 -3
- package/dist/{chunk-45CP2YKY.js → chunk-MAMBI2HR.js} +9 -3
- package/dist/{chunk-45CP2YKY.js.map → chunk-MAMBI2HR.js.map} +1 -1
- package/dist/{chunk-F73UKJY6.js → chunk-MN5PZMEK.js} +91 -108
- package/dist/chunk-MN5PZMEK.js.map +1 -0
- package/dist/chunk-PJI4MUZ7.js +131 -0
- package/dist/chunk-PJI4MUZ7.js.map +1 -0
- package/dist/{chunk-T6BWIGWJ.js → chunk-RIYHP2WU.js} +2 -2
- package/dist/{chunk-7HMK3B45.js → chunk-RL2MUXM3.js} +16 -7
- package/dist/chunk-RL2MUXM3.js.map +1 -0
- package/dist/{chunk-HQDVI2DB.js → chunk-SYKHLPTK.js} +4 -3
- package/dist/{chunk-HQDVI2DB.js.map → chunk-SYKHLPTK.js.map} +1 -1
- package/dist/{chunk-T6YLHJLS.js → chunk-XIXVVBW2.js} +198 -51
- package/dist/chunk-XIXVVBW2.js.map +1 -0
- package/dist/chunk-ZPJ6V2VG.js +156 -0
- package/dist/chunk-ZPJ6V2VG.js.map +1 -0
- package/dist/codex-capacity-recovery.d.ts +12 -0
- package/dist/codex-failure-eligibility.d.ts +9 -0
- package/dist/company-profile-schema.d.ts +4 -2
- package/dist/connect-attempts.d.ts +15 -0
- package/dist/connect-attempts.js +25 -0
- package/dist/connect-attempts.js.map +1 -0
- package/dist/connection-authority.d.ts +7 -63
- package/dist/connection-refresh-lock.d.ts +4 -0
- package/dist/connection-token-resolver.d.ts +8 -43
- package/dist/database-timing.d.ts +10 -0
- package/dist/database.d.ts +4 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.d.ts +9 -0
- package/dist/editable-artifacts.js +7 -5
- package/dist/index.d.ts +447 -78
- package/dist/index.js +12705 -12872
- package/dist/index.js.map +1 -1
- package/dist/insights-usage-bundle.d.ts +3 -0
- package/dist/integration-bindings.d.ts +1 -1
- package/dist/integration-facets.d.ts +18 -1
- package/dist/knowledge-entries-schema.d.ts +2 -0
- package/dist/knowledge-entries.d.ts +1 -0
- package/dist/knowledge-indexing.d.ts +1 -0
- package/dist/live-session-attempt.d.ts +35 -137
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/mcp-account-bindings.d.ts +4 -0
- package/dist/mcp-oauth.d.ts +11 -0
- package/dist/mcp-operations.js +2 -2
- package/dist/migrate.js +1 -1
- package/dist/organization-integration-policy.d.ts +51 -0
- package/dist/organization-integration-policy.js +18 -0
- package/dist/organization-integration-policy.js.map +1 -0
- package/dist/organization-usage.d.ts +20 -0
- package/dist/plugin-packages.d.ts +9 -12
- package/dist/pr-review.d.ts +0 -11
- package/dist/preference-registry-schema.d.ts +17 -0
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.d.ts +124 -6
- package/dist/retained-provider-commands.js +29 -97
- package/dist/retained-provider-commands.js.map +1 -1
- package/dist/retired-session-create-metadata.d.ts +4 -0
- package/dist/runtime-posture.d.ts +7 -5
- package/dist/schema.d.ts +876 -656
- package/dist/schema.js +5 -7
- package/dist/session-admission-block.d.ts +32 -0
- package/dist/session-attempt-writers.d.ts +4 -0
- package/dist/session-background-commands.js +5 -4
- package/dist/session-command-output.js +8 -126
- package/dist/session-command-output.js.map +1 -1
- package/dist/session-control.d.ts +5 -1
- package/dist/session-event-slices.d.ts +10 -0
- package/dist/session-event-slices.js +8 -222
- package/dist/session-event-slices.js.map +1 -1
- package/dist/session-file-attachments.d.ts +28 -0
- package/dist/session-goal-reports.d.ts +43 -0
- package/dist/session-mcp-approval.d.ts +4 -0
- package/dist/session-mcp-credential-rotation.js +5 -4
- package/dist/session-mcp-credential-rotation.js.map +1 -1
- package/dist/session-message-search.d.ts +22 -0
- package/dist/session-queue-commands.d.ts +2 -6
- package/dist/session-retry.d.ts +36 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/skill-source-release-impact.d.ts +18 -0
- package/dist/skill-source-release.d.ts +9 -0
- package/dist/skills.d.ts +7 -1
- package/dist/user-resource-authority.d.ts +0 -8
- package/dist/video-generation.js +3 -3
- package/dist/workspace-tool-defaults.js +5 -4
- package/dist/workspace-tool-defaults.js.map +1 -1
- package/drizzle/0468_knowledge_relationship_projection.sql +30 -0
- package/drizzle/0469_knowledge_source_discovery.sql +85 -0
- package/drizzle/0470_organization_integration_policy.sql +151 -0
- package/drizzle/0471_usage_events_account_recent_index.sql +4 -0
- package/drizzle/0472_usage_events_workspace_recent_index.sql +4 -0
- package/drizzle/0473_organization_usage_analytical_capability.sql +237 -0
- package/drizzle/0474_goal_report_requirements.sql +156 -0
- package/drizzle/0475_session_event_history_statistics.sql +15 -0
- package/drizzle/0476_session_event_history_policy_planning.sql +29 -0
- package/drizzle/0477_managed_sign_in_methods.sql +303 -0
- package/drizzle/0478_sender_owned_connections.sql +2772 -0
- package/drizzle/0479_connection_create_idempotency.sql +16 -0
- package/drizzle/0480_private_session_organization_key_administration.sql +154 -0
- package/drizzle/0481_personal_connection_visibility_cleanup.sql +45 -0
- package/drizzle/0482_remove_packs.sql +417 -0
- package/drizzle/0483_preclaim_admission_block.sql +85 -0
- package/drizzle/0484_insights_usage_projection.sql +211 -0
- package/drizzle/0485_inherited_mcp_approval_policies.sql +6 -0
- package/drizzle/0486_reasoning_configuration_message_forks.sql +35 -0
- package/drizzle/0487_skill_catalog_message_forks.sql +26 -0
- package/drizzle/0488_permanent_skill_removal.sql +207 -0
- package/drizzle/0489_modal_command_byte_offsets.sql +17 -0
- package/drizzle/0490_periodic_capture_attempt_cadence.sql +6 -0
- package/drizzle/0491_warm_capture_holder_reclamation.sql +57 -0
- package/drizzle/0492_codex_accepted_source_authority.sql +249 -0
- package/drizzle/0493_stopping_command_error_containment.sql +58 -0
- package/drizzle/0494_mcp_account_bindings.sql +334 -0
- package/drizzle/0495_consented_sandbox_recovery.sql +220 -0
- package/drizzle/0496_supervised_command_settlement.sql +386 -0
- package/drizzle/0497_modal_inventory_owner_read_capability.sql +112 -0
- package/drizzle/0498_enrollment_membership_fence.sql +87 -0
- package/drizzle/0499_session_attachment_access.sql +224 -0
- package/drizzle/0500_session_execution_epoch.sql +37 -0
- package/drizzle/0501_session_sharing_execution.sql +591 -0
- package/drizzle/0502_active_message_boundary_forks.sql +30 -0
- package/package.json +17 -5
- package/src/automations.ts +17 -73
- package/src/canonical-human-identities.ts +17 -5
- package/src/capability-components.ts +7 -24
- package/src/capability-integrations.ts +301 -21
- package/src/capability-reconciliation.ts +19 -0
- package/src/codex-capacity-recovery.ts +47 -0
- package/src/codex-failure-eligibility.ts +93 -0
- package/src/connect-attempts.ts +49 -18
- package/src/connection-authority.ts +12 -158
- package/src/connection-refresh-lock.ts +22 -0
- package/src/connection-token-resolver.ts +58 -530
- package/src/database-timing.ts +43 -0
- package/src/database.ts +61 -26
- package/src/editable-artifacts.ts +1 -1
- package/src/index.ts +3578 -1273
- package/src/insights-model-bundle.ts +19 -7
- package/src/insights-usage-bundle.ts +16 -19
- package/src/integration-bindings.ts +2 -7
- package/src/integration-facets.ts +73 -6
- package/src/knowledge-entries.ts +8 -1
- package/src/live-session-attempt.ts +0 -22
- package/src/mcp-account-bindings.ts +12 -0
- package/src/mcp-oauth.ts +26 -0
- package/src/organization-integration-policy.ts +235 -0
- package/src/organization-usage.ts +83 -0
- package/src/plugin-packages.ts +226 -42
- package/src/pr-review.ts +7 -69
- package/src/preference-registry-schema.ts +1 -0
- package/src/provision-roles.ts +50 -37
- package/src/retained-provider-commands.ts +286 -9
- package/src/retired-session-create-metadata.ts +16 -0
- package/src/runtime-posture.ts +217 -13
- package/src/schema.ts +104 -120
- package/src/session-admission-block.ts +29 -0
- package/src/session-attempt-writers.ts +48 -0
- package/src/session-control.ts +21 -5
- package/src/session-event-slices.ts +76 -26
- package/src/session-file-attachments.ts +66 -0
- package/src/session-goal-reports.ts +255 -0
- package/src/session-mcp-approval.ts +30 -0
- package/src/session-message-search.ts +348 -0
- package/src/session-queue-commands.ts +60 -36
- package/src/session-retry.ts +319 -0
- package/src/skill-config-migration.ts +11 -2
- package/src/skill-source-release-impact.ts +33 -0
- package/src/skill-source-release.ts +67 -26
- package/src/skills.ts +44 -6
- package/src/user-resource-authority.ts +0 -56
- package/dist/chunk-6T2GGYWE.js.map +0 -1
- package/dist/chunk-7HMK3B45.js.map +0 -1
- package/dist/chunk-DPONPYJD.js.map +0 -1
- package/dist/chunk-F73UKJY6.js.map +0 -1
- package/dist/chunk-T6YLHJLS.js.map +0 -1
- package/dist/host-mcp-binding-match.d.ts +0 -5
- package/dist/host-mcp-bindings.d.ts +0 -79
- package/dist/host-mcp-resolvers.d.ts +0 -47
- package/dist/host-mcp-task-authority.d.ts +0 -43
- package/dist/host-selection-identity.d.ts +0 -6
- package/dist/pack-components.d.ts +0 -83
- package/dist/pack-installations.d.ts +0 -86
- package/src/host-mcp-binding-match.ts +0 -34
- package/src/host-mcp-bindings.ts +0 -777
- package/src/host-mcp-resolvers.ts +0 -266
- package/src/host-mcp-task-authority.ts +0 -384
- package/src/host-selection-identity.ts +0 -29
- package/src/pack-components.ts +0 -980
- package/src/pack-installations.ts +0 -950
- /package/dist/{chunk-MWMXWHJD.js.map → chunk-7JCDSMX2.js.map} +0 -0
- /package/dist/{chunk-ERKVSPDX.js.map → chunk-LVY33EWL.js.map} +0 -0
- /package/dist/{chunk-T6BWIGWJ.js.map → chunk-RIYHP2WU.js.map} +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import {
|
|
2
|
+
metadataWithTurnExecutionPolicyV1,
|
|
3
|
+
type SessionRetryRequest,
|
|
4
|
+
type SessionRetryResponse,
|
|
5
|
+
SessionRetryResponse as SessionRetryResponseSchema,
|
|
6
|
+
type TurnExecutionPolicyV1,
|
|
7
|
+
} from "@opengeni/contracts";
|
|
8
|
+
import { and, desc, eq, sql } from "drizzle-orm";
|
|
9
|
+
import { rawRows, type Database, type SessionActivityDatabase } from "./database";
|
|
10
|
+
import * as schema from "./schema";
|
|
11
|
+
import { clearCodexCapacityRecovery } from "./codex-capacity-recovery";
|
|
12
|
+
import { fromPostgresLosslessJson, withLosslessContentWriteVersion } from "./lossless-json";
|
|
13
|
+
import { sessionAttemptPendingWritersSql } from "./session-attempt-writers";
|
|
14
|
+
import {
|
|
15
|
+
canonicalSessionCommandHash,
|
|
16
|
+
evaluateSessionControl,
|
|
17
|
+
lockSessionEventWriteRows,
|
|
18
|
+
registerSessionWorkflowWakeInTransaction,
|
|
19
|
+
reserveSessionCommandReceipt,
|
|
20
|
+
updateSessionCommandReceiptResult,
|
|
21
|
+
SessionCommandIdempotencyError,
|
|
22
|
+
} from "./session-control";
|
|
23
|
+
|
|
24
|
+
export class SessionRetryConflictError extends Error {
|
|
25
|
+
constructor(
|
|
26
|
+
readonly code:
|
|
27
|
+
| "RETRY_STALE_FAILURE"
|
|
28
|
+
| "RETRY_EXECUTION_UNRESOLVED"
|
|
29
|
+
| "RETRY_PAUSED"
|
|
30
|
+
| "RETRY_SANDBOX_RECOVERY_REQUIRED"
|
|
31
|
+
| "RETRY_UNSUPPORTED_FAILURE",
|
|
32
|
+
message: string,
|
|
33
|
+
) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "SessionRetryConflictError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Authorized receipt reads precede mutable model/billing admission checks. */
|
|
40
|
+
export async function getSessionRetryReceiptInTransaction(
|
|
41
|
+
db: Database,
|
|
42
|
+
input: {
|
|
43
|
+
workspaceId: string;
|
|
44
|
+
sessionId: string;
|
|
45
|
+
subjectId: string;
|
|
46
|
+
request: SessionRetryRequest;
|
|
47
|
+
},
|
|
48
|
+
): Promise<SessionRetryResponse | null> {
|
|
49
|
+
const [receipt] = await db
|
|
50
|
+
.select()
|
|
51
|
+
.from(schema.sessionCommandReceipts)
|
|
52
|
+
.where(
|
|
53
|
+
and(
|
|
54
|
+
eq(schema.sessionCommandReceipts.workspaceId, input.workspaceId),
|
|
55
|
+
eq(schema.sessionCommandReceipts.targetSessionId, input.sessionId),
|
|
56
|
+
eq(schema.sessionCommandReceipts.actorType, "human"),
|
|
57
|
+
eq(schema.sessionCommandReceipts.actorSubjectId, input.subjectId),
|
|
58
|
+
eq(schema.sessionCommandReceipts.action, "session.retry"),
|
|
59
|
+
eq(schema.sessionCommandReceipts.operationKey, input.request.clientEventId),
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
.limit(1);
|
|
63
|
+
if (!receipt) return null;
|
|
64
|
+
if (receipt.canonicalRequestHash !== canonicalSessionCommandHash(input.request))
|
|
65
|
+
throw new SessionCommandIdempotencyError();
|
|
66
|
+
return { ...SessionRetryResponseSchema.parse(receipt.result), outcome: "replayed" };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type EffectiveSandboxRecoveryBlocked = (
|
|
70
|
+
db: Database,
|
|
71
|
+
session: typeof schema.sessions.$inferSelect,
|
|
72
|
+
) => Promise<boolean>;
|
|
73
|
+
|
|
74
|
+
/** The root composition binds the canonical effective-route guard. Keep this
|
|
75
|
+
* leaf independent of the root barrel; there is no permissive default. */
|
|
76
|
+
export function createRetryFailedSessionInTransaction(
|
|
77
|
+
sessionEffectiveSandboxRecoveryBlocked: EffectiveSandboxRecoveryBlocked,
|
|
78
|
+
) {
|
|
79
|
+
return (
|
|
80
|
+
db: SessionActivityDatabase,
|
|
81
|
+
input: Parameters<typeof retryFailedSessionInTransaction>[1],
|
|
82
|
+
) => retryFailedSessionInTransaction(db, input, sessionEffectiveSandboxRecoveryBlocked);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Caller owns the tenant/activity transaction and authorization. No provider I/O. */
|
|
86
|
+
async function retryFailedSessionInTransaction(
|
|
87
|
+
db: SessionActivityDatabase,
|
|
88
|
+
input: {
|
|
89
|
+
accountId: string;
|
|
90
|
+
workspaceId: string;
|
|
91
|
+
sessionId: string;
|
|
92
|
+
subjectId: string;
|
|
93
|
+
request: SessionRetryRequest;
|
|
94
|
+
executionPolicy: TurnExecutionPolicyV1;
|
|
95
|
+
},
|
|
96
|
+
sessionEffectiveSandboxRecoveryBlocked: EffectiveSandboxRecoveryBlocked,
|
|
97
|
+
): Promise<SessionRetryResponse & { eventIds: string[] }> {
|
|
98
|
+
const { accountId, workspaceId, sessionId, request } = input;
|
|
99
|
+
const locks = await lockSessionEventWriteRows(db, {
|
|
100
|
+
workspaceId,
|
|
101
|
+
controlLock: "share",
|
|
102
|
+
sessionIds: [sessionId],
|
|
103
|
+
});
|
|
104
|
+
const session = locks.sessions[0];
|
|
105
|
+
if (!session || session.accountId !== accountId)
|
|
106
|
+
throw new SessionRetryConflictError("RETRY_STALE_FAILURE", "Session is unavailable");
|
|
107
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
108
|
+
accountId,
|
|
109
|
+
workspaceId,
|
|
110
|
+
targetSessionId: sessionId,
|
|
111
|
+
targetTurnId: null,
|
|
112
|
+
actor: { type: "human", subjectId: input.subjectId },
|
|
113
|
+
action: "session.retry",
|
|
114
|
+
operationKey: request.clientEventId,
|
|
115
|
+
canonicalRequestHash: canonicalSessionCommandHash(request),
|
|
116
|
+
});
|
|
117
|
+
if (reserved.replay)
|
|
118
|
+
return {
|
|
119
|
+
...SessionRetryResponseSchema.parse(reserved.receipt.result),
|
|
120
|
+
outcome: "replayed",
|
|
121
|
+
eventIds: [],
|
|
122
|
+
};
|
|
123
|
+
const control = await evaluateSessionControl(db, workspaceId, sessionId, {
|
|
124
|
+
workspaceControl: locks.control ?? undefined,
|
|
125
|
+
});
|
|
126
|
+
if (control.state !== "active")
|
|
127
|
+
throw new SessionRetryConflictError(
|
|
128
|
+
"RETRY_PAUSED",
|
|
129
|
+
"Resume the deliberately paused workstream before retrying",
|
|
130
|
+
);
|
|
131
|
+
if (session.status !== "failed" || session.activeTurnId !== null)
|
|
132
|
+
throw new SessionRetryConflictError(
|
|
133
|
+
"RETRY_STALE_FAILURE",
|
|
134
|
+
"The failed session boundary changed",
|
|
135
|
+
);
|
|
136
|
+
const [failure] = await db
|
|
137
|
+
.select()
|
|
138
|
+
.from(schema.sessionEvents)
|
|
139
|
+
.where(
|
|
140
|
+
and(
|
|
141
|
+
eq(schema.sessionEvents.workspaceId, workspaceId),
|
|
142
|
+
eq(schema.sessionEvents.sessionId, sessionId),
|
|
143
|
+
sql`(${schema.sessionEvents.turnAssociation} is null or ${schema.sessionEvents.turnAssociation} = 'current')`,
|
|
144
|
+
sql`(${schema.sessionEvents.type} = 'turn.failed' or (${schema.sessionEvents.type} = 'session.status.changed' and ${schema.sessionEvents.payload}->>'status' = 'failed' and ${schema.sessionEvents.turnId} is null))`,
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
.orderBy(desc(schema.sessionEvents.sequence))
|
|
148
|
+
.limit(1);
|
|
149
|
+
if (!failure || failure.id !== request.failureEventId)
|
|
150
|
+
throw new SessionRetryConflictError(
|
|
151
|
+
"RETRY_STALE_FAILURE",
|
|
152
|
+
"The failure event is no longer current",
|
|
153
|
+
);
|
|
154
|
+
const failurePayload = fromPostgresLosslessJson(failure.payload, failure.payloadCodecVersion);
|
|
155
|
+
if (
|
|
156
|
+
failurePayload !== null &&
|
|
157
|
+
typeof failurePayload === "object" &&
|
|
158
|
+
!Array.isArray(failurePayload) &&
|
|
159
|
+
(failurePayload as Record<string, unknown>).code === "provider_safety_refusal"
|
|
160
|
+
)
|
|
161
|
+
throw new SessionRetryConflictError(
|
|
162
|
+
"RETRY_UNSUPPORTED_FAILURE",
|
|
163
|
+
"A provider safety refusal cannot be retried as the same logical turn",
|
|
164
|
+
);
|
|
165
|
+
if (!failure.turnId)
|
|
166
|
+
throw new SessionRetryConflictError(
|
|
167
|
+
"RETRY_UNSUPPORTED_FAILURE",
|
|
168
|
+
"This failure has no retained logical turn to retry",
|
|
169
|
+
);
|
|
170
|
+
const [turn] = await db
|
|
171
|
+
.select()
|
|
172
|
+
.from(schema.sessionTurns)
|
|
173
|
+
.where(
|
|
174
|
+
and(
|
|
175
|
+
eq(schema.sessionTurns.workspaceId, workspaceId),
|
|
176
|
+
eq(schema.sessionTurns.sessionId, sessionId),
|
|
177
|
+
eq(schema.sessionTurns.id, failure.turnId),
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
.for("update");
|
|
181
|
+
if (!turn || turn.status !== "failed" || turn.activeAttemptId !== null)
|
|
182
|
+
throw new SessionRetryConflictError("RETRY_STALE_FAILURE", "The failed turn changed");
|
|
183
|
+
if (failure.turnGeneration !== null && failure.turnGeneration !== turn.executionGeneration)
|
|
184
|
+
throw new SessionRetryConflictError(
|
|
185
|
+
"RETRY_STALE_FAILURE",
|
|
186
|
+
"The failed execution generation changed",
|
|
187
|
+
);
|
|
188
|
+
if (turn.scheduledTaskRunId !== null)
|
|
189
|
+
throw new SessionRetryConflictError(
|
|
190
|
+
"RETRY_UNSUPPORTED_FAILURE",
|
|
191
|
+
"A settled scheduled occurrence cannot be reopened as a human retry",
|
|
192
|
+
);
|
|
193
|
+
const [unresolved] = await rawRows<{ pending: boolean }>(
|
|
194
|
+
db,
|
|
195
|
+
sql`select (
|
|
196
|
+
exists (select 1 from session_turn_attempts attempt where attempt.workspace_id = ${workspaceId} and attempt.session_id = ${sessionId}
|
|
197
|
+
and (attempt.state <> 'closed' or ${sessionAttemptPendingWritersSql(sql`attempt`)}
|
|
198
|
+
or (attempt.quiesced_at is null and exists (select 1 from session_attempt_interruptions interruption where interruption.attempt_id = attempt.id))))
|
|
199
|
+
or exists (select 1 from session_pending_tool_calls pending where pending.workspace_id = ${workspaceId} and pending.session_id = ${sessionId})
|
|
200
|
+
or exists (select 1 from session_events tool_event where tool_event.workspace_id = ${workspaceId} and tool_event.session_id = ${sessionId} and tool_event.turn_id = ${turn.id}
|
|
201
|
+
and tool_event.type = 'agent.toolCall.output' and tool_event.payload->'recovery'->>'outcome' = 'unknown')
|
|
202
|
+
or exists (select 1 from session_turns live where live.workspace_id = ${workspaceId} and live.session_id = ${sessionId} and live.status in ('running', 'recovering', 'requires_action', 'waiting_capacity'))
|
|
203
|
+
) as pending`,
|
|
204
|
+
);
|
|
205
|
+
if (unresolved?.pending)
|
|
206
|
+
throw new SessionRetryConflictError(
|
|
207
|
+
"RETRY_EXECUTION_UNRESOLVED",
|
|
208
|
+
"Execution or tool settlement is unresolved; retry cannot replay it",
|
|
209
|
+
);
|
|
210
|
+
const now = new Date();
|
|
211
|
+
if (await sessionEffectiveSandboxRecoveryBlocked(db, session))
|
|
212
|
+
throw new SessionRetryConflictError(
|
|
213
|
+
"RETRY_SANDBOX_RECOVERY_REQUIRED",
|
|
214
|
+
"The effective sandbox route requires recovery; review its checkpoint before retrying",
|
|
215
|
+
);
|
|
216
|
+
// A never-claimed prompt must traverse normal first claim exactly once so
|
|
217
|
+
// its original user history item is inserted. Started turns retain history.
|
|
218
|
+
const preclaim = turn.executionGeneration === 0;
|
|
219
|
+
const policy = input.executionPolicy;
|
|
220
|
+
const capacityRecoveryRetry =
|
|
221
|
+
failurePayload !== null &&
|
|
222
|
+
typeof failurePayload === "object" &&
|
|
223
|
+
!Array.isArray(failurePayload) &&
|
|
224
|
+
(failurePayload as Record<string, unknown>).code === "codex_capacity_recovery_exhausted";
|
|
225
|
+
// Only this explicitly authorized, exact failure-event Retry may replenish
|
|
226
|
+
// the breaker. Preserve accepted credential policy/refusal ledgers and all
|
|
227
|
+
// unrelated recovery budgets. Receipt replay returned above cannot reset it.
|
|
228
|
+
const retryMetadata = capacityRecoveryRetry
|
|
229
|
+
? clearCodexCapacityRecovery(turn.metadata ?? {})
|
|
230
|
+
: turn.metadata;
|
|
231
|
+
if (capacityRecoveryRetry) {
|
|
232
|
+
await db
|
|
233
|
+
.update(schema.sessionGoals)
|
|
234
|
+
.set({ continuationSuppressedTurnId: null, updatedAt: now })
|
|
235
|
+
.where(
|
|
236
|
+
and(
|
|
237
|
+
eq(schema.sessionGoals.workspaceId, workspaceId),
|
|
238
|
+
eq(schema.sessionGoals.sessionId, sessionId),
|
|
239
|
+
eq(schema.sessionGoals.continuationSuppressedTurnId, turn.id),
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
await db
|
|
244
|
+
.update(schema.sessionTurns)
|
|
245
|
+
.set({
|
|
246
|
+
status: preclaim ? "queued" : "recovering",
|
|
247
|
+
finishedAt: null,
|
|
248
|
+
updatedAt: now,
|
|
249
|
+
version: turn.version + 1,
|
|
250
|
+
model: policy.productModelId,
|
|
251
|
+
reasoningEffort: policy.reasoningEffort,
|
|
252
|
+
latencyMode: policy.latencyMode,
|
|
253
|
+
metadata: metadataWithTurnExecutionPolicyV1(retryMetadata, policy),
|
|
254
|
+
})
|
|
255
|
+
.where(eq(schema.sessionTurns.id, turn.id));
|
|
256
|
+
const events = await db
|
|
257
|
+
.insert(schema.sessionEvents)
|
|
258
|
+
.values(
|
|
259
|
+
withLosslessContentWriteVersion(
|
|
260
|
+
[
|
|
261
|
+
{
|
|
262
|
+
accountId,
|
|
263
|
+
workspaceId,
|
|
264
|
+
sessionId,
|
|
265
|
+
sequence: session.lastSequence + 1,
|
|
266
|
+
type: "turn.recovery.requested",
|
|
267
|
+
turnId: turn.id,
|
|
268
|
+
turnGeneration: turn.executionGeneration,
|
|
269
|
+
turnAssociation: "current",
|
|
270
|
+
payload: {
|
|
271
|
+
reason: "human_retry",
|
|
272
|
+
failureEventId: failure.id,
|
|
273
|
+
operationId: request.clientEventId,
|
|
274
|
+
model: policy.productModelId,
|
|
275
|
+
reasoningEffort: policy.reasoningEffort,
|
|
276
|
+
latencyMode: policy.latencyMode,
|
|
277
|
+
},
|
|
278
|
+
occurredAt: now,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
accountId,
|
|
282
|
+
workspaceId,
|
|
283
|
+
sessionId,
|
|
284
|
+
sequence: session.lastSequence + 2,
|
|
285
|
+
type: "session.status.changed",
|
|
286
|
+
turnId: turn.id,
|
|
287
|
+
turnGeneration: turn.executionGeneration,
|
|
288
|
+
turnAssociation: "current",
|
|
289
|
+
payload: { status: preclaim ? "queued" : "recovering" },
|
|
290
|
+
occurredAt: now,
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
"payload",
|
|
294
|
+
"payloadCodecVersion",
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
.returning({ id: schema.sessionEvents.id });
|
|
298
|
+
await db
|
|
299
|
+
.update(schema.sessions)
|
|
300
|
+
.set({
|
|
301
|
+
status: preclaim ? "queued" : "recovering",
|
|
302
|
+
activeTurnId: preclaim ? null : turn.id,
|
|
303
|
+
admissionBlock: null,
|
|
304
|
+
lastSequence: session.lastSequence + 2,
|
|
305
|
+
queueVersion: session.queueVersion + 1,
|
|
306
|
+
updatedAt: now,
|
|
307
|
+
})
|
|
308
|
+
.where(eq(schema.sessions.id, sessionId));
|
|
309
|
+
await registerSessionWorkflowWakeInTransaction(db, {
|
|
310
|
+
accountId,
|
|
311
|
+
workspaceId,
|
|
312
|
+
sessionId,
|
|
313
|
+
temporalWorkflowId: session.temporalWorkflowId ?? `session-${sessionId}`,
|
|
314
|
+
reason: "human_failed_session_retry",
|
|
315
|
+
});
|
|
316
|
+
const response = { outcome: "accepted", turnId: turn.id, failureEventId: failure.id } as const;
|
|
317
|
+
await updateSessionCommandReceiptResult(db, reserved.receipt.id, { result: response });
|
|
318
|
+
return { ...response, eventIds: events.map((event) => event.id) };
|
|
319
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import {
|
|
3
|
-
StoredCapabilityPack,
|
|
4
3
|
StoredSessionSkills,
|
|
4
|
+
StoredAutomationSessionTemplate,
|
|
5
5
|
validateSkillTextFiles,
|
|
6
6
|
type SkillFile,
|
|
7
7
|
} from "@opengeni/contracts";
|
|
@@ -74,7 +74,16 @@ export function convertLegacyPackConfig(value: unknown, identity: string): Objec
|
|
|
74
74
|
},
|
|
75
75
|
};
|
|
76
76
|
});
|
|
77
|
-
|
|
77
|
+
// Historical migration 0433 only: validate the execution fields this
|
|
78
|
+
// conversion rewrites. The remaining archived manifest is opaque evidence,
|
|
79
|
+
// not an active capability or an admission contract.
|
|
80
|
+
if (converted.skills !== undefined) StoredSessionSkills.parse(converted.skills);
|
|
81
|
+
if (converted.automationTemplates !== undefined) {
|
|
82
|
+
if (!Array.isArray(converted.automationTemplates))
|
|
83
|
+
throw new Error("Expected historical automation template array");
|
|
84
|
+
for (const entry of converted.automationTemplates)
|
|
85
|
+
StoredAutomationSessionTemplate.parse(object(entry).sessionTemplate);
|
|
86
|
+
}
|
|
78
87
|
return converted;
|
|
79
88
|
}
|
|
80
89
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type SkillSourceReleaseHead = {
|
|
2
|
+
id: string;
|
|
3
|
+
account_id: string;
|
|
4
|
+
scope: string;
|
|
5
|
+
scope_workspace_id: string | null;
|
|
6
|
+
scope_version: number;
|
|
7
|
+
status: string;
|
|
8
|
+
active_revision_id: string | null;
|
|
9
|
+
provenance_source: string | null;
|
|
10
|
+
title: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type SkillRetentionReason = "customized" | "re_scoped";
|
|
14
|
+
|
|
15
|
+
/** One decision for both the user-visible impact and committed source release. */
|
|
16
|
+
export function classifySkillSourceRelease(
|
|
17
|
+
head: SkillSourceReleaseHead,
|
|
18
|
+
workspaceId: string,
|
|
19
|
+
): {
|
|
20
|
+
disposition: "inactive" | "retained" | "removed";
|
|
21
|
+
retentionReasons: SkillRetentionReason[];
|
|
22
|
+
} {
|
|
23
|
+
if (head.status !== "active" || !head.active_revision_id)
|
|
24
|
+
return { disposition: "inactive", retentionReasons: [] };
|
|
25
|
+
const retentionReasons: SkillRetentionReason[] = [];
|
|
26
|
+
if (head.provenance_source !== "portable_skill") retentionReasons.push("customized");
|
|
27
|
+
if (head.scope !== "workspace" || head.scope_workspace_id !== workspaceId)
|
|
28
|
+
retentionReasons.push("re_scoped");
|
|
29
|
+
return {
|
|
30
|
+
disposition: retentionReasons.length ? "retained" : "removed",
|
|
31
|
+
retentionReasons,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -2,6 +2,10 @@ import type { SkillActor } from "@opengeni/contracts";
|
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
3
|
import { rawRows, withWorkspaceSubjectRls, type Database } from "./database";
|
|
4
4
|
import { deactivatePreferenceRegistry } from "./preference-registry";
|
|
5
|
+
import {
|
|
6
|
+
classifySkillSourceRelease,
|
|
7
|
+
type SkillSourceReleaseHead,
|
|
8
|
+
} from "./skill-source-release-impact";
|
|
5
9
|
|
|
6
10
|
export type SkillSourceReleaseReceipt = {
|
|
7
11
|
skillId: string;
|
|
@@ -11,6 +15,31 @@ export type SkillSourceReleaseReceipt = {
|
|
|
11
15
|
warning: string | null;
|
|
12
16
|
};
|
|
13
17
|
|
|
18
|
+
/** Caller supplies workspace/subject RLS; never bypass registry visibility for a preview. */
|
|
19
|
+
export async function readSkillSourceReleaseHeads(
|
|
20
|
+
db: Database,
|
|
21
|
+
workspaceId: string,
|
|
22
|
+
facetInstallationIds: readonly string[],
|
|
23
|
+
): Promise<SkillSourceReleaseHead[]> {
|
|
24
|
+
if (!facetInstallationIds.length) return [];
|
|
25
|
+
return rawRows<SkillSourceReleaseHead>(
|
|
26
|
+
db,
|
|
27
|
+
sql`
|
|
28
|
+
SELECT DISTINCT h.id,h.account_id,h.scope,h.scope_workspace_id,h.scope_version,
|
|
29
|
+
h.status,h.active_revision_id,r.provenance_source,r.title
|
|
30
|
+
FROM skill_source_bindings b
|
|
31
|
+
JOIN capability_facet_installations fi ON fi.facet_id=b.skill_facet_id
|
|
32
|
+
AND fi.account_id=b.account_id AND fi.workspace_id=b.workspace_id
|
|
33
|
+
JOIN preference_registry_preferences h ON h.id=b.preference_id AND h.account_id=b.account_id
|
|
34
|
+
LEFT JOIN preference_registry_revisions r ON r.id=h.active_revision_id
|
|
35
|
+
AND r.preference_id=h.id AND r.account_id=h.account_id
|
|
36
|
+
WHERE b.workspace_id=${workspaceId}::uuid
|
|
37
|
+
AND fi.id=ANY(string_to_array(${facetInstallationIds.join(",")},',')::uuid[])
|
|
38
|
+
ORDER BY h.id
|
|
39
|
+
`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
14
43
|
export class SkillSourceRemovalAuthorityError extends Error {
|
|
15
44
|
readonly code = "skill_source_removal_requires_human";
|
|
16
45
|
constructor() {
|
|
@@ -21,6 +50,14 @@ export class SkillSourceRemovalAuthorityError extends Error {
|
|
|
21
50
|
}
|
|
22
51
|
}
|
|
23
52
|
|
|
53
|
+
/** Never disclose the identity of a head absent from the caller's locked snapshot. */
|
|
54
|
+
export class SkillSourceReleaseSnapshotChangedError extends Error {
|
|
55
|
+
constructor() {
|
|
56
|
+
super("Skill source visibility changed during removal; review a refreshed preview.");
|
|
57
|
+
this.name = "SkillSourceReleaseSnapshotChangedError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
24
61
|
/**
|
|
25
62
|
* Called inside the source-owner removal transaction, with source installations
|
|
26
63
|
* locked. Distribution identity/history stays bound after uninstall, so reinstall
|
|
@@ -28,7 +65,13 @@ export class SkillSourceRemovalAuthorityError extends Error {
|
|
|
28
65
|
*/
|
|
29
66
|
export async function releaseOrphanedSkillHeads(
|
|
30
67
|
db: Database,
|
|
31
|
-
input: {
|
|
68
|
+
input: {
|
|
69
|
+
workspaceId: string;
|
|
70
|
+
facetInstallationIds: readonly string[];
|
|
71
|
+
skillActor?: SkillActor;
|
|
72
|
+
/** Exact heads already locked before the caller compared removal impact. */
|
|
73
|
+
lockedHeadIds?: readonly string[];
|
|
74
|
+
},
|
|
32
75
|
): Promise<SkillSourceReleaseReceipt[]> {
|
|
33
76
|
if (!input.facetInstallationIds.length) return [];
|
|
34
77
|
const bindings = await rawRows<{ id: string }>(
|
|
@@ -48,35 +91,37 @@ export async function releaseOrphanedSkillHeads(
|
|
|
48
91
|
throw new SkillSourceRemovalAuthorityError();
|
|
49
92
|
}
|
|
50
93
|
return withWorkspaceSubjectRls(db, input.workspaceId, actor.subjectId, async (tx) => {
|
|
94
|
+
const visibleHeads = await readSkillSourceReleaseHeads(
|
|
95
|
+
tx,
|
|
96
|
+
input.workspaceId,
|
|
97
|
+
input.facetInstallationIds,
|
|
98
|
+
);
|
|
99
|
+
const lockedHeadIds = new Set(input.lockedHeadIds ?? visibleHeads.map((head) => head.id));
|
|
100
|
+
if (visibleHeads.some((head) => !lockedHeadIds.has(head.id)))
|
|
101
|
+
throw new SkillSourceReleaseSnapshotChangedError();
|
|
51
102
|
// The established definer lock capability accepts at most two heads; acquire
|
|
52
103
|
// individual heads in canonical order rather than widening its authority.
|
|
53
|
-
|
|
104
|
+
// An inaccessible re-scoped head is preserved, never an excuse to acquire
|
|
105
|
+
// authority over another scope or disclose its identity in a receipt.
|
|
106
|
+
for (const binding of input.lockedHeadIds === undefined ? visibleHeads : []) {
|
|
54
107
|
await tx.execute(
|
|
55
108
|
sql`SELECT preference_id FROM preference_registry_lock_heads(ARRAY[${binding.id}::uuid])`,
|
|
56
109
|
);
|
|
57
110
|
}
|
|
58
|
-
const rows = await
|
|
59
|
-
id: string;
|
|
60
|
-
account_id: string;
|
|
61
|
-
scope: string;
|
|
62
|
-
scope_workspace_id: string | null;
|
|
63
|
-
scope_version: number;
|
|
64
|
-
status: string;
|
|
65
|
-
active_revision_id: string | null;
|
|
66
|
-
provenance_source: string | null;
|
|
67
|
-
}>(
|
|
111
|
+
const rows = await readSkillSourceReleaseHeads(
|
|
68
112
|
tx,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
FROM preference_registry_preferences h LEFT JOIN preference_registry_revisions r
|
|
72
|
-
ON r.id=h.active_revision_id AND r.preference_id=h.id AND r.account_id=h.account_id
|
|
73
|
-
WHERE h.id=ANY(string_to_array(${bindings.map((binding) => binding.id).join(",")},',')::uuid[])
|
|
74
|
-
ORDER BY h.id
|
|
75
|
-
`,
|
|
113
|
+
input.workspaceId,
|
|
114
|
+
input.facetInstallationIds,
|
|
76
115
|
);
|
|
116
|
+
// A second subject may have moved a previously invisible head into this
|
|
117
|
+
// scope since either read. Never widen the locked set or acquire a new head
|
|
118
|
+
// lock out of canonical order during cleanup.
|
|
119
|
+
if (rows.some((head) => !lockedHeadIds.has(head.id)))
|
|
120
|
+
throw new SkillSourceReleaseSnapshotChangedError();
|
|
77
121
|
const receipts: SkillSourceReleaseReceipt[] = [];
|
|
78
122
|
for (const head of rows) {
|
|
79
|
-
|
|
123
|
+
const impact = classifySkillSourceRelease(head, input.workspaceId);
|
|
124
|
+
if (impact.disposition === "inactive") {
|
|
80
125
|
receipts.push({
|
|
81
126
|
skillId: head.id,
|
|
82
127
|
revisionId: head.active_revision_id,
|
|
@@ -84,11 +129,7 @@ export async function releaseOrphanedSkillHeads(
|
|
|
84
129
|
eventId: null,
|
|
85
130
|
warning: null,
|
|
86
131
|
});
|
|
87
|
-
} else if (
|
|
88
|
-
head.provenance_source !== "portable_skill" ||
|
|
89
|
-
head.scope !== "workspace" ||
|
|
90
|
-
head.scope_workspace_id !== input.workspaceId
|
|
91
|
-
) {
|
|
132
|
+
} else if (impact.disposition === "retained") {
|
|
92
133
|
receipts.push({
|
|
93
134
|
skillId: head.id,
|
|
94
135
|
revisionId: head.active_revision_id,
|
|
@@ -103,7 +144,7 @@ export async function releaseOrphanedSkillHeads(
|
|
|
103
144
|
actorSubjectId: actor.subjectId,
|
|
104
145
|
principalKind: actor.principalKind,
|
|
105
146
|
preferenceId: head.id,
|
|
106
|
-
expectedCurrentRevisionId: head.active_revision_id
|
|
147
|
+
expectedCurrentRevisionId: head.active_revision_id!,
|
|
107
148
|
expectedScopeVersion: head.scope_version,
|
|
108
149
|
authorizeScope: (scope) => {
|
|
109
150
|
if (scope !== "workspace")
|
package/src/skills.ts
CHANGED
|
@@ -88,12 +88,13 @@ export async function applySkillLifecycle(
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
const removing = request.operation === "remove" || Boolean(request.removalOperationId);
|
|
92
|
+
if (content === undefined && !removing)
|
|
93
|
+
throw new Error("Skill lifecycle requires a readable SKILL.md");
|
|
94
|
+
const metadata = removing ? null : readSkillMetadata(content!);
|
|
93
95
|
const canonicalRequest = {
|
|
94
96
|
...request,
|
|
95
|
-
title: metadata.name,
|
|
96
|
-
description: metadata.description,
|
|
97
|
+
...(metadata ? { title: metadata.name, description: metadata.description } : {}),
|
|
97
98
|
};
|
|
98
99
|
const rows = await rawRows<{ receipt: SkillWriteReceipt }>(
|
|
99
100
|
tx,
|
|
@@ -117,6 +118,30 @@ export type SkillReadContext = {
|
|
|
117
118
|
subjectId?: string;
|
|
118
119
|
};
|
|
119
120
|
|
|
121
|
+
/** Scope only, for authorizing an exact human deletion retry after the head is gone.
|
|
122
|
+
* The lifecycle still verifies the complete request fingerprint before replay.
|
|
123
|
+
*/
|
|
124
|
+
export async function readSkillRemovalScope(
|
|
125
|
+
db: Database,
|
|
126
|
+
context: SkillReadContext & { subjectId: string },
|
|
127
|
+
skillId: string,
|
|
128
|
+
operationId: string,
|
|
129
|
+
): Promise<"workspace" | "user" | null> {
|
|
130
|
+
return withWorkspaceSubjectRls(db, context.workspaceId, context.subjectId, async (tx) => {
|
|
131
|
+
const [row] = await rawRows<{ scope: string }>(
|
|
132
|
+
tx,
|
|
133
|
+
sql`
|
|
134
|
+
SELECT receipt->>'removedScope' AS scope FROM skill_write_receipts
|
|
135
|
+
WHERE account_id=${context.accountId}::uuid AND workspace_id=${context.workspaceId}::uuid
|
|
136
|
+
AND operation_id=${operationId}::uuid AND receipt->>'skillId'=${skillId}
|
|
137
|
+
AND receipt->>'removed'='true' AND actor->>'kind'='human'
|
|
138
|
+
AND actor->>'principalKind'='human_session' AND actor->>'subjectId'=${context.subjectId}
|
|
139
|
+
`,
|
|
140
|
+
);
|
|
141
|
+
return row?.scope === "workspace" || row?.scope === "user" ? row.scope : null;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
/** Recheck the host-bound attempt before any agent-facing Skill read/search. */
|
|
121
146
|
export async function assertSkillReadAttempt(
|
|
122
147
|
db: Database,
|
|
@@ -212,6 +237,7 @@ export async function listSkillRecords(
|
|
|
212
237
|
AND active.account_id=h.account_id AND active.expires_at <= transaction_timestamp()
|
|
213
238
|
) THEN 'expired' ELSE h.status END,
|
|
214
239
|
'activeRevisionId',h.active_revision_id,'revisionId',r.id,
|
|
240
|
+
'removalOperationId',r.skill_removal_operation_id,
|
|
215
241
|
'activationMode',coalesce(r.skill_activation_mode,'workspace_managed'),
|
|
216
242
|
'pendingRevisionIds',coalesce((SELECT jsonb_agg(DISTINCT pending.receipt->>'revisionId')
|
|
217
243
|
FROM skill_write_receipts pending WHERE pending.account_id=h.account_id
|
|
@@ -346,7 +372,7 @@ export async function skillReviewResolution(
|
|
|
346
372
|
db: Database,
|
|
347
373
|
context: SkillReadContext,
|
|
348
374
|
review: SkillReviewReference,
|
|
349
|
-
): Promise<"pending" | "activated" | "declined" | "superseded" | "unavailable"> {
|
|
375
|
+
): Promise<"pending" | "activated" | "removed" | "declined" | "superseded" | "unavailable"> {
|
|
350
376
|
const run = async (tx: Database) => {
|
|
351
377
|
const [record] = await listSkillRecords(tx, context, {
|
|
352
378
|
skillId: review.skillId,
|
|
@@ -354,7 +380,19 @@ export async function skillReviewResolution(
|
|
|
354
380
|
metadataOnly: true,
|
|
355
381
|
limit: 1,
|
|
356
382
|
});
|
|
357
|
-
if (!record)
|
|
383
|
+
if (!record) {
|
|
384
|
+
if (review.removalOperationId) {
|
|
385
|
+
const [removed] = await rawRows<{ operation_id: string }>(
|
|
386
|
+
tx,
|
|
387
|
+
sql`
|
|
388
|
+
SELECT operation_id FROM skill_write_receipts
|
|
389
|
+
WHERE account_id=${context.accountId}::uuid AND workspace_id=${context.workspaceId}::uuid
|
|
390
|
+
AND receipt->>'skillId'=${review.skillId} AND receipt->>'removed'='true' LIMIT 1`,
|
|
391
|
+
);
|
|
392
|
+
if (removed) return "removed" as const;
|
|
393
|
+
}
|
|
394
|
+
return "unavailable" as const;
|
|
395
|
+
}
|
|
358
396
|
const [event] = await rawRows<{ type: string }>(
|
|
359
397
|
tx,
|
|
360
398
|
sql`
|
|
@@ -301,62 +301,6 @@ export async function issueSelfUserResourceGrant(
|
|
|
301
301
|
});
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
export async function issueSelfLocalConnectionUseGrant(
|
|
305
|
-
db: Database,
|
|
306
|
-
input: {
|
|
307
|
-
accountId: string;
|
|
308
|
-
workspaceId: string;
|
|
309
|
-
subjectId: string;
|
|
310
|
-
authorityId: string;
|
|
311
|
-
context: "user_private" | "workspace_shared";
|
|
312
|
-
workspaceSharedAcknowledged: boolean;
|
|
313
|
-
},
|
|
314
|
-
): Promise<UserResourceAuthorityGrant> {
|
|
315
|
-
return await withOwnerContext(db, input, async (scopedDb) => {
|
|
316
|
-
const [row] = await rawRows<
|
|
317
|
-
Omit<UserResourceAuthorityGrant, "delegation"> & {
|
|
318
|
-
organizationId: string;
|
|
319
|
-
authorityGeneration: number;
|
|
320
|
-
}
|
|
321
|
-
>(
|
|
322
|
-
scopedDb,
|
|
323
|
-
sql`
|
|
324
|
-
select grant_id as "grantId", organization_id as "organizationId",
|
|
325
|
-
authority_generation::int as "authorityGeneration",
|
|
326
|
-
target_workspace_id as "targetWorkspaceId",
|
|
327
|
-
target_session_id as "targetSessionId", action, grant_mode as mode,
|
|
328
|
-
grant_context as context, authority_epoch::int as "authorityEpoch",
|
|
329
|
-
grant_generation::int as generation, grant_status as status,
|
|
330
|
-
expires_at::text as "expiresAt"
|
|
331
|
-
from issue_self_local_connection_use_grant(
|
|
332
|
-
${input.accountId}::uuid, ${input.authorityId}::uuid,
|
|
333
|
-
${input.workspaceId}::uuid, ${input.context},
|
|
334
|
-
${input.workspaceSharedAcknowledged}
|
|
335
|
-
)
|
|
336
|
-
`,
|
|
337
|
-
);
|
|
338
|
-
if (!row) throw new Error("local connection grant was not returned");
|
|
339
|
-
const { organizationId, authorityGeneration, ...grant } = row;
|
|
340
|
-
return {
|
|
341
|
-
...grant,
|
|
342
|
-
expiresAt: rfc3339(grant.expiresAt),
|
|
343
|
-
delegation: {
|
|
344
|
-
authorityId: input.authorityId,
|
|
345
|
-
grantId: grant.grantId,
|
|
346
|
-
organizationId,
|
|
347
|
-
workspaceId: grant.targetWorkspaceId,
|
|
348
|
-
sessionId: grant.targetSessionId,
|
|
349
|
-
action: grant.action,
|
|
350
|
-
mode: grant.mode,
|
|
351
|
-
context: grant.context,
|
|
352
|
-
authorityEpoch: grant.authorityEpoch,
|
|
353
|
-
authorityGeneration,
|
|
354
|
-
grantGeneration: grant.generation,
|
|
355
|
-
},
|
|
356
|
-
};
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
304
|
export async function revokeSelfUserResourceGrant(
|
|
361
305
|
db: Database,
|
|
362
306
|
input: { accountId: string; workspaceId: string; subjectId: string; grantId: string },
|