@toloka-platform/sdk 0.11.1 → 0.11.3
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/README.md +4 -2
- package/dist/cli/toloka.js +167 -160
- package/dist/cli/toloka.js.jsc +0 -0
- package/dist/cli/toloka.js.jsc.sha256 +1 -1
- package/dist/cli/toloka.js.sha256 +1 -1
- package/docs/authoring.md +15 -5
- package/docs/cli.md +48 -16
- package/docs/getting-started.md +1 -1
- package/docs/limitations.md +2 -2
- package/docs/testing.md +5 -3
- package/docs/troubleshooting.md +1 -1
- package/examples/README.md +11 -0
- package/examples/app-crowdtesting/README.md +41 -0
- package/examples/app-crowdtesting/fields.ts +6 -0
- package/examples/app-crowdtesting/nodes/app-flow-test/node.ts +381 -0
- package/examples/app-crowdtesting/nodes/collection/node.ts +8 -0
- package/examples/app-crowdtesting/pipeline.browser.integration.spec.ts +77 -0
- package/examples/app-crowdtesting/pipeline.ts +17 -0
- package/examples/egocentric-video-collection/README.md +35 -0
- package/examples/egocentric-video-collection/fields.ts +24 -0
- package/examples/egocentric-video-collection/nodes/collection/node.ts +5 -0
- package/examples/egocentric-video-collection/nodes/record-activity/node.ts +760 -0
- package/examples/egocentric-video-collection/pipeline.browser.integration.spec.ts +65 -0
- package/examples/egocentric-video-collection/pipeline.ts +17 -0
- package/examples/product-moderation/README.md +49 -0
- package/examples/product-moderation/fields.ts +37 -0
- package/examples/product-moderation/nodes/collection/node.ts +16 -0
- package/examples/product-moderation/nodes/moderate-product/instruction.ts +77 -0
- package/examples/product-moderation/nodes/moderate-product/node.ts +368 -0
- package/examples/product-moderation/pipeline.browser.integration.spec.ts +48 -0
- package/examples/product-moderation/pipeline.ts +33 -0
- package/examples/video-pair-rating/fields.ts +16 -4
- package/examples/video-pair-rating/nodes/adjudicate/node.ts +78 -3
- package/examples/video-pair-rating/nodes/adjudicate_op/node.ts +28 -3
- package/examples/video-pair-rating/nodes/overall_preference_pick/node.ts +28 -3
- package/examples/video-pair-rating/nodes/rate_pair/node.ts +65 -2
- package/openapi/gen/api.ts +22 -0
- package/openapi/gen/models/generationBudget.ts +8 -0
- package/openapi/gen/models/generationBudgetReasoningEffort.ts +2 -0
- package/openapi/gen/models/generationModel.ts +12 -0
- package/openapi/gen/models/generationModelDefaultReasoningEffort.ts +2 -0
- package/openapi/gen/models/generationModelId.ts +2 -0
- package/openapi/gen/models/index.ts +8 -0
- package/openapi/gen/models/invalidFieldSchema.ts +6 -0
- package/openapi/gen/models/listGenerationModels200.ts +7 -0
- package/openapi/gen/models/listGenerationModels200DefaultGenerationModelId.ts +2 -0
- package/openapi/gen/models/pipelineGraphSnapshot.ts +2 -0
- package/package.json +2 -2
- package/skills/catalog.json +16 -12
- package/skills/toloka-build/SKILL.md +44 -56
- package/skills/toloka-build/references/human-work.md +156 -0
- package/skills/toloka-build/references/local-setup.md +136 -0
- package/skills/toloka-build/references/node-checks.md +11 -0
- package/skills/toloka-build/references/node-semantics.md +22 -213
- package/skills/toloka-build/references/task-ui.md +61 -0
- package/skills/toloka-build/references/workflow-and-cli.md +29 -227
- package/skills/toloka-fine-tuning/SKILL.md +1 -1
- package/skills/toloka-get-started/SKILL.md +1 -1
- package/skills/toloka-gisting/SKILL.md +1 -1
- package/src/authoring/index.ts +26 -7
- package/src/checks/task-ui-typecheck.ts +3 -3
- package/src/cli/analytics.ts +1 -0
- package/src/cli/auth.ts +51 -18
- package/src/cli/check.ts +88 -17
- package/src/cli/commands/auth.ts +5 -3
- package/src/cli/commands/generation-model.ts +18 -0
- package/src/cli/commands/pipeline.ts +1 -1
- package/src/cli/commands/project.ts +7 -2
- package/src/cli/commands/source.ts +14 -3
- package/src/cli/commands/ui.ts +23 -0
- package/src/cli/config.ts +15 -9
- package/src/cli/context.ts +30 -12
- package/src/cli/credentials.ts +2 -2
- package/src/cli/environment.ts +5 -1
- package/src/cli/generation-models.ts +46 -0
- package/src/cli/program.ts +7 -0
- package/src/cli/project-context.ts +5 -2
- package/src/cli/project.ts +288 -138
- package/src/cli/trainings.ts +12 -0
- package/src/cli/ui.ts +63 -0
- package/src/client.ts +5 -0
- package/src/core-entity-types.json +32 -0
- package/src/graph-rules.json +11 -0
- package/src/importer.ts +46 -40
- package/src/index.ts +4 -0
- package/src/testing/task-ui-browser.ts +45 -2
- package/src/testing/task-ui.ts +2 -9
- package/src/transport.ts +28 -6
- package/src/validate.ts +232 -2
package/src/cli/auth.ts
CHANGED
|
@@ -451,27 +451,47 @@ export type AuthStatusOptions = {
|
|
|
451
451
|
export type AuthStatusResult = { readonly authenticated: boolean; readonly output: string };
|
|
452
452
|
|
|
453
453
|
type ResolvedValue = { readonly value: string; readonly source: string };
|
|
454
|
+
type ResolvedCredential = ResolvedValue & {
|
|
455
|
+
readonly type: "apiKey" | "scopedToken";
|
|
456
|
+
readonly environmentVariable?: "TOLOKA_SCOPED_TOKEN" | "TOLOKA_API_KEY";
|
|
457
|
+
};
|
|
454
458
|
|
|
455
|
-
const firstDefined = (
|
|
456
|
-
candidates
|
|
457
|
-
): ResolvedValue | undefined => candidates.find((candidate) => candidate !== undefined);
|
|
459
|
+
const firstDefined = <T>(candidates: ReadonlyArray<T | undefined>): T | undefined =>
|
|
460
|
+
candidates.find((candidate) => candidate !== undefined);
|
|
458
461
|
|
|
459
|
-
const
|
|
462
|
+
const resolveStatusCredential = (
|
|
460
463
|
options: AuthStatusOptions,
|
|
461
464
|
environment: CliEnvironment,
|
|
462
465
|
found: FoundCredentials | undefined,
|
|
463
466
|
storedPath: string,
|
|
464
|
-
):
|
|
465
|
-
firstDefined([
|
|
467
|
+
): ResolvedCredential | undefined =>
|
|
468
|
+
firstDefined<ResolvedCredential>([
|
|
466
469
|
options.flagApiKey === undefined
|
|
467
470
|
? undefined
|
|
468
|
-
: { value: options.flagApiKey, source: "from --api-key" },
|
|
471
|
+
: { type: "apiKey", value: options.flagApiKey, source: "from --api-key" },
|
|
472
|
+
environment.TOLOKA_SCOPED_TOKEN === undefined
|
|
473
|
+
? undefined
|
|
474
|
+
: {
|
|
475
|
+
type: "scopedToken",
|
|
476
|
+
value: environment.TOLOKA_SCOPED_TOKEN,
|
|
477
|
+
source: "from TOLOKA_SCOPED_TOKEN",
|
|
478
|
+
environmentVariable: "TOLOKA_SCOPED_TOKEN",
|
|
479
|
+
},
|
|
469
480
|
environment.TOLOKA_API_KEY === undefined
|
|
470
481
|
? undefined
|
|
471
|
-
: {
|
|
482
|
+
: {
|
|
483
|
+
type: "apiKey",
|
|
484
|
+
value: environment.TOLOKA_API_KEY,
|
|
485
|
+
source: "from TOLOKA_API_KEY",
|
|
486
|
+
environmentVariable: "TOLOKA_API_KEY",
|
|
487
|
+
},
|
|
472
488
|
found?.credentials.apiKey === undefined
|
|
473
489
|
? undefined
|
|
474
|
-
: {
|
|
490
|
+
: {
|
|
491
|
+
type: "apiKey",
|
|
492
|
+
value: found.credentials.apiKey,
|
|
493
|
+
source: `stored in ${storedPath}`,
|
|
494
|
+
},
|
|
475
495
|
]);
|
|
476
496
|
|
|
477
497
|
const resolvePlatform = (
|
|
@@ -576,17 +596,23 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
|
|
|
576
596
|
const now = (options.now ?? unixSeconds)();
|
|
577
597
|
const credentialLookup =
|
|
578
598
|
options.credentialLookup ??
|
|
579
|
-
(await findCommandCredentials(
|
|
599
|
+
(await findCommandCredentials(
|
|
600
|
+
environment,
|
|
601
|
+
options.flagApiKey ??
|
|
602
|
+
environment.TOLOKA_SCOPED_TOKEN ??
|
|
603
|
+
environment.TOLOKA_API_KEY,
|
|
604
|
+
));
|
|
580
605
|
const { found, warning } = credentialLookup;
|
|
581
606
|
const storedPath = found?.path ?? credentialsPath(environment);
|
|
582
|
-
const
|
|
607
|
+
const credential = resolveStatusCredential(options, environment, found, storedPath);
|
|
583
608
|
const platform = resolvePlatform(options, environment, found);
|
|
584
|
-
const browser =
|
|
609
|
+
const browser =
|
|
610
|
+
credential === undefined ? await findBrowserLogin(platform.value, environment) : undefined;
|
|
585
611
|
const projectLine = formatProjectContextLine(options.project, found?.credentials);
|
|
586
612
|
|
|
587
613
|
const lines: string[] = warning === undefined ? [] : [warning, ""];
|
|
588
614
|
|
|
589
|
-
if (
|
|
615
|
+
if (credential === undefined && browser === undefined) {
|
|
590
616
|
lines.push(
|
|
591
617
|
"Not logged in.",
|
|
592
618
|
"",
|
|
@@ -600,11 +626,15 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
|
|
|
600
626
|
const { value: baseUrl, source: urlSource } = platform;
|
|
601
627
|
lines.push(`Platform: ${baseUrl} (${urlSource})`);
|
|
602
628
|
let clientConfig: ClientConfig;
|
|
603
|
-
if (
|
|
629
|
+
if (credential !== undefined) {
|
|
630
|
+
const label = credential.type === "scopedToken" ? "Scoped token:" : "API key: ";
|
|
604
631
|
lines.push(
|
|
605
|
-
|
|
632
|
+
`${label} ${options.showToken === true ? credential.value : maskApiKey(credential.value)} (${credential.source})`,
|
|
606
633
|
);
|
|
607
|
-
clientConfig =
|
|
634
|
+
clientConfig =
|
|
635
|
+
credential.type === "scopedToken"
|
|
636
|
+
? { baseUrl, scopedToken: credential.value }
|
|
637
|
+
: { baseUrl, apiKey: credential.value };
|
|
608
638
|
} else {
|
|
609
639
|
assertCli(browser, "No login is in effect.", { hint: { description: "Run toloka login." } });
|
|
610
640
|
const status = await browserStatus(browser, baseUrl, environment, options, now);
|
|
@@ -617,8 +647,11 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
|
|
|
617
647
|
}
|
|
618
648
|
lines.push(projectLine);
|
|
619
649
|
|
|
620
|
-
if (
|
|
621
|
-
lines.push(
|
|
650
|
+
if (found?.credentials.apiKey !== undefined && credential?.environmentVariable !== undefined) {
|
|
651
|
+
lines.push(
|
|
652
|
+
"",
|
|
653
|
+
`${credential.environmentVariable} is set, so the key stored in ${found.path} is ignored.`,
|
|
654
|
+
);
|
|
622
655
|
}
|
|
623
656
|
if (found?.legacy === true) {
|
|
624
657
|
lines.push(
|
package/src/cli/check.ts
CHANGED
|
@@ -23,14 +23,21 @@ import {
|
|
|
23
23
|
type TestEvidence,
|
|
24
24
|
type TestEvidenceKind,
|
|
25
25
|
} from "../testing/test-evidence";
|
|
26
|
+
import { platformRuleIssues } from "../validate";
|
|
26
27
|
import { cliRelaunchCommand } from "./cli-entry";
|
|
27
28
|
import { bunSubprocessEnvironment } from "./native-runtime";
|
|
28
|
-
import {
|
|
29
|
+
import { CREDENTIAL_ENVIRONMENT_NAMES } from "./environment";
|
|
29
30
|
import { loadProjectSource, type ProjectSource } from "./source";
|
|
30
31
|
import { assertCli, REPORT_HINT, TolokaCliError } from "./cli-error";
|
|
31
32
|
import type { TolokaCliErrorOptions } from "./cli-error";
|
|
32
33
|
|
|
33
|
-
export type CheckStatus =
|
|
34
|
+
export type CheckStatus =
|
|
35
|
+
| "passed"
|
|
36
|
+
| "failed"
|
|
37
|
+
| "missing"
|
|
38
|
+
| "partial"
|
|
39
|
+
| "warning"
|
|
40
|
+
| "infrastructure_error";
|
|
34
41
|
export type CheckKind =
|
|
35
42
|
| "source"
|
|
36
43
|
| "project-types"
|
|
@@ -38,6 +45,7 @@ export type CheckKind =
|
|
|
38
45
|
| "code-test"
|
|
39
46
|
| "ui-test"
|
|
40
47
|
| "pipeline-flow"
|
|
48
|
+
| "platform-rules"
|
|
41
49
|
| "test-runner";
|
|
42
50
|
|
|
43
51
|
export type CheckDiagnostic = {
|
|
@@ -276,13 +284,13 @@ const loadSourceWithoutPlatformCredentials = async (
|
|
|
276
284
|
environment: NodeJS.ProcessEnv,
|
|
277
285
|
): Promise<ProjectSource> => {
|
|
278
286
|
const targets = [process.env, environment];
|
|
279
|
-
const saved = targets.map((current) =>
|
|
280
|
-
for (const current of targets) for (const name of
|
|
287
|
+
const saved = targets.map((current) => CREDENTIAL_ENVIRONMENT_NAMES.map((name) => current[name]));
|
|
288
|
+
for (const current of targets) for (const name of CREDENTIAL_ENVIRONMENT_NAMES) delete current[name];
|
|
281
289
|
try {
|
|
282
290
|
return await loadProjectSource(sourceRoot);
|
|
283
291
|
} finally {
|
|
284
292
|
targets.forEach((current, target) => {
|
|
285
|
-
|
|
293
|
+
CREDENTIAL_ENVIRONMENT_NAMES.forEach((name, index) => {
|
|
286
294
|
const value = saved[target]?.[index];
|
|
287
295
|
if (value === undefined) delete current[name];
|
|
288
296
|
else current[name] = value;
|
|
@@ -504,6 +512,39 @@ const uiTypeResults = async (
|
|
|
504
512
|
return results;
|
|
505
513
|
};
|
|
506
514
|
|
|
515
|
+
const platformRuleResults = (
|
|
516
|
+
source: ProjectSource,
|
|
517
|
+
selected: readonly CheckInventoryPipeline[],
|
|
518
|
+
): CheckResult[] => {
|
|
519
|
+
const selectedDirectories = new Set(selected.map((pipeline) => pipeline.sourceDirectory));
|
|
520
|
+
return source.pipelines
|
|
521
|
+
.filter((pipeline) => selectedDirectories.has(pipeline.sourceDirectory))
|
|
522
|
+
.map((pipeline) => {
|
|
523
|
+
const issues = platformRuleIssues(pipeline.definition.graph);
|
|
524
|
+
return {
|
|
525
|
+
scope: "pipeline",
|
|
526
|
+
kind: "platform-rules",
|
|
527
|
+
status: issues.length === 0 ? "passed" : "warning",
|
|
528
|
+
sourceDirectory: pipeline.sourceDirectory,
|
|
529
|
+
pipelineFingerprint: pipelineFingerprint(pipeline.definition),
|
|
530
|
+
...boundedDiagnostics(
|
|
531
|
+
issues.length === 0
|
|
532
|
+
? []
|
|
533
|
+
: [
|
|
534
|
+
...issues.map((issue) => ({
|
|
535
|
+
message: `node ${issue.node}: ${issue.reason}`,
|
|
536
|
+
severity: "warning" as const,
|
|
537
|
+
})),
|
|
538
|
+
{
|
|
539
|
+
message: "toloka apply refuses to write this pipeline until these are fixed.",
|
|
540
|
+
severity: "warning" as const,
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
),
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
};
|
|
547
|
+
|
|
507
548
|
const evidenceStatus = (
|
|
508
549
|
evidence: readonly TestEvidence[],
|
|
509
550
|
): Pick<CheckResult, "status" | "diagnostics" | "truncatedDiagnosticCount"> => {
|
|
@@ -536,9 +577,9 @@ const evidenceStatus = (
|
|
|
536
577
|
status: "partial",
|
|
537
578
|
diagnostics: [
|
|
538
579
|
{
|
|
539
|
-
message: `Hardware-backed fields not
|
|
580
|
+
message: `Hardware-backed fields not covered by a real device: ${[
|
|
540
581
|
...new Set(passing.flatMap((entry) => entry.untestedHardwareFields ?? [])),
|
|
541
|
-
].join(", ")}
|
|
582
|
+
].join(", ")}. This does not block apply; try the task on a real device before launching a run.`,
|
|
542
583
|
},
|
|
543
584
|
],
|
|
544
585
|
};
|
|
@@ -559,6 +600,38 @@ const evidenceFor = (
|
|
|
559
600
|
(nodeId === undefined || entry.nodeId === nodeId),
|
|
560
601
|
);
|
|
561
602
|
|
|
603
|
+
const typedTestUsage: Record<TestEvidenceKind, { call: string; counts: string }> = {
|
|
604
|
+
code: { call: "test.code(pipeline, node, name, callback)", counts: "runs the node" },
|
|
605
|
+
ui: { call: "test.ui(pipeline, node, name, callback)", counts: "submits the rendered task" },
|
|
606
|
+
pipeline: { call: "test.pipeline(pipeline, name, callback)", counts: "runs the connected nodes" },
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const coverageStatus = (
|
|
610
|
+
evidence: readonly TestEvidence[],
|
|
611
|
+
inventory: readonly CheckInventoryPipeline[],
|
|
612
|
+
pipeline: CheckInventoryPipeline,
|
|
613
|
+
kind: TestEvidenceKind,
|
|
614
|
+
nodeId?: string,
|
|
615
|
+
): Pick<CheckResult, "status" | "diagnostics" | "truncatedDiagnosticCount"> => {
|
|
616
|
+
const status = evidenceStatus(evidenceFor(evidence, pipeline, kind, nodeId));
|
|
617
|
+
if (status.status !== "missing") return status;
|
|
618
|
+
const target = nodeId === undefined ? "this pipeline" : "this node";
|
|
619
|
+
const { call, counts } = typedTestUsage[kind];
|
|
620
|
+
const knownFingerprints = new Set(inventory.map((candidate) => candidate.pipelineFingerprint));
|
|
621
|
+
const strayTests = evidence.filter(
|
|
622
|
+
(entry) =>
|
|
623
|
+
entry.kind === kind &&
|
|
624
|
+
entry.status === "passed" &&
|
|
625
|
+
entry.nodeId === nodeId &&
|
|
626
|
+
!knownFingerprints.has(entry.pipelineFingerprint),
|
|
627
|
+
);
|
|
628
|
+
const message =
|
|
629
|
+
strayTests.length > 0
|
|
630
|
+
? `Passing ${kind} tests for ${target} (${strayTests.length}) ran against a different pipeline definition than the one ${pipeline.sourceDirectory}/pipeline.ts exports and do not count. Pass that exported pipeline to the test instead of building one inside the test.`
|
|
631
|
+
: `No passing ${kind} test covers ${target}. Add ${call} with the pipeline ${pipeline.sourceDirectory}/pipeline.ts exports; it counts once it ${counts}, makes an assertion, and passes.`;
|
|
632
|
+
return { status: "missing", diagnostics: [{ message }] };
|
|
633
|
+
};
|
|
634
|
+
|
|
562
635
|
const coverageResults = (
|
|
563
636
|
inventory: readonly CheckInventoryPipeline[],
|
|
564
637
|
evidence: readonly TestEvidence[],
|
|
@@ -576,7 +649,7 @@ const coverageResults = (
|
|
|
576
649
|
sourceDirectory: pipeline.sourceDirectory,
|
|
577
650
|
pipelineFingerprint: pipeline.pipelineFingerprint,
|
|
578
651
|
nodeId: node.id,
|
|
579
|
-
...
|
|
652
|
+
...coverageStatus(evidence, inventory, pipeline, evidenceKind, node.id),
|
|
580
653
|
});
|
|
581
654
|
}
|
|
582
655
|
if (nodeSelector === undefined) {
|
|
@@ -585,7 +658,7 @@ const coverageResults = (
|
|
|
585
658
|
kind: "pipeline-flow",
|
|
586
659
|
sourceDirectory: pipeline.sourceDirectory,
|
|
587
660
|
pipelineFingerprint: pipeline.pipelineFingerprint,
|
|
588
|
-
...
|
|
661
|
+
...coverageStatus(evidence, inventory, pipeline, "pipeline"),
|
|
589
662
|
});
|
|
590
663
|
}
|
|
591
664
|
return result;
|
|
@@ -596,6 +669,7 @@ const statusLabel: Record<CheckStatus, string> = {
|
|
|
596
669
|
failed: "FAIL",
|
|
597
670
|
missing: "MISSING",
|
|
598
671
|
partial: "PARTIAL",
|
|
672
|
+
warning: "WARN",
|
|
599
673
|
infrastructure_error: "ERROR",
|
|
600
674
|
};
|
|
601
675
|
|
|
@@ -629,13 +703,7 @@ export const formatCheckResults = (results: readonly CheckResult[]): string =>
|
|
|
629
703
|
|
|
630
704
|
const exitCodeForResults = (results: readonly CheckResult[]): 0 | 1 | 2 => {
|
|
631
705
|
if (results.some((result) => result.status === "infrastructure_error")) return 2;
|
|
632
|
-
if (
|
|
633
|
-
results.some(
|
|
634
|
-
(result) =>
|
|
635
|
-
result.status === "failed" || result.status === "missing" || result.status === "partial",
|
|
636
|
-
)
|
|
637
|
-
)
|
|
638
|
-
return 1;
|
|
706
|
+
if (results.some((result) => result.status === "failed" || result.status === "missing")) return 1;
|
|
639
707
|
return 0;
|
|
640
708
|
};
|
|
641
709
|
|
|
@@ -693,7 +761,7 @@ export async function checkProject(
|
|
|
693
761
|
try {
|
|
694
762
|
const selectedPipeline = selected.pipelines.length === 1 ? selected.pipelines[0] : undefined;
|
|
695
763
|
const checkEnvironment = { ...(options.environment ?? process.env) };
|
|
696
|
-
for (const name of
|
|
764
|
+
for (const name of CREDENTIAL_ENVIRONMENT_NAMES) delete checkEnvironment[name];
|
|
697
765
|
const environment = {
|
|
698
766
|
...checkEnvironment,
|
|
699
767
|
[TEST_EVIDENCE_DIR_ENV]: evidenceDirectory,
|
|
@@ -723,6 +791,7 @@ export async function checkProject(
|
|
|
723
791
|
...uiResults,
|
|
724
792
|
projectToolResult("test-runner", vitestRun),
|
|
725
793
|
...coverageResults(selected.pipelines, evidence, selected.nodeId),
|
|
794
|
+
...(selected.nodeId === undefined ? platformRuleResults(source, selected.pipelines) : []),
|
|
726
795
|
]);
|
|
727
796
|
const exitCode = exitCodeForResults(results);
|
|
728
797
|
return {
|
|
@@ -787,6 +856,7 @@ const isCheckKind = (value: unknown): value is CheckKind =>
|
|
|
787
856
|
value === "code-test" ||
|
|
788
857
|
value === "ui-test" ||
|
|
789
858
|
value === "pipeline-flow" ||
|
|
859
|
+
value === "platform-rules" ||
|
|
790
860
|
value === "test-runner";
|
|
791
861
|
|
|
792
862
|
const isCheckStatus = (value: unknown): value is CheckStatus =>
|
|
@@ -794,6 +864,7 @@ const isCheckStatus = (value: unknown): value is CheckStatus =>
|
|
|
794
864
|
value === "failed" ||
|
|
795
865
|
value === "missing" ||
|
|
796
866
|
value === "partial" ||
|
|
867
|
+
value === "warning" ||
|
|
797
868
|
value === "infrastructure_error";
|
|
798
869
|
|
|
799
870
|
const parseFreshCheckDiagnostic = (value: unknown): CheckDiagnostic => {
|
package/src/cli/commands/auth.ts
CHANGED
|
@@ -59,13 +59,15 @@ export const registerAuthCommands = (program: Command, host: CommandHost): void
|
|
|
59
59
|
program
|
|
60
60
|
.command("auth-status")
|
|
61
61
|
.alias("whoami")
|
|
62
|
-
.description("show which platform and
|
|
63
|
-
.option("--show-token", "print the API key or access token instead of masking it", false)
|
|
62
|
+
.description("show which platform and credential the CLI would use")
|
|
63
|
+
.option("--show-token", "print the API key, scoped token, or access token instead of masking it", false)
|
|
64
64
|
.action(async (options: { showToken: boolean }) => {
|
|
65
65
|
const flags = globals();
|
|
66
66
|
const credentialLookup = await findCommandCredentials(
|
|
67
67
|
context.environment,
|
|
68
|
-
flags.apiKey ??
|
|
68
|
+
flags.apiKey ??
|
|
69
|
+
context.environment.TOLOKA_SCOPED_TOKEN ??
|
|
70
|
+
context.environment.TOLOKA_API_KEY,
|
|
69
71
|
);
|
|
70
72
|
const stored = credentialLookup.found?.credentials;
|
|
71
73
|
const sourceRoot = await resolveSourceRoot(flags, context.io);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { resolveContext, type CommandHost } from "../context";
|
|
3
|
+
import { listGenerationModels } from "../generation-models";
|
|
4
|
+
|
|
5
|
+
export const registerGenerationModelCommands = (program: Command, host: CommandHost): void => {
|
|
6
|
+
const { context, globals } = host;
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.command("generation-model")
|
|
10
|
+
.description("inspect the models synthetic generation nodes can use")
|
|
11
|
+
.command("list")
|
|
12
|
+
.option("--json", "print JSON", false)
|
|
13
|
+
.description("list generation models with reasoning efforts and maxCostPerAttempt bounds")
|
|
14
|
+
.action(async (options: { json: boolean }) => {
|
|
15
|
+
const { client } = await resolveContext(globals(), context);
|
|
16
|
+
await listGenerationModels(client, { json: options.json }, context.io);
|
|
17
|
+
});
|
|
18
|
+
};
|
|
@@ -38,7 +38,7 @@ export const registerPipelineCommands = (program: Command, host: CommandHost): v
|
|
|
38
38
|
.argument("[name]", pipelineNameHelp)
|
|
39
39
|
.option("--wait", "wait for the run to reach a terminal state", false)
|
|
40
40
|
.option("--timeout <ms>", "how long to wait", positiveInteger)
|
|
41
|
-
.description("start a run on
|
|
41
|
+
.description("start a run on an applied pipeline")
|
|
42
42
|
.action(async (name: string | undefined, options: { wait: boolean; timeout?: number }) => {
|
|
43
43
|
const { client, state, projectId } = await resolveContext(globals(), context);
|
|
44
44
|
assertProjectSelected(projectId);
|
|
@@ -15,6 +15,7 @@ import { readTolokaState } from "../../state";
|
|
|
15
15
|
import { assertCli } from "../cli-error";
|
|
16
16
|
import { formatTable } from "../table";
|
|
17
17
|
import type { CreateScopedTokenBodyAnyOfPreset, ScopedTokenSummary } from "../../../openapi/gen/models";
|
|
18
|
+
import { SCOPED_TOKEN_PREFIX } from "../../transport";
|
|
18
19
|
|
|
19
20
|
const TOKEN_PRESETS: readonly CreateScopedTokenBodyAnyOfPreset[] = [
|
|
20
21
|
"read",
|
|
@@ -157,8 +158,12 @@ export const registerProjectCommands = (program: Command, host: CommandHost): vo
|
|
|
157
158
|
expiresIn?: number;
|
|
158
159
|
projects?: string;
|
|
159
160
|
}) => {
|
|
160
|
-
const { client, projectId } = await resolveContext(globals(), context);
|
|
161
|
+
const { client, credential, projectId } = await resolveContext(globals(), context);
|
|
161
162
|
assertProjectSelected(projectId);
|
|
163
|
+
const usesScopedToken =
|
|
164
|
+
credential.type === "scopedToken" ||
|
|
165
|
+
(credential.type === "apiKey" && credential.apiKey.startsWith(SCOPED_TOKEN_PREFIX));
|
|
166
|
+
assertCli(!usesScopedToken, "A scoped token cannot issue tokens.", { hint: { disabled: true } });
|
|
162
167
|
const scope =
|
|
163
168
|
options.permissions === undefined
|
|
164
169
|
? { preset: options.preset }
|
|
@@ -174,7 +179,7 @@ export const registerProjectCommands = (program: Command, host: CommandHost): vo
|
|
|
174
179
|
: { projectIds: options.projects.split(",").map((projectId) => projectId.trim()) }),
|
|
175
180
|
});
|
|
176
181
|
context.io.stdout(token);
|
|
177
|
-
context.io.stderr(`Token expires at ${expiresAt}. Use it as
|
|
182
|
+
context.io.stderr(`Token expires at ${expiresAt}. Use it as TOLOKA_SCOPED_TOKEN.`);
|
|
178
183
|
},
|
|
179
184
|
);
|
|
180
185
|
project
|
|
@@ -14,6 +14,9 @@ import { applyProject, importProject, planProject, validateProject } from "../pr
|
|
|
14
14
|
import { assertProjectSelected, resolveProjectContext } from "../project-context";
|
|
15
15
|
import { readTolokaState } from "../../state";
|
|
16
16
|
import { assertCli, REPORT_HINT } from "../cli-error";
|
|
17
|
+
import { writeOutput } from "../table";
|
|
18
|
+
|
|
19
|
+
const VALIDATE_WAIT = { timeoutMs: 5 * 60_000, pollIntervalMs: 5_000 };
|
|
17
20
|
|
|
18
21
|
const runChecksInterruptibly = async (
|
|
19
22
|
options: CheckProjectOptions,
|
|
@@ -88,24 +91,32 @@ export const registerSourceCommands = (program: Command, host: CommandHost): voi
|
|
|
88
91
|
|
|
89
92
|
program
|
|
90
93
|
.command("plan")
|
|
94
|
+
.option("--json", "print the planned changes and missing secrets as JSON", false)
|
|
91
95
|
.description("show what apply would change")
|
|
92
|
-
.action(async () => {
|
|
96
|
+
.action(async (options: { json: boolean }) => {
|
|
93
97
|
const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
|
|
94
98
|
const result = await planProject(client, {
|
|
95
99
|
sourceRoot,
|
|
96
100
|
...(projectId === undefined ? {} : { projectId }),
|
|
97
101
|
});
|
|
98
|
-
|
|
102
|
+
writeOutput(
|
|
103
|
+
context.io,
|
|
104
|
+
options.json,
|
|
105
|
+
{ changes: result.changes, missingSecrets: result.missingSecrets },
|
|
106
|
+
result.output,
|
|
107
|
+
);
|
|
99
108
|
});
|
|
100
109
|
|
|
101
110
|
program
|
|
102
111
|
.command("validate")
|
|
112
|
+
.option("--wait", "wait up to 5 minutes for running lints and checks to finish", false)
|
|
103
113
|
.description("run platform spec validation and report matching checks")
|
|
104
|
-
.action(async () => {
|
|
114
|
+
.action(async (options: { wait: boolean }) => {
|
|
105
115
|
const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
|
|
106
116
|
const result = await validateProject(client, {
|
|
107
117
|
sourceRoot,
|
|
108
118
|
...(projectId === undefined ? {} : { projectId }),
|
|
119
|
+
...(options.wait ? { wait: VALIDATE_WAIT } : {}),
|
|
109
120
|
});
|
|
110
121
|
context.io.stdout(result.output);
|
|
111
122
|
if (!result.valid) exitCode.value = 1;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { describeTaskUiComponents } from "../../testing/task-ui-browser";
|
|
3
|
+
import { resolveSourceRoot, type CommandHost } from "../context";
|
|
4
|
+
import { uiComponentsCommand } from "../ui";
|
|
5
|
+
|
|
6
|
+
export const registerUiCommands = (program: Command, host: CommandHost): void => {
|
|
7
|
+
const { context, globals } = host;
|
|
8
|
+
const ui = program.command("ui").description("inspect the Task UI components the launcher renders");
|
|
9
|
+
ui.command("components")
|
|
10
|
+
.argument("[names...]", "component names such as Field.RadioGroup; omit to list every component")
|
|
11
|
+
.option("--json", "print the component contracts as JSON", false)
|
|
12
|
+
.description("list Task UI components, or print the props, value shape, and example of the named ones")
|
|
13
|
+
.action(async (names: string[], options: { json: boolean }) => {
|
|
14
|
+
const sourceRoot = await resolveSourceRoot(globals(), context.io);
|
|
15
|
+
await uiComponentsCommand({
|
|
16
|
+
sourceRoot,
|
|
17
|
+
names,
|
|
18
|
+
json: options.json,
|
|
19
|
+
io: context.io,
|
|
20
|
+
describe: context.taskUiComponents ?? describeTaskUiComponents,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
};
|
package/src/cli/config.ts
CHANGED
|
@@ -12,12 +12,14 @@ export type TolokaCliFlags = {
|
|
|
12
12
|
|
|
13
13
|
export type TolokaEnvironment = Readonly<Record<string, string | undefined>> & {
|
|
14
14
|
readonly TOLOKA_API_KEY?: string;
|
|
15
|
+
readonly TOLOKA_SCOPED_TOKEN?: string;
|
|
15
16
|
readonly TOLOKA_BASE_URL?: string;
|
|
16
17
|
readonly TOLOKA_PROJECT_ID?: string;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export type CliCredential =
|
|
20
21
|
| { readonly type: "apiKey"; readonly apiKey: string }
|
|
22
|
+
| { readonly type: "scopedToken"; readonly scopedToken: string }
|
|
21
23
|
| { readonly type: "browser"; readonly provider: BrowserLoginProvider };
|
|
22
24
|
|
|
23
25
|
export type ResolvedTolokaConfig = {
|
|
@@ -60,15 +62,22 @@ export const MISSING_CREDENTIALS_MESSAGE = [
|
|
|
60
62
|
"",
|
|
61
63
|
" toloka login",
|
|
62
64
|
"",
|
|
63
|
-
"Automation can pass --api-key, set TOLOKA_API_KEY, or pipe a key to toloka login --api-key-stdin.",
|
|
65
|
+
"Automation can pass --api-key, set TOLOKA_SCOPED_TOKEN or TOLOKA_API_KEY, or pipe a key to toloka login --api-key-stdin.",
|
|
64
66
|
"Run toloka auth-status to see what is in effect.",
|
|
65
67
|
].join("\n");
|
|
66
68
|
|
|
67
|
-
export const
|
|
69
|
+
export const resolveConfiguredCredential = (
|
|
68
70
|
flags: TolokaCliFlags,
|
|
69
71
|
environment: TolokaEnvironment,
|
|
70
72
|
stored?: StoredCredentials,
|
|
71
|
-
):
|
|
73
|
+
): Exclude<CliCredential, { readonly type: "browser" }> | undefined => {
|
|
74
|
+
if (flags.apiKey !== undefined) return { type: "apiKey", apiKey: flags.apiKey };
|
|
75
|
+
if (environment.TOLOKA_SCOPED_TOKEN !== undefined) {
|
|
76
|
+
return { type: "scopedToken", scopedToken: environment.TOLOKA_SCOPED_TOKEN };
|
|
77
|
+
}
|
|
78
|
+
const apiKey = environment.TOLOKA_API_KEY ?? stored?.apiKey;
|
|
79
|
+
return apiKey === undefined ? undefined : { type: "apiKey", apiKey };
|
|
80
|
+
};
|
|
72
81
|
|
|
73
82
|
export const resolveBaseUrl = (
|
|
74
83
|
flags: TolokaCliFlags,
|
|
@@ -88,7 +97,7 @@ export function resolveTolokaConfig(
|
|
|
88
97
|
stored?: StoredCredentials,
|
|
89
98
|
browserLogin?: BrowserLoginProvider,
|
|
90
99
|
): ResolvedTolokaConfig {
|
|
91
|
-
const
|
|
100
|
+
const configuredCredential = resolveConfiguredCredential(flags, environment, stored);
|
|
92
101
|
const baseUrl = resolveBaseUrl(flags, environment, stored);
|
|
93
102
|
const project = resolveProjectContext({
|
|
94
103
|
flags,
|
|
@@ -98,11 +107,8 @@ export function resolveTolokaConfig(
|
|
|
98
107
|
effectiveBaseUrl: baseUrl,
|
|
99
108
|
});
|
|
100
109
|
const credential: CliCredential | undefined =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
: browserLogin !== undefined
|
|
104
|
-
? { type: "browser", provider: browserLogin }
|
|
105
|
-
: undefined;
|
|
110
|
+
configuredCredential ??
|
|
111
|
+
(browserLogin === undefined ? undefined : { type: "browser", provider: browserLogin });
|
|
106
112
|
assertCli(credential, MISSING_CREDENTIALS_MESSAGE, { hint: { disabled: true } });
|
|
107
113
|
return { baseUrl, credential, ...(project === undefined ? {} : { projectId: project.id }) };
|
|
108
114
|
}
|
package/src/cli/context.ts
CHANGED
|
@@ -10,8 +10,14 @@ import type { CliAnalytics } from "./analytics";
|
|
|
10
10
|
import type { BrowserLoginFlow } from "./browser-login";
|
|
11
11
|
import { createBrowserSessionAuth, findBrowserLogin } from "./browser-session";
|
|
12
12
|
import type { checkProject } from "./check";
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
13
|
+
import type { DescribeTaskUiComponents } from "../testing/task-ui-browser";
|
|
14
|
+
import {
|
|
15
|
+
PRODUCTION_BASE_URL,
|
|
16
|
+
resolveBaseUrl,
|
|
17
|
+
resolveConfiguredCredential,
|
|
18
|
+
resolveTolokaConfig,
|
|
19
|
+
} from "./config";
|
|
20
|
+
import type { CliCredential, ResolvedTolokaConfig, TolokaEnvironment } from "./config";
|
|
15
21
|
import { findCommandCredentials } from "./credentials";
|
|
16
22
|
import type { StoredCredentials } from "./credentials";
|
|
17
23
|
import { resolveProjectContext } from "./project-context";
|
|
@@ -36,6 +42,7 @@ export type ProgramContext = {
|
|
|
36
42
|
readonly browserLogin?: BrowserLoginFlow;
|
|
37
43
|
readonly runChecks?: typeof checkProject;
|
|
38
44
|
readonly analytics?: CliAnalytics;
|
|
45
|
+
readonly taskUiComponents?: DescribeTaskUiComponents;
|
|
39
46
|
};
|
|
40
47
|
|
|
41
48
|
export type GlobalOptions = {
|
|
@@ -54,6 +61,7 @@ export type Resolved = {
|
|
|
54
61
|
readonly sourceRoot: string;
|
|
55
62
|
readonly client: PotatoClient;
|
|
56
63
|
readonly baseUrl: string;
|
|
64
|
+
readonly credential: CliCredential;
|
|
57
65
|
readonly state: TolokaState | undefined;
|
|
58
66
|
readonly stored: StoredCredentials | undefined;
|
|
59
67
|
readonly projectId?: string;
|
|
@@ -61,10 +69,11 @@ export type Resolved = {
|
|
|
61
69
|
|
|
62
70
|
export const CREDENTIAL_PRECEDENCE = [
|
|
63
71
|
"Credentials resolve in this order, highest first:",
|
|
64
|
-
" 1. --api-key
|
|
65
|
-
" 2.
|
|
66
|
-
" 3.
|
|
67
|
-
" 4. the
|
|
72
|
+
" 1. --api-key",
|
|
73
|
+
" 2. TOLOKA_SCOPED_TOKEN",
|
|
74
|
+
" 3. TOLOKA_API_KEY",
|
|
75
|
+
" 4. the API key stored by toloka login --api-key-stdin",
|
|
76
|
+
" 5. the browser login stored by toloka login",
|
|
68
77
|
`The platform defaults to ${PRODUCTION_BASE_URL}.`,
|
|
69
78
|
].join("\n");
|
|
70
79
|
|
|
@@ -205,7 +214,9 @@ export const resolveInitRoot = (globals: GlobalOptions): string =>
|
|
|
205
214
|
export const readCommandCredentials = async (globals: GlobalOptions, context: ProgramContext) => {
|
|
206
215
|
const { found, warning } = await findCommandCredentials(
|
|
207
216
|
context.environment,
|
|
208
|
-
globals.apiKey ??
|
|
217
|
+
globals.apiKey ??
|
|
218
|
+
context.environment.TOLOKA_SCOPED_TOKEN ??
|
|
219
|
+
context.environment.TOLOKA_API_KEY,
|
|
209
220
|
);
|
|
210
221
|
if (warning !== undefined) context.io.stderr(warning);
|
|
211
222
|
return found?.credentials;
|
|
@@ -229,7 +240,7 @@ export const resolveClient = async (
|
|
|
229
240
|
): Promise<{ readonly config: ResolvedTolokaConfig; readonly client: PotatoClient }> => {
|
|
230
241
|
const { environment } = context;
|
|
231
242
|
const browserLogin =
|
|
232
|
-
|
|
243
|
+
resolveConfiguredCredential(globals, environment, stored) === undefined
|
|
233
244
|
? (await findBrowserLogin(resolveBaseUrl(globals, environment, stored), environment))?.provider
|
|
234
245
|
: undefined;
|
|
235
246
|
const config = resolveTolokaConfig(globals, state, environment, stored, browserLogin);
|
|
@@ -239,10 +250,16 @@ export const resolveClient = async (
|
|
|
239
250
|
createClient(
|
|
240
251
|
credential.type === "apiKey"
|
|
241
252
|
? { baseUrl, apiKey: credential.apiKey }
|
|
242
|
-
:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
253
|
+
: credential.type === "scopedToken"
|
|
254
|
+
? { baseUrl, scopedToken: credential.scopedToken }
|
|
255
|
+
: {
|
|
256
|
+
baseUrl,
|
|
257
|
+
accessToken: createBrowserSessionAuth({
|
|
258
|
+
provider: credential.provider,
|
|
259
|
+
baseUrl,
|
|
260
|
+
environment,
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
246
263
|
);
|
|
247
264
|
return { config, client };
|
|
248
265
|
};
|
|
@@ -261,6 +278,7 @@ export const resolveContext = async (
|
|
|
261
278
|
sourceRoot,
|
|
262
279
|
client,
|
|
263
280
|
baseUrl: config.baseUrl,
|
|
281
|
+
credential: config.credential,
|
|
264
282
|
state,
|
|
265
283
|
stored,
|
|
266
284
|
...(config.projectId === undefined ? {} : { projectId: config.projectId }),
|
package/src/cli/credentials.ts
CHANGED
|
@@ -192,12 +192,12 @@ export async function findStoredCredentials(
|
|
|
192
192
|
|
|
193
193
|
export async function findCommandCredentials(
|
|
194
194
|
environment: CliEnvironment,
|
|
195
|
-
|
|
195
|
+
overrideCredential: string | undefined,
|
|
196
196
|
): Promise<CommandCredentialLookup> {
|
|
197
197
|
try {
|
|
198
198
|
return { found: await findStoredCredentials(environment) };
|
|
199
199
|
} catch (error) {
|
|
200
|
-
if (!(error instanceof CredentialsError) ||
|
|
200
|
+
if (!(error instanceof CredentialsError) || overrideCredential === undefined) throw error;
|
|
201
201
|
return { found: undefined, warning: error.message };
|
|
202
202
|
}
|
|
203
203
|
}
|
package/src/cli/environment.ts
CHANGED
|
@@ -6,7 +6,11 @@ const LEGACY_ENVIRONMENT_NAMES: Readonly<Record<string, string>> = {
|
|
|
6
6
|
POTATO_PROJECT_ID: "TOLOKA_PROJECT_ID",
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export const
|
|
9
|
+
export const CREDENTIAL_ENVIRONMENT_NAMES = [
|
|
10
|
+
"TOLOKA_SCOPED_TOKEN",
|
|
11
|
+
"TOLOKA_API_KEY",
|
|
12
|
+
"POTATO_API_KEY",
|
|
13
|
+
];
|
|
10
14
|
|
|
11
15
|
export type NormalizedEnvironment = {
|
|
12
16
|
readonly environment: TolokaEnvironment;
|