@pipefy/pipefy-process-coder 0.1.0 → 0.1.2
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/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -262
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -247
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -589
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -1391
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -623
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -636
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -3642
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -863
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -799
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -42
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -581
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -503
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -562
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -557
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -609
- package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -146
- package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -127
- package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -91
- package/AGENTS.md +476 -456
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +830 -808
- package/bin/pipe.js +25 -4
- package/dist/apply/adopt.js +128 -0
- package/dist/apply/agentops.js +42 -0
- package/dist/apply/compile.js +722 -0
- package/dist/apply/execute.js +277 -0
- package/dist/apply/flowops.js +293 -0
- package/dist/apply/idmap.js +233 -0
- package/dist/apply/mutations.js +704 -0
- package/dist/apply/registry.js +348 -0
- package/dist/apply/types.js +18 -0
- package/dist/cli/args.js +79 -0
- package/dist/cli.js +196 -0
- package/dist/codec/flow.js +164 -0
- package/dist/codec/pack.js +88 -0
- package/dist/codec/roundtrip.js +86 -0
- package/dist/codec/unpack.js +181 -0
- package/dist/commands/agents.js +145 -0
- package/dist/commands/apply.js +982 -0
- package/dist/commands/context.js +79 -0
- package/dist/commands/create.js +63 -0
- package/dist/commands/diff.js +294 -0
- package/dist/commands/flows.js +348 -0
- package/dist/commands/misc.js +562 -0
- package/dist/commands/plan.js +301 -0
- package/dist/commands/pull.js +549 -0
- package/dist/commands/runs.js +74 -0
- package/dist/commands/skills.js +131 -0
- package/dist/commands/verify.js +205 -0
- package/dist/config.js +125 -0
- package/dist/diff/agents.js +80 -0
- package/dist/diff/diff.js +983 -0
- package/dist/diff/flow.js +258 -0
- package/{src/diff/html.ts → dist/diff/html.js} +274 -322
- package/dist/diff/render.js +90 -0
- package/dist/model/payload.js +84 -0
- package/dist/model/tree.js +7 -0
- package/dist/model/volatile.js +54 -0
- package/dist/pipefy/agents.js +131 -0
- package/dist/pipefy/automations.js +155 -0
- package/dist/pipefy/capability.js +84 -0
- package/dist/pipefy/client.js +204 -0
- package/dist/pipefy/discovery.js +148 -0
- package/dist/pipefy/internal.js +406 -0
- package/dist/pipefy/ipaas.js +269 -0
- package/dist/pipefy/reconstruct.js +676 -0
- package/dist/pipefy/reference.js +172 -0
- package/dist/pipefy/snapshot.js +185 -0
- package/dist/pipefy/toolkit.js +174 -0
- package/{src → dist}/pipefy/toolkit_bearer.py +60 -14
- package/dist/report/integrations.js +165 -0
- package/dist/report/run.js +356 -0
- package/dist/util/fsx.js +50 -0
- package/dist/util/git.js +25 -0
- package/dist/util/json.js +56 -0
- package/dist/util/log.js +69 -0
- package/dist/util/pool.js +42 -0
- package/dist/util/slug.js +23 -0
- package/dist/util/tui.js +287 -0
- package/dist/validate/index.js +86 -0
- package/dist/validate/integrity.js +370 -0
- package/dist/validate/reference.js +120 -0
- package/dist/validate/schema.js +278 -0
- package/dist/version.js +18 -0
- package/dist/workspace/agents.js +245 -0
- package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
- package/dist/workspace/flows.js +149 -0
- package/dist/workspace/layout.js +146 -0
- package/dist/workspace/lock.js +83 -0
- package/dist/workspace/read.js +168 -0
- package/dist/workspace/reference.js +24 -0
- package/dist/workspace/stamp.js +234 -0
- package/dist/workspace/write.js +194 -0
- package/package.json +38 -35
- package/src/apply/adopt.ts +0 -150
- package/src/apply/agentops.ts +0 -71
- package/src/apply/compile.ts +0 -875
- package/src/apply/execute.ts +0 -336
- package/src/apply/flowops.ts +0 -399
- package/src/apply/idmap.ts +0 -241
- package/src/apply/mutations.ts +0 -955
- package/src/apply/registry.ts +0 -430
- package/src/apply/types.ts +0 -134
- package/src/cli/args.ts +0 -88
- package/src/cli.ts +0 -211
- package/src/codec/flow.ts +0 -199
- package/src/codec/pack.ts +0 -103
- package/src/codec/roundtrip.ts +0 -94
- package/src/codec/unpack.ts +0 -198
- package/src/commands/agents.ts +0 -184
- package/src/commands/apply.ts +0 -1144
- package/src/commands/context.ts +0 -119
- package/src/commands/create.ts +0 -79
- package/src/commands/diff.ts +0 -314
- package/src/commands/flows.ts +0 -414
- package/src/commands/misc.ts +0 -644
- package/src/commands/plan.ts +0 -331
- package/src/commands/pull.ts +0 -567
- package/src/commands/runs.ts +0 -83
- package/src/commands/skills.ts +0 -137
- package/src/commands/verify.ts +0 -253
- package/src/config.ts +0 -168
- package/src/diff/agents.ts +0 -122
- package/src/diff/diff.ts +0 -1130
- package/src/diff/flow.ts +0 -318
- package/src/diff/render.ts +0 -101
- package/src/model/payload.ts +0 -154
- package/src/model/tree.ts +0 -99
- package/src/model/volatile.ts +0 -55
- package/src/pipefy/agents.ts +0 -165
- package/src/pipefy/automations.ts +0 -219
- package/src/pipefy/capability.ts +0 -119
- package/src/pipefy/client.ts +0 -267
- package/src/pipefy/discovery.ts +0 -209
- package/src/pipefy/internal.ts +0 -380
- package/src/pipefy/ipaas.ts +0 -365
- package/src/pipefy/reconstruct.ts +0 -775
- package/src/pipefy/reference.ts +0 -251
- package/src/pipefy/snapshot.ts +0 -245
- package/src/pipefy/toolkit.ts +0 -200
- package/src/report/integrations.ts +0 -231
- package/src/report/run.ts +0 -475
- package/src/util/fsx.ts +0 -45
- package/src/util/git.ts +0 -32
- package/src/util/json.ts +0 -55
- package/src/util/log.ts +0 -76
- package/src/util/pool.ts +0 -48
- package/src/util/slug.ts +0 -26
- package/src/util/tui.ts +0 -335
- package/src/validate/index.ts +0 -123
- package/src/validate/integrity.ts +0 -387
- package/src/validate/reference.ts +0 -136
- package/src/validate/schema.ts +0 -328
- package/src/workspace/agents.ts +0 -290
- package/src/workspace/flows.ts +0 -191
- package/src/workspace/layout.ts +0 -165
- package/src/workspace/lock.ts +0 -148
- package/src/workspace/read.ts +0 -165
- package/src/workspace/reference.ts +0 -24
- package/src/workspace/stamp.ts +0 -301
- package/src/workspace/write.ts +0 -225
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { debug } from "../util/log.js";
|
|
2
|
+
/**
|
|
3
|
+
* The closed sets — every place an authored value must come from a fixed list.
|
|
4
|
+
*
|
|
5
|
+
* This exists because of a structural gap, not a convenience. The schema
|
|
6
|
+
* publishes the lists:
|
|
7
|
+
*
|
|
8
|
+
* AutomationsEvents 10 values card_moved, field_updated, …
|
|
9
|
+
* AutomationsActions 15 values move_single_card, update_card_field, …
|
|
10
|
+
* FieldTypeId 24 values short_text, connector, …
|
|
11
|
+
*
|
|
12
|
+
* and then types the input fields that consume them as `ID` and `String`:
|
|
13
|
+
*
|
|
14
|
+
* CreateAutomationInput.event_id ID
|
|
15
|
+
* CreateAutomationInput.action_id ID
|
|
16
|
+
* CreatePhaseFieldInput.type_id String
|
|
17
|
+
*
|
|
18
|
+
* So GraphQL validates none of them. A wrong value passes query validation,
|
|
19
|
+
* reaches a runtime check, and comes back as the two words `is invalid` — no
|
|
20
|
+
* parameter name, no field name, no list of what would have been accepted. The
|
|
21
|
+
* list was always public; nothing was consulting it.
|
|
22
|
+
*
|
|
23
|
+
* Two consumers, and the split matters:
|
|
24
|
+
*
|
|
25
|
+
* - `pipe validate`, `plan` and `apply` read the cached copy and refuse
|
|
26
|
+
* offline, naming the value and what belongs there.
|
|
27
|
+
* - `REFERENCE.md` puts the same lists in the workspace, so whoever is editing
|
|
28
|
+
* the JSON — a person or an agent — reads what the API accepts instead of
|
|
29
|
+
* guessing and waiting for `is invalid`.
|
|
30
|
+
*
|
|
31
|
+
* Read once per pull, cached in `.ppc/reference.json`. Never assumed: an
|
|
32
|
+
* absent reference means the checks are skipped, not that a value is wrong.
|
|
33
|
+
*/
|
|
34
|
+
/** Enums whose values get authored into workspace JSON by hand. */
|
|
35
|
+
const TRACKED_ENUMS = [
|
|
36
|
+
'FieldTypeId',
|
|
37
|
+
'AutomationsEvents',
|
|
38
|
+
'AutomationsActions',
|
|
39
|
+
'Colors',
|
|
40
|
+
'FieldConditionFilter',
|
|
41
|
+
'AutomationHttpAuthentication',
|
|
42
|
+
'HttpRequestMethodTypeGQLEnum',
|
|
43
|
+
'HttpRequestAddAuthenticationTo',
|
|
44
|
+
'UpdateFieldValuesOperators',
|
|
45
|
+
'DistributeAssignmentAutomationStrategyGQLEnum',
|
|
46
|
+
'CardTypeEnum',
|
|
47
|
+
];
|
|
48
|
+
const ENUMS_QUERY = `query ClosedSets {
|
|
49
|
+
__schema { types { name kind enumValues { name } } }
|
|
50
|
+
}`;
|
|
51
|
+
const CATALOGUE_QUERY = `query AutomationCatalogue($repoId: ID!) {
|
|
52
|
+
automationEvents { id acceptedParameters actionsBlacklist }
|
|
53
|
+
automationActions(repoId: $repoId) { id acceptedParameters eventsBlacklist }
|
|
54
|
+
}`;
|
|
55
|
+
/**
|
|
56
|
+
* Read every closed set for one pipe.
|
|
57
|
+
*
|
|
58
|
+
* Returns null rather than throwing: a token that cannot introspect, or a pipe
|
|
59
|
+
* whose action catalogue is refused, must degrade to "no reference" — which
|
|
60
|
+
* skips the checks — and never to a guess that would report valid work as broken.
|
|
61
|
+
*/
|
|
62
|
+
export const readReference = async (client, repoId) => {
|
|
63
|
+
try {
|
|
64
|
+
const [schema, cat] = await Promise.all([
|
|
65
|
+
client.raw(ENUMS_QUERY),
|
|
66
|
+
client.raw(CATALOGUE_QUERY, { repoId: String(repoId) }),
|
|
67
|
+
]);
|
|
68
|
+
const byName = new Map((schema?.__schema?.types ?? [])
|
|
69
|
+
.filter((t) => t.kind === 'ENUM')
|
|
70
|
+
.map((t) => [t.name, (t.enumValues ?? []).map((v) => v.name)]));
|
|
71
|
+
const enums = {};
|
|
72
|
+
for (const name of TRACKED_ENUMS) {
|
|
73
|
+
const values = byName.get(name);
|
|
74
|
+
/**
|
|
75
|
+
* An enum the endpoint no longer publishes is omitted, not recorded empty.
|
|
76
|
+
* An empty list would read as "nothing is valid here" and refuse every
|
|
77
|
+
* value in that position.
|
|
78
|
+
*/
|
|
79
|
+
if (values?.length)
|
|
80
|
+
enums[name] = values;
|
|
81
|
+
}
|
|
82
|
+
const ref = {
|
|
83
|
+
readAt: new Date().toISOString(),
|
|
84
|
+
repoId: String(repoId),
|
|
85
|
+
enums,
|
|
86
|
+
events: (cat?.automationEvents ?? []).map((e) => ({
|
|
87
|
+
id: e.id,
|
|
88
|
+
acceptedParameters: e.acceptedParameters ?? [],
|
|
89
|
+
actionsBlacklist: e.actionsBlacklist ?? [],
|
|
90
|
+
})),
|
|
91
|
+
actions: (cat?.automationActions ?? []).map((a) => ({
|
|
92
|
+
id: a.id,
|
|
93
|
+
acceptedParameters: a.acceptedParameters ?? [],
|
|
94
|
+
eventsBlacklist: a.eventsBlacklist ?? [],
|
|
95
|
+
})),
|
|
96
|
+
};
|
|
97
|
+
debug(`reference: ${Object.keys(enums).length} enums, ${ref.events.length} events, ` +
|
|
98
|
+
`${ref.actions.length} actions for repo ${repoId}`);
|
|
99
|
+
return ref;
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
debug(`reference unavailable: ${e.message}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
/** The cached reference, in the shape the automation param check already takes. */
|
|
107
|
+
export const toCatalogue = (ref) => ({
|
|
108
|
+
events: new Map(ref.events.map((e) => [e.id, e])),
|
|
109
|
+
actions: new Map(ref.actions.map((a) => [a.id, a])),
|
|
110
|
+
});
|
|
111
|
+
export const enumValues = (ref, name) => {
|
|
112
|
+
const v = ref?.enums[name];
|
|
113
|
+
return v?.length ? v : null;
|
|
114
|
+
};
|
|
115
|
+
const table = (rows, head) => {
|
|
116
|
+
const w = Math.max(head[0].length, ...rows.map(([a]) => a.length));
|
|
117
|
+
return [
|
|
118
|
+
`| ${head[0].padEnd(w)} | ${head[1]} |`,
|
|
119
|
+
`| ${'-'.repeat(w)} | ${'-'.repeat(Math.max(3, head[1].length))} |`,
|
|
120
|
+
...rows.map(([a, b]) => `| ${a.padEnd(w)} | ${b} |`),
|
|
121
|
+
];
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* REFERENCE.md — the closed sets, on disk, next to the JSON being edited.
|
|
125
|
+
*
|
|
126
|
+
* Deliberately generated rather than written by hand. A hand-written list of
|
|
127
|
+
* what an API accepts is wrong the first time the API changes, and nothing tells
|
|
128
|
+
* you it went wrong; this one is re-read on every pull and stamped with when.
|
|
129
|
+
*/
|
|
130
|
+
export const renderReferenceMd = (ref) => {
|
|
131
|
+
const lines = ['# What the API accepts', ''];
|
|
132
|
+
if (!ref) {
|
|
133
|
+
lines.push('The closed sets could not be read on the last pull, so this file is empty and', '`pipe validate` skips the checks that depend on it. That is not a claim that', 'any value is valid — it is the absence of a claim. Re-run `pipe pull` to fill it.', '');
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
136
|
+
lines.push(`Read from the API on ${ref.readAt} for pipe ${ref.repoId}. Regenerated by every`, '`pipe pull` — edit the workspace JSON, never this file.', '', 'These are the values the endpoint accepts in each position. They matter because', 'the schema publishes the lists but types the inputs that consume them as `ID`', 'and `String`, so a wrong value is not caught by GraphQL: it reaches a runtime', 'check and comes back as `is invalid`, naming nothing. `pipe validate` checks', 'these offline so a bad value is refused with its name before anything is sent.', '', '## Automation events', '', 'Each event accepts **only** the parameters listed. A parameter that belongs to a', 'different event is still just `is invalid`.', '');
|
|
137
|
+
for (const e of [...ref.events].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
138
|
+
lines.push(`### \`${e.id}\``);
|
|
139
|
+
lines.push('');
|
|
140
|
+
lines.push(e.acceptedParameters.length
|
|
141
|
+
? `\`event_params\`: ${e.acceptedParameters.map((p) => `\`${p}\``).join(', ')}`
|
|
142
|
+
: '`event_params`: none — this event takes no parameters.');
|
|
143
|
+
if (e.actionsBlacklist.length) {
|
|
144
|
+
lines.push('');
|
|
145
|
+
lines.push(`Cannot be paired with: ${e.actionsBlacklist.map((a) => `\`${a}\``).join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
lines.push('');
|
|
148
|
+
}
|
|
149
|
+
lines.push('## Automation actions', '', 'Actions are per-pipe: a pipe without AI enabled offers fewer than this list if', 'it was pulled from a different pipe. `field_map` is accepted on every action', 'that writes a field and is not listed below — the catalogue names', '`fields_map_order` beside it and stops.', '');
|
|
150
|
+
for (const a of [...ref.actions].sort((x, y) => x.id.localeCompare(y.id))) {
|
|
151
|
+
lines.push(`### \`${a.id}\``);
|
|
152
|
+
lines.push('');
|
|
153
|
+
lines.push(a.acceptedParameters.length
|
|
154
|
+
? `\`action_params\`: ${a.acceptedParameters.map((p) => `\`${p}\``).join(', ')}`
|
|
155
|
+
: '`action_params`: none — this action takes no parameters.');
|
|
156
|
+
if (a.eventsBlacklist.length) {
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push(`Cannot be triggered by: ${a.eventsBlacklist.map((e) => `\`${e}\``).join(', ')}`);
|
|
159
|
+
}
|
|
160
|
+
lines.push('');
|
|
161
|
+
}
|
|
162
|
+
const enumRows = Object.entries(ref.enums)
|
|
163
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
164
|
+
.map(([name, values]) => [`\`${name}\``, values.map((v) => `\`${v}\``).join(', ')]);
|
|
165
|
+
if (enumRows.length) {
|
|
166
|
+
lines.push('## Other closed sets', '');
|
|
167
|
+
lines.push('Where these are used: `FieldTypeId` is a field\'s `type_id`; `Colors` is a', 'phase or pipe `color`; `FieldConditionFilter` is a field condition\'s', '`filter`; the `Http*` sets belong to `send_http_request` action params.', '');
|
|
168
|
+
lines.push(...table(enumRows, ['Set', 'Values']));
|
|
169
|
+
lines.push('');
|
|
170
|
+
}
|
|
171
|
+
return lines.join('\n');
|
|
172
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { gunzipSync } from 'node:zlib';
|
|
2
|
+
import { LIMITS } from "../config.js";
|
|
3
|
+
import { debug, progress, progressDone, step } from "../util/log.js";
|
|
4
|
+
import { retry, sleep } from "../util/pool.js";
|
|
5
|
+
/**
|
|
6
|
+
* `RepoSnapshotStatus` is `PENDING | UPLOADED | FAILED` — verified by
|
|
7
|
+
* introspection against api.pipefy.com. **UPLOADED is the success state**: the
|
|
8
|
+
* job has written the payload to S3 and `signedUrl` is available. The wider set
|
|
9
|
+
* is kept because this is an undocumented enum with no `schema_version`
|
|
10
|
+
* (ROUTE-B-V2.md ask 9), so a deployment that renames it degrades to a slow
|
|
11
|
+
* poll rather than a wrong answer.
|
|
12
|
+
*/
|
|
13
|
+
const DONE = new Set(['uploaded', 'done', 'succeeded', 'success', 'completed', 'finished', 'ready']);
|
|
14
|
+
const FAILED = new Set(['failed', 'error', 'errored', 'cancelled', 'canceled']);
|
|
15
|
+
export const isDone = (s) => DONE.has(String(s ?? '').toLowerCase());
|
|
16
|
+
export const isFailed = (s) => FAILED.has(String(s ?? '').toLowerCase());
|
|
17
|
+
export class SnapshotClient {
|
|
18
|
+
client;
|
|
19
|
+
/** Whether this endpoint has snapshots at all — decided once, cached. */
|
|
20
|
+
available = null;
|
|
21
|
+
constructor(client) {
|
|
22
|
+
this.client = client;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Where poll progress goes. The snapshot wait is the longest thing this tool
|
|
26
|
+
* does (~63s), so whoever owns the screen renders it, rather than this client
|
|
27
|
+
* scribbling its own line into someone else's frame.
|
|
28
|
+
*/
|
|
29
|
+
onProgress = null;
|
|
30
|
+
report(text) {
|
|
31
|
+
if (this.onProgress)
|
|
32
|
+
this.onProgress(text);
|
|
33
|
+
else
|
|
34
|
+
progress(text);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Snapshots are an entitlement, not a given. The whole point of the
|
|
38
|
+
* reconstruction path is that this can be false — so it is answered by
|
|
39
|
+
* introspection, up front, and never by a failed pull halfway through.
|
|
40
|
+
*/
|
|
41
|
+
async isAvailable() {
|
|
42
|
+
if (this.available !== null)
|
|
43
|
+
return this.available;
|
|
44
|
+
const [mut, pipeFields] = await Promise.all([
|
|
45
|
+
this.client.hasMutation('createRepoSnapshot'),
|
|
46
|
+
this.client.typeFields('Pipe'),
|
|
47
|
+
]);
|
|
48
|
+
this.available = mut && Boolean(pipeFields?.has('snapshot') || pipeFields?.has('snapshots'));
|
|
49
|
+
debug(`snapshot capability: createRepoSnapshot=${mut} pipe.snapshots=${pipeFields?.has('snapshots')}`);
|
|
50
|
+
return this.available;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `Pipe.snapshots` is a Relay `RepoSnapshotConnection`, not the flat array
|
|
54
|
+
* PLAN.md §1.1 recorded — so the rows come from `nodes`.
|
|
55
|
+
*
|
|
56
|
+
* `snapshots(last: N)` is not the most recent N — measured live, on a pipe
|
|
57
|
+
* with 86 snapshots: `last: 1` returned `sequenceIndex: 1`, the *oldest* one
|
|
58
|
+
* on record, while `first: 1` correctly returned `sequenceIndex: 86`. The
|
|
59
|
+
* connection is apparently ordered newest-first internally, the opposite of
|
|
60
|
+
* the usual Relay convention, which inverts what `first`/`last` each mean.
|
|
61
|
+
*
|
|
62
|
+
* This is not cosmetic: `pipe rollback`'s only-the-latest check, and
|
|
63
|
+
* `pipe apply`'s post-failure rollback offer, both call this to decide
|
|
64
|
+
* whether a version is still restorable. With `last`, that check compared
|
|
65
|
+
* against the *oldest* snapshot and refused every legitimate restore.
|
|
66
|
+
*/
|
|
67
|
+
async list(repoId, limit = 20) {
|
|
68
|
+
const data = await this.client.tryRaw(`query($id: ID!, $first: Int!) {
|
|
69
|
+
pipe(id: $id) {
|
|
70
|
+
snapshots(first: $first) {
|
|
71
|
+
totalCount
|
|
72
|
+
nodes { versionId status sequenceIndex label createdAt }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}`, { id: String(repoId), first: limit }, 'list snapshots');
|
|
76
|
+
const rows = data?.pipe?.snapshots?.nodes ?? [];
|
|
77
|
+
return [...rows].sort((a, b) => (b.sequenceIndex ?? 0) - (a.sequenceIndex ?? 0));
|
|
78
|
+
}
|
|
79
|
+
/** `snapshot(versionId:)` takes an `ID`; declaring `String!` fails the query. */
|
|
80
|
+
async detail(repoId, versionId) {
|
|
81
|
+
const data = await this.client.tryRaw(`query($id: ID!, $v: ID!) {
|
|
82
|
+
pipe(id: $id) {
|
|
83
|
+
snapshot(versionId: $v) { versionId status signedUrl metrics uploadedAt error }
|
|
84
|
+
}
|
|
85
|
+
}`, { id: String(repoId), v: versionId }, 'snapshot detail');
|
|
86
|
+
return data?.pipe?.snapshot ?? null;
|
|
87
|
+
}
|
|
88
|
+
async create(repoId) {
|
|
89
|
+
const data = await this.client.raw(`mutation($input: CreateRepoSnapshotInput!) {
|
|
90
|
+
createRepoSnapshot(input: $input) { repoSnapshot { versionId status } }
|
|
91
|
+
}`, { input: { repoId: Number(repoId) } }, 'createRepoSnapshot');
|
|
92
|
+
return data.createRepoSnapshot.repoSnapshot.versionId;
|
|
93
|
+
}
|
|
94
|
+
async rename(repoId, versionId, label) {
|
|
95
|
+
const res = await this.client.tryRaw(`mutation($input: RenameRepoSnapshotInput!) {
|
|
96
|
+
renameRepoSnapshot(input: $input) { repoSnapshot { versionId label } }
|
|
97
|
+
}`, { input: { repoId: Number(repoId), versionId, label } }, 'renameRepoSnapshot');
|
|
98
|
+
return res !== null;
|
|
99
|
+
}
|
|
100
|
+
/** Poll to a terminal state. ~63s is normal, so the wait is visible and bounded. */
|
|
101
|
+
async waitFor(repoId, versionId) {
|
|
102
|
+
const started = Date.now();
|
|
103
|
+
let unreadable = 0;
|
|
104
|
+
for (;;) {
|
|
105
|
+
const d = await this.detail(repoId, versionId);
|
|
106
|
+
const elapsed = Math.round((Date.now() - started) / 1000);
|
|
107
|
+
if (d && isDone(d.status)) {
|
|
108
|
+
if (!this.onProgress)
|
|
109
|
+
progressDone();
|
|
110
|
+
return d;
|
|
111
|
+
}
|
|
112
|
+
if (d && isFailed(d.status)) {
|
|
113
|
+
if (!this.onProgress)
|
|
114
|
+
progressDone();
|
|
115
|
+
throw new Error(`snapshot ${versionId} ${d.status}${d.error ? `: ${d.error}` : ''}`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A null detail means the *query* failed, not that the snapshot is
|
|
119
|
+
* pending. Polling a broken query for five minutes and then reporting
|
|
120
|
+
* "still unknown" hides the real cause — which is how a schema drift in
|
|
121
|
+
* this query cost a wasted snapshot and a 303s wait. Fail fast and say so.
|
|
122
|
+
*/
|
|
123
|
+
if (!d) {
|
|
124
|
+
unreadable++;
|
|
125
|
+
if (unreadable >= 3) {
|
|
126
|
+
if (!this.onProgress)
|
|
127
|
+
progressDone();
|
|
128
|
+
throw new Error(`snapshot ${versionId} status is unreadable — pipe.snapshot(versionId:) did not answer 3 times. ` +
|
|
129
|
+
`Run with --verbose to see the GraphQL error; the snapshot itself may well have been created.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
unreadable = 0;
|
|
134
|
+
}
|
|
135
|
+
if (Date.now() - started > LIMITS.snapshotTimeoutMs) {
|
|
136
|
+
if (!this.onProgress)
|
|
137
|
+
progressDone();
|
|
138
|
+
throw new Error(`snapshot ${versionId} still ${d?.status ?? 'unreadable'} after ${elapsed}s`);
|
|
139
|
+
}
|
|
140
|
+
this.report(`${String(d?.status ?? 'unreadable').toLowerCase()} ${elapsed}s (~63s typical)`);
|
|
141
|
+
await sleep(LIMITS.snapshotPollMs);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Download the payload. The signed URL is re-queried immediately before the
|
|
146
|
+
* fetch and never stored, and the raw gzip bytes are returned alongside the
|
|
147
|
+
* parsed payload so the caller can cache the compressed form.
|
|
148
|
+
*/
|
|
149
|
+
async download(repoId, versionId) {
|
|
150
|
+
const d = await this.detail(repoId, versionId);
|
|
151
|
+
if (!d?.signedUrl)
|
|
152
|
+
throw new Error(`snapshot ${versionId} has no signedUrl (status ${d?.status ?? 'unknown'})`);
|
|
153
|
+
const bytes = await retry(async () => {
|
|
154
|
+
const res = await fetch(d.signedUrl);
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
const err = new Error(`snapshot download HTTP ${res.status}`);
|
|
157
|
+
err.retryable = res.status >= 500 || res.status === 429;
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
return Buffer.from(await res.arrayBuffer());
|
|
161
|
+
}, { label: 'snapshot download' });
|
|
162
|
+
const gzipped = bytes.length > 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
163
|
+
const raw = gzipped ? gunzipSync(bytes) : bytes;
|
|
164
|
+
return { payload: JSON.parse(raw.toString('utf8')), gzip: gzipped ? bytes : null, raw };
|
|
165
|
+
}
|
|
166
|
+
/** Create, wait, download — the whole read in one call. */
|
|
167
|
+
async pull(repoId, label) {
|
|
168
|
+
if (this.onProgress)
|
|
169
|
+
this.report('requesting a snapshot');
|
|
170
|
+
else
|
|
171
|
+
step(`creating snapshot of repo ${repoId}`);
|
|
172
|
+
const versionId = await this.create(repoId);
|
|
173
|
+
await this.waitFor(repoId, versionId);
|
|
174
|
+
if (label)
|
|
175
|
+
await this.rename(repoId, versionId, label);
|
|
176
|
+
this.report('downloading');
|
|
177
|
+
const dl = await this.download(repoId, versionId);
|
|
178
|
+
return { ...dl, versionId };
|
|
179
|
+
}
|
|
180
|
+
/** Most recent already-complete snapshot, so a pull can cost zero snapshots. */
|
|
181
|
+
async latestComplete(repoId) {
|
|
182
|
+
const rows = await this.list(repoId);
|
|
183
|
+
return rows.find((r) => isDone(r.status)) ?? null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { debug } from "../util/log.js";
|
|
7
|
+
/**
|
|
8
|
+
* Bridge to the official Pipefy AI toolkit (github.com/pipefy/ai-toolkit).
|
|
9
|
+
*
|
|
10
|
+
* `pipe` does not implement Pipefy login. When the toolkit is installed, its
|
|
11
|
+
* session *is* the credential: `pipefy auth login` runs the browser OAuth flow
|
|
12
|
+
* and stores a session in the OS keychain, and everything here does is ask the
|
|
13
|
+
* toolkit for the current bearer.
|
|
14
|
+
*
|
|
15
|
+
* That means one login for the MCP server, the `pipefy` CLI and this tool, and
|
|
16
|
+
* it means the keychain, the refresh grant and the credential precedence stay in
|
|
17
|
+
* one place — the toolkit's — rather than being reimplemented against a private
|
|
18
|
+
* store shape that can change under us.
|
|
19
|
+
*
|
|
20
|
+
* The bearer is short-lived and never persisted here. It is fetched per process
|
|
21
|
+
* and re-fetched when the API rejects it, which is why callers take a provider
|
|
22
|
+
* rather than a string.
|
|
23
|
+
*/
|
|
24
|
+
const exec = promisify(execFile);
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const HELPER = join(here, 'toolkit_bearer.py');
|
|
27
|
+
const WINDOWS = process.platform === 'win32';
|
|
28
|
+
/** `pipefy` / `pipefy-mcp-server`, wherever uv put them. */
|
|
29
|
+
const toolBinDir = () => {
|
|
30
|
+
const home = process.env.USERPROFILE ?? process.env.HOME;
|
|
31
|
+
if (!home)
|
|
32
|
+
return null;
|
|
33
|
+
const dir = join(home, '.local', 'bin');
|
|
34
|
+
return existsSync(dir) ? dir : null;
|
|
35
|
+
};
|
|
36
|
+
export const toolkitBinary = (name) => {
|
|
37
|
+
const dir = toolBinDir();
|
|
38
|
+
if (!dir)
|
|
39
|
+
return null;
|
|
40
|
+
const path = join(dir, WINDOWS ? `${name}.exe` : name);
|
|
41
|
+
return existsSync(path) ? path : null;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The interpreter that can import `pipefy_auth`.
|
|
45
|
+
*
|
|
46
|
+
* `uv tool install` gives each tool its own environment, and `uv tool dir` is
|
|
47
|
+
* the documented way to find them — so the layout is asked for rather than
|
|
48
|
+
* assumed. `uv run --with` is the fallback for a machine where the CLI is not
|
|
49
|
+
* installed as a tool but uv is available.
|
|
50
|
+
*/
|
|
51
|
+
const findInterpreter = async () => {
|
|
52
|
+
const roots = [];
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await exec('uv', ['tool', 'dir'], { timeout: 15_000 });
|
|
55
|
+
const dir = stdout.trim();
|
|
56
|
+
if (dir)
|
|
57
|
+
roots.push(dir);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
debug('uv tool dir failed — uv may not be installed');
|
|
61
|
+
}
|
|
62
|
+
const appdata = process.env.APPDATA;
|
|
63
|
+
if (appdata)
|
|
64
|
+
roots.push(join(appdata, 'uv', 'tools'));
|
|
65
|
+
const home = process.env.USERPROFILE ?? process.env.HOME;
|
|
66
|
+
if (home) {
|
|
67
|
+
roots.push(join(home, '.local', 'share', 'uv', 'tools'));
|
|
68
|
+
roots.push(join(home, 'Library', 'Application Support', 'uv', 'tools'));
|
|
69
|
+
}
|
|
70
|
+
for (const root of roots) {
|
|
71
|
+
for (const tool of ['pipefy-cli', 'pipefy-mcp-server']) {
|
|
72
|
+
const python = WINDOWS
|
|
73
|
+
? join(root, tool, 'Scripts', 'python.exe')
|
|
74
|
+
: join(root, tool, 'bin', 'python');
|
|
75
|
+
if (existsSync(python)) {
|
|
76
|
+
debug(`toolkit interpreter: ${python}`);
|
|
77
|
+
return { command: python, args: [] };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// No installed tool env: let uv build an ephemeral one. Slower, and it needs
|
|
82
|
+
// the network the first time, so it is deliberately the last resort.
|
|
83
|
+
try {
|
|
84
|
+
await exec('uv', ['--version'], { timeout: 15_000 });
|
|
85
|
+
debug('falling back to `uv run --with pipefy-auth`');
|
|
86
|
+
return { command: 'uv', args: ['run', '--quiet', '--with', 'pipefy-auth', 'python'] };
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
let cachedInterpreter;
|
|
93
|
+
/** Is the toolkit usable on this machine, and does it currently hold a credential? */
|
|
94
|
+
export const inspectToolkit = async () => {
|
|
95
|
+
cachedInterpreter ??= await findInterpreter();
|
|
96
|
+
if (!cachedInterpreter)
|
|
97
|
+
return { installed: false };
|
|
98
|
+
const run = await runHelper();
|
|
99
|
+
if (!run)
|
|
100
|
+
return { installed: false };
|
|
101
|
+
if ('error' in run) {
|
|
102
|
+
return { installed: true, usable: false, cliPath: toolkitBinary('pipefy'), interpreter: cachedInterpreter.command, error: run.error };
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
installed: true,
|
|
106
|
+
usable: true,
|
|
107
|
+
cliPath: toolkitBinary('pipefy'),
|
|
108
|
+
mcpServerPath: toolkitBinary('pipefy-mcp-server'),
|
|
109
|
+
interpreter: cachedInterpreter.command,
|
|
110
|
+
credential: run.result,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Run the helper. `null` means there is nothing to run it with; `{ error }`
|
|
115
|
+
* means it ran and could not use the toolkit, carrying its own explanation
|
|
116
|
+
* (the helper prints which `pipefy_auth` version it found and what it lacks).
|
|
117
|
+
*/
|
|
118
|
+
const runHelper = async () => {
|
|
119
|
+
cachedInterpreter ??= await findInterpreter();
|
|
120
|
+
if (!cachedInterpreter)
|
|
121
|
+
return null;
|
|
122
|
+
if (!existsSync(HELPER)) {
|
|
123
|
+
debug(`toolkit helper missing at ${HELPER}`);
|
|
124
|
+
return { error: `this install of pipe is missing its toolkit helper (${HELPER})` };
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const { stdout } = await exec(cachedInterpreter.command, [...cachedInterpreter.args, HELPER], { timeout: 60_000, maxBuffer: 1024 * 1024 });
|
|
128
|
+
const parsed = JSON.parse(stdout.trim());
|
|
129
|
+
debug(`toolkit credential: ${parsed.ok ? parsed.source : parsed.reason}`);
|
|
130
|
+
return { result: parsed };
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
const err = e;
|
|
134
|
+
debug(`toolkit bearer helper failed: ${err.message}`);
|
|
135
|
+
const said = err.stderr?.trim().split(/\r?\n/).filter(Boolean).pop();
|
|
136
|
+
return { error: said || err.message.split(/\r?\n/)[0] || 'the toolkit helper failed' };
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Ask the toolkit for a bearer. `null` means the toolkit could not be used at
|
|
141
|
+
* all — distinct from a `{ ok: false }` result, which is the toolkit answering
|
|
142
|
+
* that it holds no credential.
|
|
143
|
+
*/
|
|
144
|
+
export const readBearer = async () => {
|
|
145
|
+
const run = await runHelper();
|
|
146
|
+
return run && 'result' in run ? run.result : null;
|
|
147
|
+
};
|
|
148
|
+
/** Run `pipefy auth login`, inheriting the terminal so the browser flow works. */
|
|
149
|
+
export const runToolkitLogin = async (extraArgs = []) => {
|
|
150
|
+
const cli = toolkitBinary('pipefy');
|
|
151
|
+
const { spawn } = await import('node:child_process');
|
|
152
|
+
const command = cli ?? 'pipefy';
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
const child = spawn(command, ['auth', 'login', ...extraArgs], { stdio: 'inherit' });
|
|
155
|
+
child.on('error', () => resolve(127));
|
|
156
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
/** `pipefy auth status --json`, for `pipe doctor`. Null when unavailable. */
|
|
160
|
+
export const toolkitAuthStatus = async () => {
|
|
161
|
+
const cli = toolkitBinary('pipefy');
|
|
162
|
+
if (!cli)
|
|
163
|
+
return null;
|
|
164
|
+
try {
|
|
165
|
+
const { stdout } = await exec(cli, ['auth', 'status', '--json'], { timeout: 60_000 });
|
|
166
|
+
return JSON.parse(stdout.trim());
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
debug(`pipefy auth status failed: ${e.message}`);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
export const UPGRADE_HINT = 'reinstall the toolkit: `uv tool install --force --reinstall pipefy-cli` (any pipefy-auth from 0.3.0a1 to 0.5.4b1 on PyPI works)';
|
|
174
|
+
export const INSTALL_HINT = 'install the Pipefy AI toolkit (github.com/pipefy/ai-toolkit): `uv tool install pipefy-cli`, then `pipefy auth login`';
|
|
@@ -31,22 +31,63 @@ def _fail(reason: str, message: str) -> int:
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def main() -> int:
|
|
34
|
+
# Import the module, then look the names up one by one, so an incompatible
|
|
35
|
+
# install says which names it lacks and which version it is — rather than
|
|
36
|
+
# one ImportError that left `pipe doctor` reporting the toolkit as "not
|
|
37
|
+
# installed" on a machine where it plainly was. Every pipefy-auth release on
|
|
38
|
+
# PyPI (0.3.0a1 through 0.5.4b1) exports all of these; a field report of
|
|
39
|
+
# one that lacked ServiceAccountAuth came from an install none of them
|
|
40
|
+
# explain, so this names what it found instead of guessing why.
|
|
34
41
|
try:
|
|
35
|
-
|
|
36
|
-
AuthSettings,
|
|
37
|
-
OidcClient,
|
|
38
|
-
ServiceAccountAuth,
|
|
39
|
-
StaticTokenAuth,
|
|
40
|
-
StoredSessionAuth,
|
|
41
|
-
configure_keychain_backend,
|
|
42
|
-
ensure_fresh_session,
|
|
43
|
-
resolve_pipefy_auth,
|
|
44
|
-
)
|
|
45
|
-
from pipefy_auth.resolver import ServiceAccount # noqa: PLC0415
|
|
42
|
+
import pipefy_auth # noqa: PLC0415
|
|
46
43
|
except Exception as exc: # pragma: no cover - environment problem
|
|
47
44
|
print(f"pipefy_auth is not importable: {exc}", file=sys.stderr)
|
|
48
45
|
return 3
|
|
49
46
|
|
|
47
|
+
version = getattr(pipefy_auth, "__version__", None)
|
|
48
|
+
if version is None:
|
|
49
|
+
try:
|
|
50
|
+
from importlib.metadata import version as dist_version # noqa: PLC0415
|
|
51
|
+
|
|
52
|
+
version = dist_version("pipefy-auth")
|
|
53
|
+
except Exception:
|
|
54
|
+
version = "unknown"
|
|
55
|
+
where = getattr(pipefy_auth, "__file__", "?")
|
|
56
|
+
|
|
57
|
+
# Required for every credential this helper can return.
|
|
58
|
+
required = (
|
|
59
|
+
"AuthSettings",
|
|
60
|
+
"OidcClient",
|
|
61
|
+
"StaticTokenAuth",
|
|
62
|
+
"StoredSessionAuth",
|
|
63
|
+
"ensure_fresh_session",
|
|
64
|
+
"resolve_pipefy_auth",
|
|
65
|
+
)
|
|
66
|
+
missing = [n for n in required if not hasattr(pipefy_auth, n)]
|
|
67
|
+
if missing:
|
|
68
|
+
print(
|
|
69
|
+
f"pipefy_auth {version} at {where} is incompatible: it lacks {', '.join(missing)}",
|
|
70
|
+
file=sys.stderr,
|
|
71
|
+
)
|
|
72
|
+
return 3
|
|
73
|
+
|
|
74
|
+
AuthSettings = pipefy_auth.AuthSettings
|
|
75
|
+
OidcClient = pipefy_auth.OidcClient
|
|
76
|
+
StaticTokenAuth = pipefy_auth.StaticTokenAuth
|
|
77
|
+
StoredSessionAuth = pipefy_auth.StoredSessionAuth
|
|
78
|
+
ensure_fresh_session = pipefy_auth.ensure_fresh_session
|
|
79
|
+
resolve_pipefy_auth = pipefy_auth.resolve_pipefy_auth
|
|
80
|
+
# Only a service-account credential needs these, so their absence costs
|
|
81
|
+
# that one method, not the static token or the browser session.
|
|
82
|
+
ServiceAccountAuth = getattr(pipefy_auth, "ServiceAccountAuth", None)
|
|
83
|
+
ServiceAccount = getattr(pipefy_auth, "ServiceAccount", None)
|
|
84
|
+
if ServiceAccount is None:
|
|
85
|
+
try:
|
|
86
|
+
from pipefy_auth.resolver import ServiceAccount # noqa: PLC0415
|
|
87
|
+
except Exception:
|
|
88
|
+
ServiceAccount = None
|
|
89
|
+
configure_keychain_backend = getattr(pipefy_auth, "configure_keychain_backend", None)
|
|
90
|
+
|
|
50
91
|
settings = AuthSettings()
|
|
51
92
|
|
|
52
93
|
# Select the keyring backend before any session read. On Windows the default
|
|
@@ -54,10 +95,15 @@ def main() -> int:
|
|
|
54
95
|
# ~2.5 KB), so machines set `keychain_backend = "file"` — and a reader that
|
|
55
96
|
# skips this looks in the empty store and reports "not signed in" while
|
|
56
97
|
# `pipefy auth status` says otherwise.
|
|
57
|
-
configure_keychain_backend
|
|
98
|
+
if configure_keychain_backend is not None:
|
|
99
|
+
configure_keychain_backend(settings.keychain_backend)
|
|
58
100
|
|
|
59
101
|
service_account = None
|
|
60
|
-
if
|
|
102
|
+
if (
|
|
103
|
+
ServiceAccount is not None
|
|
104
|
+
and settings.service_account_client_id
|
|
105
|
+
and settings.service_account_client_secret
|
|
106
|
+
):
|
|
61
107
|
service_account = ServiceAccount(
|
|
62
108
|
token_url=settings.service_account_url,
|
|
63
109
|
client_id=settings.service_account_client_id,
|
|
@@ -107,7 +153,7 @@ def main() -> int:
|
|
|
107
153
|
)
|
|
108
154
|
return 0
|
|
109
155
|
|
|
110
|
-
if isinstance(resolved, ServiceAccountAuth):
|
|
156
|
+
if ServiceAccountAuth is not None and isinstance(resolved, ServiceAccountAuth):
|
|
111
157
|
# The client-credentials exchange happens here so the secret never
|
|
112
158
|
# crosses the process boundary — only the short-lived bearer does.
|
|
113
159
|
import httpx # noqa: PLC0415
|