@stigmer/server 3.38.2 → 3.39.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/domain/agent/controller.d.ts +2 -2
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js +7 -3
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentchannel/controller.d.ts +2 -2
- package/dist/domain/agentchannel/controller.d.ts.map +1 -1
- package/dist/domain/agentchannel/controller.js +10 -5
- package/dist/domain/agentchannel/controller.js.map +1 -1
- package/dist/domain/agentexecution/controller.js +7 -3
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/update-status.d.ts +9 -2
- package/dist/domain/agentexecution/update-status.d.ts.map +1 -1
- package/dist/domain/agentexecution/update-status.js +28 -5
- package/dist/domain/agentexecution/update-status.js.map +1 -1
- package/dist/domain/agentexecution/validate-thinking-mode.d.ts +31 -21
- package/dist/domain/agentexecution/validate-thinking-mode.d.ts.map +1 -1
- package/dist/domain/agentexecution/validate-thinking-mode.js +61 -18
- package/dist/domain/agentexecution/validate-thinking-mode.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts +2 -2
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js +7 -3
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts +2 -2
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +7 -3
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/artifact/controller.d.ts.map +1 -1
- package/dist/domain/artifact/controller.js +9 -6
- package/dist/domain/artifact/controller.js.map +1 -1
- package/dist/domain/environment/controller.d.ts +2 -2
- package/dist/domain/environment/controller.js +7 -3
- package/dist/domain/environment/controller.js.map +1 -1
- package/dist/domain/environment/steps.d.ts +2 -1
- package/dist/domain/environment/steps.d.ts.map +1 -1
- package/dist/domain/environment/steps.js +10 -7
- package/dist/domain/environment/steps.js.map +1 -1
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js +33 -14
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/organization/slug-ledger.d.ts +86 -0
- package/dist/domain/organization/slug-ledger.d.ts.map +1 -0
- package/dist/domain/organization/slug-ledger.js +128 -0
- package/dist/domain/organization/slug-ledger.js.map +1 -0
- package/dist/domain/organization/steps.d.ts +12 -3
- package/dist/domain/organization/steps.d.ts.map +1 -1
- package/dist/domain/organization/steps.js +28 -6
- package/dist/domain/organization/steps.js.map +1 -1
- package/dist/domain/workflow/registry/data/model-registry.json +23 -14
- package/dist/domain/workflow/registry/model-registry-store.d.ts +14 -3
- package/dist/domain/workflow/registry/model-registry-store.d.ts.map +1 -1
- package/dist/domain/workflow/registry/model-registry-store.js +14 -3
- package/dist/domain/workflow/registry/model-registry-store.js.map +1 -1
- package/dist/extensions/gate-slots.d.ts +7 -5
- package/dist/extensions/gate-slots.d.ts.map +1 -1
- package/dist/extensions/gate-slots.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/errors.d.ts +8 -0
- package/dist/pipeline/errors.d.ts.map +1 -1
- package/dist/pipeline/errors.js +19 -0
- package/dist/pipeline/errors.js.map +1 -1
- package/dist/store/interface.d.ts +67 -0
- package/dist/store/interface.d.ts.map +1 -1
- package/dist/store/interface.js.map +1 -1
- package/dist/store/organization-slug-history.d.ts +71 -0
- package/dist/store/organization-slug-history.d.ts.map +1 -0
- package/dist/store/organization-slug-history.js +88 -0
- package/dist/store/organization-slug-history.js.map +1 -0
- package/dist/store/postgres/migrations.d.ts +3 -1
- package/dist/store/postgres/migrations.d.ts.map +1 -1
- package/dist/store/postgres/migrations.js +69 -1
- package/dist/store/postgres/migrations.js.map +1 -1
- package/dist/store/postgres/store.d.ts +2 -1
- package/dist/store/postgres/store.d.ts.map +1 -1
- package/dist/store/postgres/store.js +49 -0
- package/dist/store/postgres/store.js.map +1 -1
- package/dist/store/sqlite/migrations.d.ts +3 -1
- package/dist/store/sqlite/migrations.d.ts.map +1 -1
- package/dist/store/sqlite/migrations.js +54 -1
- package/dist/store/sqlite/migrations.js.map +1 -1
- package/dist/store/sqlite/store.d.ts +2 -1
- package/dist/store/sqlite/store.d.ts.map +1 -1
- package/dist/store/sqlite/store.js +55 -0
- package/dist/store/sqlite/store.js.map +1 -1
- package/package.json +6 -6
- package/src/domain/agent/__tests__/store-faults.test.ts +105 -0
- package/src/domain/agent/controller.ts +9 -5
- package/src/domain/agentchannel/__tests__/store-faults.test.ts +126 -0
- package/src/domain/agentchannel/controller.ts +12 -7
- package/src/domain/agentexecution/__tests__/run-gate.test.ts +42 -1
- package/src/domain/agentexecution/__tests__/update-status.test.ts +58 -2
- package/src/domain/agentexecution/__tests__/validate-thinking-mode.test.ts +209 -61
- package/src/domain/agentexecution/controller.ts +7 -3
- package/src/domain/agentexecution/update-status.ts +43 -3
- package/src/domain/agentexecution/validate-thinking-mode.ts +102 -47
- package/src/domain/agentinstance/__tests__/store-faults.test.ts +110 -0
- package/src/domain/agentinstance/controller.ts +9 -5
- package/src/domain/agentshare/__tests__/store-faults.test.ts +101 -0
- package/src/domain/agentshare/controller.ts +9 -5
- package/src/domain/artifact/__tests__/store-faults.test.ts +117 -0
- package/src/domain/artifact/controller.ts +12 -6
- package/src/domain/environment/__tests__/store-faults.test.ts +150 -0
- package/src/domain/environment/controller.ts +8 -5
- package/src/domain/environment/steps.ts +10 -7
- package/src/domain/organization/__tests__/organization-delete.test.ts +73 -18
- package/src/domain/organization/__tests__/organization-slugs.test.ts +386 -0
- package/src/domain/organization/controller.ts +36 -14
- package/src/domain/organization/slug-ledger.ts +209 -0
- package/src/domain/organization/steps.ts +28 -6
- package/src/domain/workflow/registry/data/model-registry.json +23 -14
- package/src/domain/workflow/registry/model-registry-store.ts +16 -3
- package/src/extensions/gate-slots.ts +7 -5
- package/src/index.ts +11 -0
- package/src/pipeline/__tests__/blind-not-found.test.ts +0 -7
- package/src/pipeline/errors.ts +23 -0
- package/src/store/__tests__/organization-slug-history.test.ts +109 -0
- package/src/store/__tests__/store-contract.ts +86 -1
- package/src/store/interface.ts +70 -0
- package/src/store/organization-slug-history.ts +159 -0
- package/src/store/postgres/__tests__/migrations.test.ts +138 -2
- package/src/store/postgres/__tests__/store-contract.test.ts +1 -0
- package/src/store/postgres/migrations.ts +84 -1
- package/src/store/postgres/store.ts +84 -0
- package/src/store/sqlite/__tests__/migrations.test.ts +139 -10
- package/src/store/sqlite/migrations.ts +69 -1
- package/src/store/sqlite/store.ts +84 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The organization domain's use of the slug ledger (the Store's
|
|
3
|
+
* `organizationSlugs`; store/interface.ts states its guarantees): an
|
|
4
|
+
* organization's slug is its for good.
|
|
5
|
+
*
|
|
6
|
+
* An organization's id is its slug, and the delete removes the row. Rows
|
|
7
|
+
* that name the organization can outlive it (every organization-scoped
|
|
8
|
+
* resource names it in `metadata.org`, and an edition may keep its own rows
|
|
9
|
+
* by the id), so a slug taken again would hand them all to the new holder.
|
|
10
|
+
* The ledger closes that: the create claims the slug atomically, the delete
|
|
11
|
+
* retires it, and nothing ever frees a retired slug.
|
|
12
|
+
*
|
|
13
|
+
* The create touches the ledger twice. CheckDuplicate reads it first, so a
|
|
14
|
+
* taken slug is refused before any gate runs (steps.ts). ClaimOrganizationSlug
|
|
15
|
+
* then claims it immediately before Persist, after the pre-side-effect gate
|
|
16
|
+
* slot, so a gate's refusal still leaves nothing written; of two concurrent
|
|
17
|
+
* creates of one slug, exactly one claim wins, which the row alone cannot
|
|
18
|
+
* give because `saveResource` upserts. A create that fails after its claim
|
|
19
|
+
* and before its row is stored releases the claim, so the caller's retry
|
|
20
|
+
* can take the slug again; a create that fails after the row keeps it,
|
|
21
|
+
* because the organization exists.
|
|
22
|
+
*
|
|
23
|
+
* The refusal says why. A retired slug answers AlreadyExists carrying the
|
|
24
|
+
* ORGANIZATION_SLUG_RESERVED reason (the organization create's contract
|
|
25
|
+
* documents it), so the CLI and the console can tell a person the slug
|
|
26
|
+
* belonged to a deleted organization. A held slug keeps the existing copy
|
|
27
|
+
* with no reason: it covers a live organization and also a create between
|
|
28
|
+
* its claim and its row, which must never read as "deleted". Both keep the
|
|
29
|
+
* AlreadyExists code, which the personal-organization retry keys on.
|
|
30
|
+
*
|
|
31
|
+
* Proven by __tests__/organization-slugs.test.ts and the organization
|
|
32
|
+
* conformance suite.
|
|
33
|
+
*/
|
|
34
|
+
import type { DescMessage } from "@bufbuild/protobuf";
|
|
35
|
+
import type { ConnectError } from "@connectrpc/connect";
|
|
36
|
+
|
|
37
|
+
import type { Logger } from "../../boot/logger.js";
|
|
38
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
39
|
+
import type { OrganizationSlugEntry, Store } from "../../store/interface.js";
|
|
40
|
+
import {
|
|
41
|
+
alreadyExistsError,
|
|
42
|
+
alreadyExistsWithReasonError,
|
|
43
|
+
internalError,
|
|
44
|
+
} from "../../pipeline/errors.js";
|
|
45
|
+
import type { PipelineStep } from "../../pipeline/pipeline.js";
|
|
46
|
+
import type { RequestContext } from "../../pipeline/request-context.js";
|
|
47
|
+
import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
|
|
48
|
+
import { metadataOf } from "../../pipeline/steps/shapes.js";
|
|
49
|
+
|
|
50
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
51
|
+
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
52
|
+
import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The ErrorInfo reason a create of a retired slug carries: a deleted
|
|
56
|
+
* organization held the slug, and a slug is never reused. Wire contract,
|
|
57
|
+
* documented on OrganizationCommandController.create; metadata `slug`.
|
|
58
|
+
*/
|
|
59
|
+
export const ORGANIZATION_SLUG_RESERVED = "ORGANIZATION_SLUG_RESERVED";
|
|
60
|
+
|
|
61
|
+
/** Where ClaimOrganizationSlug leaves the entry it won, for the release after a failure. */
|
|
62
|
+
const CLAIMED_SLUG_KEY = "organizationSlugClaim";
|
|
63
|
+
|
|
64
|
+
/** The copy of a retired slug's refusal. */
|
|
65
|
+
export function organizationSlugReservedMessage(slug: string): string {
|
|
66
|
+
return `Organization slug '${slug}' belonged to an organization that was deleted, and a slug is never reused`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The refusal for a slug an entry already holds: the reserved refusal for a
|
|
71
|
+
* retired entry, the existing duplicate copy for one still held.
|
|
72
|
+
*/
|
|
73
|
+
export function refusalForHeldSlug(entry: OrganizationSlugEntry): ConnectError {
|
|
74
|
+
if (entry.retiredAt !== "") {
|
|
75
|
+
return alreadyExistsWithReasonError(
|
|
76
|
+
organizationSlugReservedMessage(entry.slug),
|
|
77
|
+
{ reason: ORGANIZATION_SLUG_RESERVED, metadata: { slug: entry.slug } },
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return alreadyExistsError("Organization", `slug '${entry.slug}'`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Claims the new organization's slug, immediately before Persist. A lost
|
|
85
|
+
* claim is refused by the entry that holds the slug; a won claim is left in
|
|
86
|
+
* the request for releaseSlugClaimAfterFailure.
|
|
87
|
+
*/
|
|
88
|
+
export function newClaimOrganizationSlugStep(
|
|
89
|
+
store: Store,
|
|
90
|
+
): PipelineStep<typeof OrganizationSchema> {
|
|
91
|
+
return {
|
|
92
|
+
name: "ClaimOrganizationSlug",
|
|
93
|
+
async execute(
|
|
94
|
+
ctx: RequestContext<typeof OrganizationSchema>,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const slug = metadataOf(ctx.newState)?.slug ?? "";
|
|
97
|
+
// ResolveSlug and CheckDuplicate run first, so an empty slug here is
|
|
98
|
+
// a server-side ordering bug, not bad client input.
|
|
99
|
+
if (slug === "") {
|
|
100
|
+
throw internalError(
|
|
101
|
+
new Error("organization slug is empty"),
|
|
102
|
+
"failed to claim the organization slug",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
let claim;
|
|
106
|
+
try {
|
|
107
|
+
claim = await store.organizationSlugs.claim(slug);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw internalError(error, "failed to claim the organization slug");
|
|
110
|
+
}
|
|
111
|
+
if (!claim.claimed) {
|
|
112
|
+
throw refusalForHeldSlug(claim.entry);
|
|
113
|
+
}
|
|
114
|
+
ctx.set(CLAIMED_SLUG_KEY, claim.entry);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Frees the slug a failed create claimed, when its organization was never
|
|
121
|
+
* stored, so the caller's retry can take the slug again. Called by the
|
|
122
|
+
* create around its chain, as send-signal.ts releases a dedupe claim.
|
|
123
|
+
*
|
|
124
|
+
* Whether the row exists decides, rather than which step failed: a Persist
|
|
125
|
+
* that stored the row and then failed keeps the claim, as it must, because
|
|
126
|
+
* the organization exists. Any fault here is logged and leaves the slug
|
|
127
|
+
* claimed with no organization, which nothing can then take; that is a
|
|
128
|
+
* hand's to resolve, and far rarer than the failure it follows.
|
|
129
|
+
*/
|
|
130
|
+
export async function releaseSlugClaimAfterFailure(
|
|
131
|
+
store: Store,
|
|
132
|
+
logger: Logger,
|
|
133
|
+
ctx: RequestContext<typeof OrganizationSchema>,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const entry = ctx.get(CLAIMED_SLUG_KEY) as OrganizationSlugEntry | undefined;
|
|
136
|
+
if (entry === undefined) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
await store.getResource(
|
|
141
|
+
ApiResourceKind.organization,
|
|
142
|
+
entry.slug,
|
|
143
|
+
OrganizationSchema,
|
|
144
|
+
);
|
|
145
|
+
return; // stored: the organization exists and keeps its slug
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (!(error instanceof ResourceNotFoundError)) {
|
|
148
|
+
logger.error(
|
|
149
|
+
"organization create failed and its slug claim could not be checked; the slug stays claimed",
|
|
150
|
+
{
|
|
151
|
+
slug: entry.slug,
|
|
152
|
+
error: error instanceof Error ? error.message : String(error),
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await store.organizationSlugs.release(entry);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error(
|
|
162
|
+
"organization create failed and its slug claim could not be released; the slug stays claimed",
|
|
163
|
+
{
|
|
164
|
+
slug: entry.slug,
|
|
165
|
+
error: error instanceof Error ? error.message : String(error),
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Retires the organization's slug before anything else the delete does,
|
|
173
|
+
* right after the organization is loaded. Its entry may not exist yet (an
|
|
174
|
+
* organization created before the ledger, or by an older binary during a
|
|
175
|
+
* rolling upgrade), and retire records it either way, so the slug is
|
|
176
|
+
* retired before the row can go.
|
|
177
|
+
*
|
|
178
|
+
* A fault fails the delete with the organization intact. A delete that
|
|
179
|
+
* fails later (an edition's refusal on the pre-delete slot, a revocation
|
|
180
|
+
* fault) leaves a live organization whose slug reads as retired; nothing
|
|
181
|
+
* observes that, because a create of the slug is refused either way, and a
|
|
182
|
+
* retry of the delete resumes.
|
|
183
|
+
*/
|
|
184
|
+
export function newRetireOrganizationSlugStep<Desc extends DescMessage>(
|
|
185
|
+
store: Store,
|
|
186
|
+
): PipelineStep<Desc> {
|
|
187
|
+
return {
|
|
188
|
+
name: "RetireOrganizationSlug",
|
|
189
|
+
async execute(ctx: RequestContext<Desc>): Promise<void> {
|
|
190
|
+
const organization = ctx.get(EXISTING_RESOURCE_KEY) as
|
|
191
|
+
| Organization
|
|
192
|
+
| undefined;
|
|
193
|
+
const id = organization?.metadata?.id ?? "";
|
|
194
|
+
if (id === "") {
|
|
195
|
+
throw internalError(
|
|
196
|
+
new Error(
|
|
197
|
+
"organization delete reached RetireOrganizationSlug without its loaded row",
|
|
198
|
+
),
|
|
199
|
+
"failed to retire the organization slug",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
await store.organizationSlugs.retire(id);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw internalError(error, "failed to retire the organization slug");
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* uniqueness guarantee, mirroring cloud's OrganizationCreateHandler
|
|
11
11
|
* (CheckDuplicate + CopySlugToId) step-for-step.
|
|
12
12
|
*
|
|
13
|
-
* The same deviation shapes the delete
|
|
14
|
-
*
|
|
15
|
-
* rows BEFORE its row is deleted, and fails the
|
|
13
|
+
* The same deviation shapes the delete. A slug is never taken twice
|
|
14
|
+
* (slug-ledger.ts), and newRevokeOrganizationPoliciesStep still revokes
|
|
15
|
+
* the organization's policy rows BEFORE its row is deleted, and fails the
|
|
16
|
+
* delete when it cannot, so nothing that grants on the organization
|
|
17
|
+
* outlives it.
|
|
16
18
|
*
|
|
17
19
|
* Proven by organization.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
18
20
|
* __tests__/organization.test.ts and __tests__/organization-delete.test.ts.
|
|
@@ -28,6 +30,7 @@ import type { RequestContext } from "../../pipeline/request-context.js";
|
|
|
28
30
|
import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
|
|
29
31
|
import { metadataOf } from "../../pipeline/steps/shapes.js";
|
|
30
32
|
import type { IamPolicyGrantPath } from "../iampolicy/grant-path.js";
|
|
33
|
+
import { refusalForHeldSlug } from "./slug-ledger.js";
|
|
31
34
|
|
|
32
35
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
33
36
|
import { ApiResourceRefSchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
|
|
@@ -48,6 +51,13 @@ import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organizat
|
|
|
48
51
|
* organization. Checking existence by id (== the resolved slug) closes
|
|
49
52
|
* that hole and mirrors cloud's OrganizationCreateHandler.CheckDuplicate.
|
|
50
53
|
*
|
|
54
|
+
* A slug is taken for good (slug-ledger.ts), so the slug ledger is read
|
|
55
|
+
* first: a retired slug is refused with the reserved reason, a held one
|
|
56
|
+
* with the duplicate copy. The row is read after it for an organization no
|
|
57
|
+
* ledger entry records yet, one an older binary created during a rolling
|
|
58
|
+
* upgrade. This read is the early refusal, before any gate; the atomic
|
|
59
|
+
* guarantee is ClaimOrganizationSlug's, immediately before Persist.
|
|
60
|
+
*
|
|
51
61
|
* Runs after ResolveSlug (slug is set) and before BuildNewState/
|
|
52
62
|
* CopySlugToId (the id is not yet minted), so it keys on the slug value
|
|
53
63
|
* that will become the id.
|
|
@@ -70,6 +80,16 @@ export function newCheckOrgDuplicateStep(
|
|
|
70
80
|
throw internalError(new Error("organization slug is empty"), "duplicate check");
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
let entry;
|
|
84
|
+
try {
|
|
85
|
+
entry = await store.organizationSlugs.find(metadata.slug);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw internalError(error, "failed to check for duplicate organization");
|
|
88
|
+
}
|
|
89
|
+
if (entry !== undefined) {
|
|
90
|
+
throw refusalForHeldSlug(entry);
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
try {
|
|
74
94
|
await store.getResource(
|
|
75
95
|
ctx.apiResourceKind,
|
|
@@ -120,9 +140,11 @@ export function newCopySlugToIdStep(): PipelineStep<typeof OrganizationSchema> {
|
|
|
120
140
|
*
|
|
121
141
|
* The generic delete chains clean up after the row and log a fault,
|
|
122
142
|
* because a deleted resource's rows grant nothing once it is gone. An
|
|
123
|
-
* organization is the exception: its
|
|
124
|
-
*
|
|
125
|
-
*
|
|
143
|
+
* organization is the exception: its rows are the grants on the tenancy
|
|
144
|
+
* root itself, and the scope link of every resource under it. The slug is
|
|
145
|
+
* never taken again (slug-ledger.ts), so they cannot pass to a new holder,
|
|
146
|
+
* but a row left behind would still be a grant nobody administers. So this
|
|
147
|
+
* step does not catch. A fault fails the delete with the
|
|
126
148
|
* organization in place, and a retry resumes where the revocation stopped
|
|
127
149
|
* (the grant path revokes the organization's owners last, so the owner
|
|
128
150
|
* who retries still can). The grant path fires the composed driver's
|
|
@@ -963,7 +963,8 @@
|
|
|
963
963
|
"vision" : true,
|
|
964
964
|
"streaming" : true,
|
|
965
965
|
"thinking" : false,
|
|
966
|
-
"adaptiveThinking" : true
|
|
966
|
+
"adaptiveThinking" : true,
|
|
967
|
+
"thinkingRequired" : true
|
|
967
968
|
}
|
|
968
969
|
}, {
|
|
969
970
|
"id" : "claude-haiku-4.5",
|
|
@@ -995,8 +996,9 @@
|
|
|
995
996
|
"toolUse" : true,
|
|
996
997
|
"vision" : true,
|
|
997
998
|
"streaming" : true,
|
|
998
|
-
"thinking" :
|
|
999
|
-
"adaptiveThinking" : false
|
|
999
|
+
"thinking" : true,
|
|
1000
|
+
"adaptiveThinking" : false,
|
|
1001
|
+
"thinkingRequired" : false
|
|
1000
1002
|
}
|
|
1001
1003
|
}, {
|
|
1002
1004
|
"id" : "claude-opus-4.5",
|
|
@@ -1017,7 +1019,7 @@
|
|
|
1017
1019
|
"effectiveAt" : "2026-05-03T00:00:00Z"
|
|
1018
1020
|
},
|
|
1019
1021
|
"contextWindowTokens" : 200000,
|
|
1020
|
-
"maxOutputTokens" :
|
|
1022
|
+
"maxOutputTokens" : 64000,
|
|
1021
1023
|
"tokenCounterMethod" : "anthropic_native",
|
|
1022
1024
|
"summarization" : {
|
|
1023
1025
|
"triggerThreshold" : 180000,
|
|
@@ -1029,7 +1031,8 @@
|
|
|
1029
1031
|
"vision" : true,
|
|
1030
1032
|
"streaming" : true,
|
|
1031
1033
|
"thinking" : true,
|
|
1032
|
-
"adaptiveThinking" : false
|
|
1034
|
+
"adaptiveThinking" : false,
|
|
1035
|
+
"thinkingRequired" : false
|
|
1033
1036
|
}
|
|
1034
1037
|
}, {
|
|
1035
1038
|
"id" : "claude-opus-4.6",
|
|
@@ -1050,7 +1053,7 @@
|
|
|
1050
1053
|
"effectiveAt" : "2026-05-03T00:00:00Z"
|
|
1051
1054
|
},
|
|
1052
1055
|
"contextWindowTokens" : 200000,
|
|
1053
|
-
"maxOutputTokens" :
|
|
1056
|
+
"maxOutputTokens" : 128000,
|
|
1054
1057
|
"tokenCounterMethod" : "anthropic_native",
|
|
1055
1058
|
"summarization" : {
|
|
1056
1059
|
"triggerThreshold" : 180000,
|
|
@@ -1062,7 +1065,8 @@
|
|
|
1062
1065
|
"vision" : true,
|
|
1063
1066
|
"streaming" : true,
|
|
1064
1067
|
"thinking" : false,
|
|
1065
|
-
"adaptiveThinking" : true
|
|
1068
|
+
"adaptiveThinking" : true,
|
|
1069
|
+
"thinkingRequired" : false
|
|
1066
1070
|
}
|
|
1067
1071
|
}, {
|
|
1068
1072
|
"id" : "claude-opus-4.7",
|
|
@@ -1096,7 +1100,8 @@
|
|
|
1096
1100
|
"vision" : true,
|
|
1097
1101
|
"streaming" : true,
|
|
1098
1102
|
"thinking" : false,
|
|
1099
|
-
"adaptiveThinking" : true
|
|
1103
|
+
"adaptiveThinking" : true,
|
|
1104
|
+
"thinkingRequired" : false
|
|
1100
1105
|
}
|
|
1101
1106
|
}, {
|
|
1102
1107
|
"id" : "claude-opus-4.8",
|
|
@@ -1130,7 +1135,8 @@
|
|
|
1130
1135
|
"vision" : true,
|
|
1131
1136
|
"streaming" : true,
|
|
1132
1137
|
"thinking" : false,
|
|
1133
|
-
"adaptiveThinking" : true
|
|
1138
|
+
"adaptiveThinking" : true,
|
|
1139
|
+
"thinkingRequired" : false
|
|
1134
1140
|
}
|
|
1135
1141
|
}, {
|
|
1136
1142
|
"id" : "claude-sonnet-4.5",
|
|
@@ -1151,7 +1157,7 @@
|
|
|
1151
1157
|
"effectiveAt" : "2026-05-03T00:00:00Z"
|
|
1152
1158
|
},
|
|
1153
1159
|
"contextWindowTokens" : 200000,
|
|
1154
|
-
"maxOutputTokens" :
|
|
1160
|
+
"maxOutputTokens" : 64000,
|
|
1155
1161
|
"tokenCounterMethod" : "anthropic_native",
|
|
1156
1162
|
"summarization" : {
|
|
1157
1163
|
"triggerThreshold" : 180000,
|
|
@@ -1163,7 +1169,8 @@
|
|
|
1163
1169
|
"vision" : true,
|
|
1164
1170
|
"streaming" : true,
|
|
1165
1171
|
"thinking" : true,
|
|
1166
|
-
"adaptiveThinking" : false
|
|
1172
|
+
"adaptiveThinking" : false,
|
|
1173
|
+
"thinkingRequired" : false
|
|
1167
1174
|
}
|
|
1168
1175
|
}, {
|
|
1169
1176
|
"id" : "claude-sonnet-4.6",
|
|
@@ -1195,8 +1202,9 @@
|
|
|
1195
1202
|
"toolUse" : true,
|
|
1196
1203
|
"vision" : true,
|
|
1197
1204
|
"streaming" : true,
|
|
1198
|
-
"thinking" :
|
|
1199
|
-
"adaptiveThinking" :
|
|
1205
|
+
"thinking" : false,
|
|
1206
|
+
"adaptiveThinking" : true,
|
|
1207
|
+
"thinkingRequired" : false
|
|
1200
1208
|
}
|
|
1201
1209
|
}, {
|
|
1202
1210
|
"id" : "claude-sonnet-5",
|
|
@@ -1230,7 +1238,8 @@
|
|
|
1230
1238
|
"vision" : true,
|
|
1231
1239
|
"streaming" : true,
|
|
1232
1240
|
"thinking" : false,
|
|
1233
|
-
"adaptiveThinking" : true
|
|
1241
|
+
"adaptiveThinking" : true,
|
|
1242
|
+
"thinkingRequired" : false
|
|
1234
1243
|
}
|
|
1235
1244
|
} ]
|
|
1236
1245
|
}
|
|
@@ -63,6 +63,19 @@ export const FAST_VARIANT_KEY = "fast";
|
|
|
63
63
|
*/
|
|
64
64
|
export const THINKING_CAPABILITY_KEY = "thinking";
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The registry capability key of adaptive thinking, the second form that
|
|
68
|
+
* makes THINKING_MODE_ENABLED selectable (stigmer/stigmer#1280).
|
|
69
|
+
*/
|
|
70
|
+
export const ADAPTIVE_THINKING_CAPABILITY_KEY = "adaptiveThinking";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The registry capability key naming a model that always thinks and refuses
|
|
74
|
+
* a request to turn thinking off: an explicit THINKING_MODE_DISABLED is
|
|
75
|
+
* refused for it at create.
|
|
76
|
+
*/
|
|
77
|
+
export const THINKING_REQUIRED_CAPABILITY_KEY = "thinkingRequired";
|
|
78
|
+
|
|
66
79
|
export interface ModelRegistryStoreOptions {
|
|
67
80
|
/** Bundled build-time snapshot; MUST pass the sanity gate at boot. */
|
|
68
81
|
bundledDocument: string;
|
|
@@ -202,9 +215,9 @@ export class ModelRegistryStore implements ModelCatalogProvider {
|
|
|
202
215
|
* "thinking") true under the given harness. Capability flags are
|
|
203
216
|
* harness-scoped facts (they describe what works through that serving
|
|
204
217
|
* path), so there is no any-harness form: THINKING_MODE_ENABLED validates
|
|
205
|
-
* against the
|
|
206
|
-
*
|
|
207
|
-
*
|
|
218
|
+
* against the entry of the harness the execution runs on (oss#772,
|
|
219
|
+
* stigmer/stigmer#1280), because an id with an entry on both harnesses
|
|
220
|
+
* can declare different capabilities on each.
|
|
208
221
|
*/
|
|
209
222
|
hasCapabilityForHarness(
|
|
210
223
|
harness: string,
|
|
@@ -55,11 +55,13 @@
|
|
|
55
55
|
*
|
|
56
56
|
* The tenth, `org-delete:pre-delete`: the organization delete chain after
|
|
57
57
|
* LoadExistingForDelete and before any write, so the organization exists
|
|
58
|
-
* and is loaded (EXISTING_RESOURCE_KEY) while its steps run.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
58
|
+
* and is loaded (EXISTING_RESOURCE_KEY) while its steps run. Whatever an
|
|
59
|
+
* edition keeps for the organization must go before the row does, or be
|
|
60
|
+
* refused: a row left behind is trust or state that nobody administers.
|
|
61
|
+
* (The slug itself is never taken again, domain/organization/slug-ledger.ts,
|
|
62
|
+
* so nothing left behind can pass to a new holder of the slug; the order
|
|
63
|
+
* still keeps an edition's rows from outliving their organization.) Its
|
|
64
|
+
* steps may refuse (Enterprise refuses while an
|
|
63
65
|
* identity provider still signs in platform-managed organizations) or
|
|
64
66
|
* remove the edition's own rows; either way a throw fails the delete with
|
|
65
67
|
* the organization intact, and the retry re-runs every step, so each owns
|
package/src/index.ts
CHANGED
|
@@ -293,6 +293,7 @@ export { RequestContext } from "./pipeline/request-context.js";
|
|
|
293
293
|
export {
|
|
294
294
|
abortedError,
|
|
295
295
|
alreadyExistsError,
|
|
296
|
+
alreadyExistsWithReasonError,
|
|
296
297
|
ERROR_REASON_DOMAIN,
|
|
297
298
|
failedPreconditionError,
|
|
298
299
|
internalError,
|
|
@@ -398,6 +399,16 @@ export {
|
|
|
398
399
|
AuditNotFoundError,
|
|
399
400
|
ResourceNotFoundError,
|
|
400
401
|
} from "./store/interface.js";
|
|
402
|
+
// The organization-slug ledger (`Store.organizationSlugs`): an
|
|
403
|
+
// organization's slug is its for good. A composition that keeps its own
|
|
404
|
+
// rows by organization id retires the slugs its history holds through it,
|
|
405
|
+
// and a create of a retired slug carries the reason below.
|
|
406
|
+
export type {
|
|
407
|
+
OrganizationSlugClaim,
|
|
408
|
+
OrganizationSlugEntry,
|
|
409
|
+
OrganizationSlugStore,
|
|
410
|
+
} from "./store/interface.js";
|
|
411
|
+
export { ORGANIZATION_SLUG_RESERVED } from "./domain/organization/slug-ledger.js";
|
|
401
412
|
// The list index's read shapes, which `Store.queryResources` speaks
|
|
402
413
|
// (store/list-index.ts). Declaring an index stays internal: the list is
|
|
403
414
|
// the composition root's (boot/list-indexes.ts), one per server.
|
|
@@ -37,13 +37,6 @@ const SRC = path.resolve(HERE, "../..");
|
|
|
37
37
|
/** Known blind sites by module (relative to `src`), with their count. */
|
|
38
38
|
const PENDING: ReadonlyMap<string, number> = new Map([
|
|
39
39
|
// Store loads, stigmer/stigmer#1345 (fixed domain by domain).
|
|
40
|
-
["domain/agent/controller.ts", 1],
|
|
41
|
-
["domain/agentchannel/controller.ts", 1],
|
|
42
|
-
["domain/agentinstance/controller.ts", 1],
|
|
43
|
-
["domain/agentshare/controller.ts", 1],
|
|
44
|
-
["domain/artifact/controller.ts", 1],
|
|
45
|
-
["domain/environment/controller.ts", 1],
|
|
46
|
-
["domain/environment/steps.ts", 1],
|
|
47
40
|
["domain/plugin/controller.ts", 1],
|
|
48
41
|
["domain/skill/controller.ts", 1],
|
|
49
42
|
["domain/workflow/controller.ts", 1],
|
package/src/pipeline/errors.ts
CHANGED
|
@@ -53,6 +53,29 @@ export function alreadyExistsError(resource: string, id: string): ConnectError {
|
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* An AlreadyExists refusal a client acts on without parsing text: the
|
|
58
|
+
* authored message, and the reason carried as an ErrorInfo detail, exactly
|
|
59
|
+
* as failedPreconditionError carries one. The code stays AlreadyExists, so
|
|
60
|
+
* a caller that keys on the code alone (a retry with another name) keeps
|
|
61
|
+
* working.
|
|
62
|
+
*/
|
|
63
|
+
export function alreadyExistsWithReasonError(
|
|
64
|
+
message: string,
|
|
65
|
+
reason: RefusalReason,
|
|
66
|
+
): ConnectError {
|
|
67
|
+
return new ConnectError(message, Code.AlreadyExists, undefined, [
|
|
68
|
+
{
|
|
69
|
+
desc: ErrorInfoSchema,
|
|
70
|
+
value: create(ErrorInfoSchema, {
|
|
71
|
+
reason: reason.reason,
|
|
72
|
+
domain: ERROR_REASON_DOMAIN,
|
|
73
|
+
metadata: { ...reason.metadata },
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
/**
|
|
57
80
|
* Go FailedPreconditionError — the system is not in a state required for
|
|
58
81
|
* the operation (vs AlreadyExists, which tells the caller to stop). With a
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the driver-neutral half of the organization-slug ledger's data
|
|
3
|
+
* migration (organization-slug-history.ts): the frozen kind table is
|
|
4
|
+
* exactly the kinds `api_resource_kind.proto` scoped to an organization
|
|
5
|
+
* when the ledger arrived, spelled out here as the step's statement about
|
|
6
|
+
* the store as it was, each with a schema whose rows carry the shared
|
|
7
|
+
* metadata; a row's
|
|
8
|
+
* organization is read from its bytes, a row with no metadata or an empty
|
|
9
|
+
* organization names none, and bytes that do not decode are a thrown
|
|
10
|
+
* fault, never a skipped row. The drivers' replay tests pin the SQL around
|
|
11
|
+
* these functions.
|
|
12
|
+
*/
|
|
13
|
+
import { create, toBinary } from "@bufbuild/protobuf";
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
|
|
16
|
+
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
ORGANIZATION_SCOPED_KINDS_AT_LEDGER,
|
|
20
|
+
organizationNamedBy,
|
|
21
|
+
undecodableRowError,
|
|
22
|
+
} from "../organization-slug-history.js";
|
|
23
|
+
|
|
24
|
+
const AGENT = ORGANIZATION_SCOPED_KINDS_AT_LEDGER.find(
|
|
25
|
+
(entry) => entry.kind === "agent",
|
|
26
|
+
)!;
|
|
27
|
+
|
|
28
|
+
describe("the frozen kind table", () => {
|
|
29
|
+
it("is exactly the kinds api_resource_kind.proto scoped to an organization when the ledger arrived, in its order", () => {
|
|
30
|
+
expect(
|
|
31
|
+
ORGANIZATION_SCOPED_KINDS_AT_LEDGER.map((entry) => entry.kind),
|
|
32
|
+
).toEqual([
|
|
33
|
+
"iam_policy",
|
|
34
|
+
"invitation",
|
|
35
|
+
"identity_provider",
|
|
36
|
+
"oauth_app",
|
|
37
|
+
"platform_client",
|
|
38
|
+
"team",
|
|
39
|
+
"agent",
|
|
40
|
+
"session",
|
|
41
|
+
"skill",
|
|
42
|
+
"mcp_server",
|
|
43
|
+
"agent_instance",
|
|
44
|
+
"agent_share",
|
|
45
|
+
"agent_channel",
|
|
46
|
+
"channel_app",
|
|
47
|
+
"workflow",
|
|
48
|
+
"workflow_instance",
|
|
49
|
+
"workflow_execution",
|
|
50
|
+
"environment",
|
|
51
|
+
"artifact",
|
|
52
|
+
"schedule",
|
|
53
|
+
"memory",
|
|
54
|
+
"plugin",
|
|
55
|
+
"subscription",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("gives every kind a schema whose rows carry the metadata every resource shares", () => {
|
|
60
|
+
for (const entry of ORGANIZATION_SCOPED_KINDS_AT_LEDGER) {
|
|
61
|
+
const metadata = entry.schema.fields.find(
|
|
62
|
+
(field) => field.name === "metadata",
|
|
63
|
+
);
|
|
64
|
+
expect(metadata?.fieldKind, entry.kind).toBe("message");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("organizationNamedBy", () => {
|
|
70
|
+
it("reads the organization a row names", () => {
|
|
71
|
+
const bytes = toBinary(
|
|
72
|
+
AgentSchema,
|
|
73
|
+
create(AgentSchema, {
|
|
74
|
+
metadata: { id: "agt_1", slug: "helper", org: "acme" },
|
|
75
|
+
spec: { instructions: "a conformant instruction body" },
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
expect(organizationNamedBy(AGENT, bytes)).toBe("acme");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("names no organization for a row with no metadata or an empty org", () => {
|
|
82
|
+
expect(
|
|
83
|
+
organizationNamedBy(AGENT, toBinary(AgentSchema, create(AgentSchema))),
|
|
84
|
+
).toBe("");
|
|
85
|
+
expect(
|
|
86
|
+
organizationNamedBy(
|
|
87
|
+
AGENT,
|
|
88
|
+
toBinary(
|
|
89
|
+
AgentSchema,
|
|
90
|
+
create(AgentSchema, { metadata: { id: "agt_2", org: "" } }),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
).toBe("");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("throws on bytes that do not decode, and the driver's error names the row", () => {
|
|
97
|
+
const broken = new Uint8Array([0xff, 0xff, 0xff]);
|
|
98
|
+
expect(() => organizationNamedBy(AGENT, broken)).toThrow();
|
|
99
|
+
let caught: unknown;
|
|
100
|
+
try {
|
|
101
|
+
organizationNamedBy(AGENT, broken);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
caught = error;
|
|
104
|
+
}
|
|
105
|
+
expect(undecodableRowError(AGENT, "agt_broken", caught).message).toMatch(
|
|
106
|
+
/^agent 'agt_broken' cannot be read for the organization it names: /,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|