@topolo/cli 0.2.4 → 0.3.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/index.js +522 -27
- package/package.json +1 -1
- package/skills/codex/AGENTS.md +12 -3
- package/skills/topolo/SKILL.md +132 -167
package/dist/index.js
CHANGED
|
@@ -2098,6 +2098,262 @@ function firstString(...values) {
|
|
|
2098
2098
|
}
|
|
2099
2099
|
return null;
|
|
2100
2100
|
}
|
|
2101
|
+
var SUPPORTED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2102
|
+
"$schema",
|
|
2103
|
+
"$id",
|
|
2104
|
+
"$ref",
|
|
2105
|
+
"$defs",
|
|
2106
|
+
"definitions",
|
|
2107
|
+
"title",
|
|
2108
|
+
"description",
|
|
2109
|
+
"default",
|
|
2110
|
+
"examples",
|
|
2111
|
+
"deprecated",
|
|
2112
|
+
"readOnly",
|
|
2113
|
+
"writeOnly",
|
|
2114
|
+
"type",
|
|
2115
|
+
"nullable",
|
|
2116
|
+
"const",
|
|
2117
|
+
"enum",
|
|
2118
|
+
"allOf",
|
|
2119
|
+
"anyOf",
|
|
2120
|
+
"oneOf",
|
|
2121
|
+
"properties",
|
|
2122
|
+
"required",
|
|
2123
|
+
"additionalProperties",
|
|
2124
|
+
"propertyNames",
|
|
2125
|
+
"minProperties",
|
|
2126
|
+
"maxProperties",
|
|
2127
|
+
"items",
|
|
2128
|
+
"minItems",
|
|
2129
|
+
"maxItems",
|
|
2130
|
+
"uniqueItems",
|
|
2131
|
+
"minLength",
|
|
2132
|
+
"maxLength",
|
|
2133
|
+
"pattern",
|
|
2134
|
+
"format",
|
|
2135
|
+
"minimum",
|
|
2136
|
+
"maximum",
|
|
2137
|
+
"exclusiveMinimum",
|
|
2138
|
+
"exclusiveMaximum",
|
|
2139
|
+
"multipleOf"
|
|
2140
|
+
]);
|
|
2141
|
+
function validateTopoloActionInput(schema, input2) {
|
|
2142
|
+
const schemaIssue = inspectSchema(schema, "$", schema, 0);
|
|
2143
|
+
if (schemaIssue) return { valid: false, errors: [schemaIssue] };
|
|
2144
|
+
const errors = [];
|
|
2145
|
+
validateNode(schema, input2, "$", schema, errors, 0);
|
|
2146
|
+
return { valid: errors.length === 0, errors };
|
|
2147
|
+
}
|
|
2148
|
+
function inspectSchema(schema, path4, root, depth) {
|
|
2149
|
+
if (depth > 128) return { path: path4, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
|
|
2150
|
+
const unsupportedKeyword = Object.keys(schema).find((keyword) => !SUPPORTED_SCHEMA_KEYWORDS.has(keyword));
|
|
2151
|
+
if (unsupportedKeyword) {
|
|
2152
|
+
return { path: path4, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
|
|
2153
|
+
}
|
|
2154
|
+
if (typeof schema.format === "string" && !["uri", "email", "date-time"].includes(schema.format)) {
|
|
2155
|
+
return { path: path4, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
|
|
2156
|
+
}
|
|
2157
|
+
if (typeof schema.$ref === "string" && !resolveLocalReference(root, schema.$ref)) {
|
|
2158
|
+
return { path: path4, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
|
|
2159
|
+
}
|
|
2160
|
+
const children = [];
|
|
2161
|
+
if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
|
|
2162
|
+
if (isRecord(schema.$defs)) children.push(...Object.entries(schema.$defs));
|
|
2163
|
+
if (isRecord(schema.definitions)) children.push(...Object.entries(schema.definitions));
|
|
2164
|
+
children.push(
|
|
2165
|
+
["additionalProperties", schema.additionalProperties],
|
|
2166
|
+
["propertyNames", schema.propertyNames],
|
|
2167
|
+
["items", schema.items],
|
|
2168
|
+
...arraySchemaEntries("allOf", schema.allOf),
|
|
2169
|
+
...arraySchemaEntries("anyOf", schema.anyOf),
|
|
2170
|
+
...arraySchemaEntries("oneOf", schema.oneOf)
|
|
2171
|
+
);
|
|
2172
|
+
for (const [key, child] of children) {
|
|
2173
|
+
if (!isRecord(child)) continue;
|
|
2174
|
+
const issue = inspectSchema(child, `${path4}.${key}`, root, depth + 1);
|
|
2175
|
+
if (issue) return issue;
|
|
2176
|
+
}
|
|
2177
|
+
return null;
|
|
2178
|
+
}
|
|
2179
|
+
function arraySchemaEntries(keyword, value) {
|
|
2180
|
+
return Array.isArray(value) ? value.map((candidate, index) => [`${keyword}[${index}]`, candidate]) : [];
|
|
2181
|
+
}
|
|
2182
|
+
function validateNode(schema, value, path4, root, errors, depth) {
|
|
2183
|
+
if (depth > 128) {
|
|
2184
|
+
errors.push({ path: path4, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
if (typeof schema.$ref === "string") {
|
|
2188
|
+
const resolved = resolveLocalReference(root, schema.$ref);
|
|
2189
|
+
if (!resolved) {
|
|
2190
|
+
errors.push({ path: path4, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
validateNode(resolved, value, path4, root, errors, depth + 1);
|
|
2194
|
+
}
|
|
2195
|
+
if (Array.isArray(schema.allOf)) {
|
|
2196
|
+
for (const candidate of schema.allOf) {
|
|
2197
|
+
if (isRecord(candidate)) validateNode(candidate, value, path4, root, errors, depth + 1);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
|
|
2201
|
+
errors.push({ path: path4, code: "any_of", message: "Value does not match any allowed schema." });
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
if (Array.isArray(schema.oneOf)) {
|
|
2205
|
+
const matches = schema.oneOf.filter((candidate) => candidateMatches(candidate, value, root, depth)).length;
|
|
2206
|
+
if (matches !== 1) {
|
|
2207
|
+
errors.push({ path: path4, code: "one_of", message: "Value must match exactly one allowed schema." });
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (value === null && schema.nullable === true) return;
|
|
2212
|
+
if (Object.prototype.hasOwnProperty.call(schema, "const") && !sameJsonValue(value, schema.const)) {
|
|
2213
|
+
errors.push({ path: path4, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => sameJsonValue(value, candidate))) {
|
|
2217
|
+
errors.push({ path: path4, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (!matchesType(schema.type, value)) {
|
|
2221
|
+
errors.push({ path: path4, code: "type", message: `Expected ${String(schema.type)}.` });
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
if (typeof value === "string") validateString(schema, value, path4, errors);
|
|
2225
|
+
if (typeof value === "number") validateNumber(schema, value, path4, errors);
|
|
2226
|
+
if (Array.isArray(value)) validateArray(schema, value, path4, root, errors, depth);
|
|
2227
|
+
if (isRecord(value)) validateObject(schema, value, path4, root, errors, depth);
|
|
2228
|
+
}
|
|
2229
|
+
function validateObject(schema, value, path4, root, errors, depth) {
|
|
2230
|
+
if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
|
|
2231
|
+
errors.push({ path: path4, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
|
|
2232
|
+
}
|
|
2233
|
+
if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
|
|
2234
|
+
errors.push({ path: path4, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
|
|
2235
|
+
}
|
|
2236
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
2237
|
+
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
2238
|
+
for (const key of required) {
|
|
2239
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
2240
|
+
errors.push({ path: `${path4}.${key}`, code: "required", message: "Required property is missing." });
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
if (isRecord(schema.propertyNames)) {
|
|
2244
|
+
for (const key of Object.keys(value)) {
|
|
2245
|
+
validateNode(schema.propertyNames, key, `${path4}.${key}`, root, errors, depth + 1);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2249
|
+
const childSchema = properties[key];
|
|
2250
|
+
if (isRecord(childSchema)) {
|
|
2251
|
+
validateNode(childSchema, child, `${path4}.${key}`, root, errors, depth + 1);
|
|
2252
|
+
} else if (schema.additionalProperties === false) {
|
|
2253
|
+
errors.push({ path: `${path4}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
|
|
2254
|
+
} else if (isRecord(schema.additionalProperties)) {
|
|
2255
|
+
validateNode(schema.additionalProperties, child, `${path4}.${key}`, root, errors, depth + 1);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
function validateArray(schema, value, path4, root, errors, depth) {
|
|
2260
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
2261
|
+
errors.push({ path: path4, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
|
|
2262
|
+
}
|
|
2263
|
+
if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
|
|
2264
|
+
errors.push({ path: path4, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
|
|
2265
|
+
}
|
|
2266
|
+
if (schema.uniqueItems === true) {
|
|
2267
|
+
const unique2 = new Set(value.map((item) => JSON.stringify(item)));
|
|
2268
|
+
if (unique2.size !== value.length) errors.push({ path: path4, code: "unique_items", message: "Expected every item to be unique." });
|
|
2269
|
+
}
|
|
2270
|
+
if (isRecord(schema.items)) {
|
|
2271
|
+
value.forEach((item, index) => validateNode(schema.items, item, `${path4}[${index}]`, root, errors, depth + 1));
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
function validateString(schema, value, path4, errors) {
|
|
2275
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
2276
|
+
errors.push({ path: path4, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
|
|
2277
|
+
}
|
|
2278
|
+
if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
|
|
2279
|
+
errors.push({ path: path4, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
|
|
2280
|
+
}
|
|
2281
|
+
if (typeof schema.pattern === "string") {
|
|
2282
|
+
try {
|
|
2283
|
+
if (!new RegExp(schema.pattern).test(value)) errors.push({ path: path4, code: "pattern", message: "Value does not match the required pattern." });
|
|
2284
|
+
} catch {
|
|
2285
|
+
errors.push({ path: path4, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
if (schema.format === "uri" && !isUrl(value)) errors.push({ path: path4, code: "format_uri", message: "Expected an absolute URL." });
|
|
2289
|
+
if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path: path4, code: "format_email", message: "Expected an email address." });
|
|
2290
|
+
if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path: path4, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
|
|
2291
|
+
}
|
|
2292
|
+
function validateNumber(schema, value, path4, errors) {
|
|
2293
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path: path4, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
|
|
2294
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path: path4, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
|
|
2295
|
+
const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum === true && typeof schema.minimum === "number" ? schema.minimum : null;
|
|
2296
|
+
const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum === true && typeof schema.maximum === "number" ? schema.maximum : null;
|
|
2297
|
+
if (exclusiveMinimum !== null && value <= exclusiveMinimum) {
|
|
2298
|
+
errors.push({ path: path4, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
|
|
2299
|
+
}
|
|
2300
|
+
if (exclusiveMaximum !== null && value >= exclusiveMaximum) {
|
|
2301
|
+
errors.push({ path: path4, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
|
|
2302
|
+
}
|
|
2303
|
+
if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
|
|
2304
|
+
errors.push({ path: path4, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
function candidateMatches(candidate, value, root, depth) {
|
|
2308
|
+
if (!isRecord(candidate)) return false;
|
|
2309
|
+
const issues = [];
|
|
2310
|
+
validateNode(candidate, value, "$", root, issues, depth + 1);
|
|
2311
|
+
return issues.length === 0;
|
|
2312
|
+
}
|
|
2313
|
+
function resolveLocalReference(root, reference) {
|
|
2314
|
+
if (!reference.startsWith("#/")) return null;
|
|
2315
|
+
let current = root;
|
|
2316
|
+
for (const token of reference.slice(2).split("/").map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))) {
|
|
2317
|
+
if (Array.isArray(current)) {
|
|
2318
|
+
if (!/^\d+$/.test(token) || Number(token) >= current.length) return null;
|
|
2319
|
+
current = current[Number(token)];
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
if (!isRecord(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
|
|
2323
|
+
current = current[token];
|
|
2324
|
+
}
|
|
2325
|
+
return isRecord(current) ? current : null;
|
|
2326
|
+
}
|
|
2327
|
+
function matchesType(type, value) {
|
|
2328
|
+
if (type === void 0) return true;
|
|
2329
|
+
if (Array.isArray(type)) return type.some((candidate) => matchesType(candidate, value));
|
|
2330
|
+
if (type === "null") return value === null;
|
|
2331
|
+
if (type === "array") return Array.isArray(value);
|
|
2332
|
+
if (type === "object") return isRecord(value);
|
|
2333
|
+
if (type === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
2334
|
+
return typeof value === type;
|
|
2335
|
+
}
|
|
2336
|
+
function sameJsonValue(left, right) {
|
|
2337
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
2338
|
+
}
|
|
2339
|
+
function isRecord(value) {
|
|
2340
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2341
|
+
}
|
|
2342
|
+
function isUrl(value) {
|
|
2343
|
+
try {
|
|
2344
|
+
return Boolean(new URL(value).protocol);
|
|
2345
|
+
} catch {
|
|
2346
|
+
return false;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
function isDateTime(value) {
|
|
2350
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value));
|
|
2351
|
+
}
|
|
2352
|
+
function isMultipleOf(value, divisor) {
|
|
2353
|
+
if (!(divisor > 0)) return false;
|
|
2354
|
+
const quotient = value / divisor;
|
|
2355
|
+
return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
|
|
2356
|
+
}
|
|
2101
2357
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
2102
2358
|
var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
|
|
2103
2359
|
var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
|
|
@@ -2317,7 +2573,10 @@ var TopoloClient = class _TopoloClient {
|
|
|
2317
2573
|
serviceKey,
|
|
2318
2574
|
overrides: this.serviceUrls
|
|
2319
2575
|
});
|
|
2320
|
-
|
|
2576
|
+
if (baseUrl) return { serviceKey, baseUrl };
|
|
2577
|
+
const actionPage = await this.searchActions({ service: match.appId, limit: 1 }).catch(() => null);
|
|
2578
|
+
const actionBaseUrl = actionPage?.actions.find((action) => action.apiBaseUrl)?.apiBaseUrl ?? null;
|
|
2579
|
+
return actionBaseUrl ? { serviceKey, baseUrl: requireHttpOrigin(actionBaseUrl) } : null;
|
|
2321
2580
|
}
|
|
2322
2581
|
/**
|
|
2323
2582
|
* Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
|
|
@@ -2402,7 +2661,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2402
2661
|
}
|
|
2403
2662
|
const entries = [];
|
|
2404
2663
|
for (const item of raw) {
|
|
2405
|
-
if (!
|
|
2664
|
+
if (!isRecord2(item)) continue;
|
|
2406
2665
|
const s = item;
|
|
2407
2666
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
2408
2667
|
if (!appId) continue;
|
|
@@ -2452,6 +2711,44 @@ var TopoloClient = class _TopoloClient {
|
|
|
2452
2711
|
}
|
|
2453
2712
|
return entry;
|
|
2454
2713
|
}
|
|
2714
|
+
async listBindableResources(application) {
|
|
2715
|
+
const service = await this.getService(application);
|
|
2716
|
+
const identity = await this.introspect();
|
|
2717
|
+
if (identity.apiKey) {
|
|
2718
|
+
return {
|
|
2719
|
+
application: { appId: service.appId, slug: service.slug, name: service.name },
|
|
2720
|
+
organizationId: identity.organization?.id ?? null,
|
|
2721
|
+
resources: identity.apiKey.resourceBindings.map((binding) => ({
|
|
2722
|
+
resourceType: binding.resourceType,
|
|
2723
|
+
resourceId: binding.resourceId,
|
|
2724
|
+
label: null,
|
|
2725
|
+
slug: null,
|
|
2726
|
+
status: "active",
|
|
2727
|
+
organizationId: identity.organization?.id ?? null,
|
|
2728
|
+
metadata: {}
|
|
2729
|
+
})),
|
|
2730
|
+
resourceTypes: service.apiKeyResources,
|
|
2731
|
+
syncError: null,
|
|
2732
|
+
source: "api_key_binding"
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
const response = await this.readAction(
|
|
2736
|
+
"app_topolo_auth.service_api_key_resources.list",
|
|
2737
|
+
{ appId: service.appId }
|
|
2738
|
+
);
|
|
2739
|
+
const payload = isRecord2(response) && isRecord2(response.data) ? response.data : response;
|
|
2740
|
+
if (!isRecord2(payload) || !Array.isArray(payload.resources)) {
|
|
2741
|
+
throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
|
|
2742
|
+
}
|
|
2743
|
+
return {
|
|
2744
|
+
application: { appId: service.appId, slug: service.slug, name: service.name },
|
|
2745
|
+
organizationId: readOptionalString(payload.organizationId ?? payload.organization_id),
|
|
2746
|
+
resources: payload.resources.map(normalizeBindableResource).filter((resource) => Boolean(resource)),
|
|
2747
|
+
resourceTypes: normalizeApiKeyResourceMetadataList(payload.resourceTypes ?? payload.resource_types),
|
|
2748
|
+
syncError: readOptionalString(payload.syncError ?? payload.sync_error),
|
|
2749
|
+
source: "auth_catalog"
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2455
2752
|
/**
|
|
2456
2753
|
* List callable actions from the live registry-canonical catalog (definitions
|
|
2457
2754
|
* from the Catalog registry, scoping from Auth). The response is scoped to the
|
|
@@ -2546,10 +2843,44 @@ var TopoloClient = class _TopoloClient {
|
|
|
2546
2843
|
const entry = await this.getAction(action);
|
|
2547
2844
|
return this.executeResolvedAction(entry, input2, options);
|
|
2548
2845
|
}
|
|
2846
|
+
async validateActionInput(action, input2 = {}) {
|
|
2847
|
+
const entry = await this.getAction(action);
|
|
2848
|
+
return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input2) };
|
|
2849
|
+
}
|
|
2850
|
+
async planAction(action, input2 = {}, options = {}) {
|
|
2851
|
+
const entry = await this.getAction(action);
|
|
2852
|
+
const validation = validateTopoloActionInput(entry.inputSchema, input2);
|
|
2853
|
+
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2854
|
+
const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
|
|
2855
|
+
let request = null;
|
|
2856
|
+
if (validation.valid) {
|
|
2857
|
+
const mapped = mapActionInput(entry, input2);
|
|
2858
|
+
request = {
|
|
2859
|
+
method: entry.method,
|
|
2860
|
+
service: entry.appSlug || entry.appId,
|
|
2861
|
+
apiBaseUrl: entry.apiBaseUrl,
|
|
2862
|
+
path: mapped.path,
|
|
2863
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2864
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2865
|
+
...options.resource ? { resource: options.resource } : {}
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
return {
|
|
2869
|
+
action: entry,
|
|
2870
|
+
validation,
|
|
2871
|
+
confirmationRequired,
|
|
2872
|
+
confirmationProvided: options.confirm === true,
|
|
2873
|
+
executable: validation.valid && agentAccess !== "off" && (!confirmationRequired || options.confirm === true),
|
|
2874
|
+
request,
|
|
2875
|
+
verification: stringArray(entry.agentContract?.verify),
|
|
2876
|
+
rollbackActionId: pickActionString(entry.agentContract?.rollbackActionId),
|
|
2877
|
+
nextActionIds: stringArray(entry.agentContract?.nextActionIds)
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2549
2880
|
async readAction(action, input2 = {}, options = {}) {
|
|
2550
2881
|
const entry = await this.getAction(action);
|
|
2551
2882
|
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2552
|
-
if (
|
|
2883
|
+
if (!entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
|
|
2553
2884
|
throw new TopoloPermissionError(
|
|
2554
2885
|
`Topolo action "${entry.actionId}" is not an automatic read-only action.`,
|
|
2555
2886
|
entry.requiredPermission ? [entry.requiredPermission] : []
|
|
@@ -2570,6 +2901,13 @@ var TopoloClient = class _TopoloClient {
|
|
|
2570
2901
|
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
2571
2902
|
);
|
|
2572
2903
|
}
|
|
2904
|
+
const validation = validateTopoloActionInput(entry.inputSchema, input2);
|
|
2905
|
+
if (!validation.valid) {
|
|
2906
|
+
throw new TopoloSdkError(
|
|
2907
|
+
"invalid_action_input",
|
|
2908
|
+
`Topolo action "${entry.actionId}" input is invalid: ${validation.errors.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2573
2911
|
const mapped = mapActionInput(entry, input2);
|
|
2574
2912
|
const service = entry.appSlug || entry.appId;
|
|
2575
2913
|
return this.request({
|
|
@@ -2584,7 +2922,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2584
2922
|
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2585
2923
|
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2586
2924
|
},
|
|
2587
|
-
confirm: options.confirm === true
|
|
2925
|
+
confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
|
|
2588
2926
|
});
|
|
2589
2927
|
}
|
|
2590
2928
|
/**
|
|
@@ -2641,7 +2979,14 @@ var TopoloClient = class _TopoloClient {
|
|
|
2641
2979
|
id: organization.id,
|
|
2642
2980
|
slug: organization.slug,
|
|
2643
2981
|
name: organization.name ?? organization.slug
|
|
2644
|
-
} : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null
|
|
2982
|
+
} : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null,
|
|
2983
|
+
apiKey: kind === "api_key" ? {
|
|
2984
|
+
id: readOptionalString(payload.apiKey?.id),
|
|
2985
|
+
name: readOptionalString(payload.apiKey?.name),
|
|
2986
|
+
appId: readOptionalString(payload.apiKey?.appId),
|
|
2987
|
+
scopes: stringArray(payload.apiKey?.scopes),
|
|
2988
|
+
resourceBindings: normalizeResourceBindings(payload.apiKey?.resourceBindings)
|
|
2989
|
+
} : null
|
|
2645
2990
|
};
|
|
2646
2991
|
}
|
|
2647
2992
|
};
|
|
@@ -2656,7 +3001,7 @@ function resolveConfiguredAppId(slug, appIds) {
|
|
|
2656
3001
|
return null;
|
|
2657
3002
|
}
|
|
2658
3003
|
function normalizeApiKeyResourceMetadata(input2) {
|
|
2659
|
-
if (!
|
|
3004
|
+
if (!isRecord2(input2)) return null;
|
|
2660
3005
|
const resourceType = readString(input2.resourceType ?? input2.resource_type);
|
|
2661
3006
|
const label = readString(input2.label);
|
|
2662
3007
|
const emptyLabel = readString(input2.emptyLabel ?? input2.empty_label);
|
|
@@ -2689,10 +3034,38 @@ function normalizeApiKeyResourceMetadataList(input2) {
|
|
|
2689
3034
|
}
|
|
2690
3035
|
return resources;
|
|
2691
3036
|
}
|
|
3037
|
+
function normalizeBindableResource(input2) {
|
|
3038
|
+
if (!isRecord2(input2)) return null;
|
|
3039
|
+
const resourceType = readString(input2.resourceType ?? input2.resource_type);
|
|
3040
|
+
const resourceId = readString(input2.resourceId ?? input2.resource_id);
|
|
3041
|
+
if (!resourceType || !resourceId) return null;
|
|
3042
|
+
return {
|
|
3043
|
+
resourceType,
|
|
3044
|
+
resourceId,
|
|
3045
|
+
label: readOptionalString(input2.label),
|
|
3046
|
+
slug: readOptionalString(input2.slug),
|
|
3047
|
+
status: readOptionalString(input2.status),
|
|
3048
|
+
organizationId: readOptionalString(input2.organizationId ?? input2.organization_id),
|
|
3049
|
+
metadata: isRecord2(input2.metadata) ? input2.metadata : {}
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
function normalizeResourceBindings(input2) {
|
|
3053
|
+
if (!Array.isArray(input2)) return [];
|
|
3054
|
+
return input2.flatMap((binding) => {
|
|
3055
|
+
if (!isRecord2(binding)) return [];
|
|
3056
|
+
const resourceType = readString(binding.resourceType ?? binding.resource_type);
|
|
3057
|
+
const resourceId = readString(binding.resourceId ?? binding.resource_id);
|
|
3058
|
+
return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
2692
3061
|
function readString(value) {
|
|
2693
3062
|
return typeof value === "string" ? value.trim() : "";
|
|
2694
3063
|
}
|
|
2695
|
-
function
|
|
3064
|
+
function readOptionalString(value) {
|
|
3065
|
+
const normalized = readString(value);
|
|
3066
|
+
return normalized || null;
|
|
3067
|
+
}
|
|
3068
|
+
function isRecord2(value) {
|
|
2696
3069
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2697
3070
|
}
|
|
2698
3071
|
function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
@@ -2764,14 +3137,14 @@ function catalogSearchQuery(options) {
|
|
|
2764
3137
|
return query;
|
|
2765
3138
|
}
|
|
2766
3139
|
function normalizeCatalogPage(value) {
|
|
2767
|
-
if (!
|
|
3140
|
+
if (!isRecord2(value)) return { nextCursor: null, hasMore: false };
|
|
2768
3141
|
return {
|
|
2769
3142
|
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2770
3143
|
hasMore: value.hasMore === true
|
|
2771
3144
|
};
|
|
2772
3145
|
}
|
|
2773
3146
|
function normalizeServiceCatalogEntry(item) {
|
|
2774
|
-
if (!
|
|
3147
|
+
if (!isRecord2(item)) return null;
|
|
2775
3148
|
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2776
3149
|
if (!appId) return null;
|
|
2777
3150
|
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
@@ -2826,6 +3199,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2826
3199
|
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
2827
3200
|
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2828
3201
|
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
3202
|
+
agentContract: plainObject(a.agent_contract ?? a.agentContract),
|
|
2829
3203
|
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
2830
3204
|
destructive: Boolean(a.destructive),
|
|
2831
3205
|
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
@@ -2849,6 +3223,9 @@ function pickAuthorizationMode(value) {
|
|
|
2849
3223
|
function plainObject(value) {
|
|
2850
3224
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2851
3225
|
}
|
|
3226
|
+
function stringArray(value) {
|
|
3227
|
+
return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
|
|
3228
|
+
}
|
|
2852
3229
|
function mapActionInput(action, input2) {
|
|
2853
3230
|
const remaining = { ...input2 || {} };
|
|
2854
3231
|
const path4 = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
@@ -3959,8 +4336,10 @@ function formatPretty(info) {
|
|
|
3959
4336
|
return lines.join("\n");
|
|
3960
4337
|
}
|
|
3961
4338
|
|
|
4339
|
+
// src/commands/actions.ts
|
|
4340
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
4341
|
+
|
|
3962
4342
|
// src/commands/api.ts
|
|
3963
|
-
var STATIC_APP_IDS = new Set(Object.keys(DEFAULT_APP_URLS));
|
|
3964
4343
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
3965
4344
|
var WRITE_METHODS2 = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3966
4345
|
function registerApiCommand(program2) {
|
|
@@ -3987,9 +4366,8 @@ function registerApiCommand(program2) {
|
|
|
3987
4366
|
const config = await readConfig();
|
|
3988
4367
|
const profile = resolvedProfileConfig(config, environment);
|
|
3989
4368
|
const configuredUrl = resolveConfiguredServiceUrl2(service, profile.serviceUrls);
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
assertServiceUrlForEnvironment(service, configuredUrl ?? staticUrl, environment);
|
|
4369
|
+
if (configuredUrl) {
|
|
4370
|
+
assertServiceUrlForEnvironment(service, configuredUrl, environment);
|
|
3993
4371
|
}
|
|
3994
4372
|
let body = void 0;
|
|
3995
4373
|
if (opts.data) {
|
|
@@ -4096,7 +4474,7 @@ function registerActionsCommand(program2) {
|
|
|
4096
4474
|
fail(err, mode);
|
|
4097
4475
|
}
|
|
4098
4476
|
});
|
|
4099
|
-
actions.command("call <action>").description("Call a permission-scoped Topolo action").option("-d, --data <json>", "JSON action input", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Acknowledge a mutating action").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
4477
|
+
actions.command("call <action>").description("Call a permission-scoped Topolo action").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Acknowledge a mutating action").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
4100
4478
|
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
4101
4479
|
try {
|
|
4102
4480
|
const topolo = await buildClient();
|
|
@@ -4114,11 +4492,78 @@ function registerActionsCommand(program2) {
|
|
|
4114
4492
|
fail(err, mode);
|
|
4115
4493
|
}
|
|
4116
4494
|
});
|
|
4495
|
+
actions.command("validate <action>").description("Validate action input locally against the published JSON Schema").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
4496
|
+
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
4497
|
+
try {
|
|
4498
|
+
const topolo = await requiredClient();
|
|
4499
|
+
const result = await topolo.client.validateActionInput(action, parseJsonObject(opts.data || "{}"));
|
|
4500
|
+
emit(result, mode, () => result.validation.valid ? `Valid input for ${result.action.actionId}.` : result.validation.errors.map((issue) => `${issue.path}: ${issue.message}`).join("\n"));
|
|
4501
|
+
} catch (err) {
|
|
4502
|
+
fail(err, mode);
|
|
4503
|
+
}
|
|
4504
|
+
});
|
|
4505
|
+
actions.command("plan <action>").description("Validate and preview the exact request, confirmation, verification, and rollback plan").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Include confirmation in the plan without executing it").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
4506
|
+
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
4507
|
+
try {
|
|
4508
|
+
const topolo = await requiredClient();
|
|
4509
|
+
const resource = parseResourceOption(opts.resourceType, opts.resourceId);
|
|
4510
|
+
const plan = await topolo.client.planAction(action, parseJsonObject(opts.data || "{}"), {
|
|
4511
|
+
confirm: opts.confirm === true,
|
|
4512
|
+
...resource ? { resource } : {}
|
|
4513
|
+
});
|
|
4514
|
+
emit(plan, mode, () => formatActionPlan(plan));
|
|
4515
|
+
} catch (err) {
|
|
4516
|
+
fail(err, mode);
|
|
4517
|
+
}
|
|
4518
|
+
});
|
|
4519
|
+
actions.command("examples <action>").description("Show published examples and public docs for one action").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
|
|
4520
|
+
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
4521
|
+
try {
|
|
4522
|
+
const topolo = await requiredClient();
|
|
4523
|
+
const entry = await topolo.client.getAction(action);
|
|
4524
|
+
const result = {
|
|
4525
|
+
actionId: entry.actionId,
|
|
4526
|
+
docsUrl: entry.agentContract?.docsUrl ?? null,
|
|
4527
|
+
examples: entry.agentContract?.examples ?? []
|
|
4528
|
+
};
|
|
4529
|
+
emit(result, mode, () => result.examples.length ? JSON.stringify(result, null, 2) : `No inline examples published. Docs: ${result.docsUrl || "not published"}`);
|
|
4530
|
+
} catch (err) {
|
|
4531
|
+
fail(err, mode);
|
|
4532
|
+
}
|
|
4533
|
+
});
|
|
4534
|
+
actions.command("capabilities").description("Discover capability and schema actions for one application").option("-s, --service <service>", "App id, slug, or name").option("--json", "Output JSON").option("--no-json", "Output human-readable table").action(async (opts, command) => {
|
|
4535
|
+
const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
|
|
4536
|
+
try {
|
|
4537
|
+
const topolo = await requiredClient();
|
|
4538
|
+
const service = opts.service || command.parent?.opts().service;
|
|
4539
|
+
if (!service?.trim()) throw new Error("--service is required for capability discovery");
|
|
4540
|
+
const result = await topolo.client.searchActions({ service: service.trim(), query: "capabilities", limit: 100 });
|
|
4541
|
+
emit(result, mode, () => formatActionRows(result.actions, result.page.nextCursor));
|
|
4542
|
+
} catch (err) {
|
|
4543
|
+
fail(err, mode);
|
|
4544
|
+
}
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
async function requiredClient() {
|
|
4548
|
+
const topolo = await buildClient();
|
|
4549
|
+
if (!topolo) throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
4550
|
+
return topolo;
|
|
4117
4551
|
}
|
|
4118
4552
|
function parseJsonObject(value) {
|
|
4553
|
+
let source = value;
|
|
4554
|
+
if (value.startsWith("@")) {
|
|
4555
|
+
const path4 = value.slice(1);
|
|
4556
|
+
if (!path4) throw new Error("--data @file path must not be empty");
|
|
4557
|
+
try {
|
|
4558
|
+
source = readFileSync2(path4, "utf8");
|
|
4559
|
+
} catch (err) {
|
|
4560
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
4561
|
+
throw new Error(`Could not read --data file ${path4}: ${detail}`);
|
|
4562
|
+
}
|
|
4563
|
+
}
|
|
4119
4564
|
let parsed;
|
|
4120
4565
|
try {
|
|
4121
|
-
parsed = JSON.parse(
|
|
4566
|
+
parsed = JSON.parse(source);
|
|
4122
4567
|
} catch {
|
|
4123
4568
|
throw new Error("--data must be valid JSON");
|
|
4124
4569
|
}
|
|
@@ -4164,21 +4609,37 @@ function formatActionDetail(action) {
|
|
|
4164
4609
|
`Path: ${action.path}`,
|
|
4165
4610
|
`Permission: ${action.requiredPermission}`,
|
|
4166
4611
|
`Confirmation: ${action.requiresConfirmation ? "required" : "not required"}`,
|
|
4612
|
+
`Docs: ${action.agentContract?.docsUrl || "-"}`,
|
|
4613
|
+
`Rollback: ${action.agentContract?.rollbackActionId || "-"}`,
|
|
4167
4614
|
"",
|
|
4168
4615
|
action.description
|
|
4169
4616
|
].join("\n");
|
|
4170
4617
|
}
|
|
4618
|
+
function formatActionPlan(plan) {
|
|
4619
|
+
const lines = [
|
|
4620
|
+
`Action: ${plan.action.actionId}`,
|
|
4621
|
+
`Valid: ${plan.validation.valid ? "yes" : "no"}`,
|
|
4622
|
+
`Executable: ${plan.executable ? "yes" : "no"}`,
|
|
4623
|
+
`Confirmation: ${plan.confirmationRequired ? plan.confirmationProvided ? "provided" : "required" : "not required"}`
|
|
4624
|
+
];
|
|
4625
|
+
if (plan.request) lines.push(`Request: ${plan.request.method} ${plan.request.apiBaseUrl || plan.request.service}${plan.request.path}`);
|
|
4626
|
+
if (plan.request?.resource) lines.push(`Resource: ${plan.request.resource.resourceType}:${plan.request.resource.resourceId}`);
|
|
4627
|
+
if (plan.validation.errors.length) lines.push("", ...plan.validation.errors.map((issue) => `${issue.path}: ${issue.message}`));
|
|
4628
|
+
if (plan.verification.length) lines.push("", "Verify:", ...plan.verification.map((step) => `- ${step}`));
|
|
4629
|
+
if (plan.rollbackActionId) lines.push("", `Rollback: ${plan.rollbackActionId}`);
|
|
4630
|
+
return lines.join("\n");
|
|
4631
|
+
}
|
|
4171
4632
|
|
|
4172
4633
|
// src/commands/apps.ts
|
|
4173
4634
|
import { execSync } from "child_process";
|
|
4174
|
-
import { existsSync as existsSync3, mkdirSync, readFileSync as
|
|
4635
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
4175
4636
|
import path2 from "path";
|
|
4176
4637
|
|
|
4177
4638
|
// src/commands/app-audit-local.ts
|
|
4178
4639
|
import {
|
|
4179
4640
|
existsSync as existsSync2,
|
|
4180
4641
|
readdirSync,
|
|
4181
|
-
readFileSync as
|
|
4642
|
+
readFileSync as readFileSync3,
|
|
4182
4643
|
statSync
|
|
4183
4644
|
} from "fs";
|
|
4184
4645
|
import path from "path";
|
|
@@ -4189,7 +4650,7 @@ function readEvidenceFile(appRoot) {
|
|
|
4189
4650
|
const candidate = path.join(appRoot, ".topolo", "evidence.json");
|
|
4190
4651
|
if (!isFile(candidate)) return null;
|
|
4191
4652
|
try {
|
|
4192
|
-
const raw = JSON.parse(
|
|
4653
|
+
const raw = JSON.parse(readFileSync3(candidate, "utf8"));
|
|
4193
4654
|
if (raw && typeof raw === "object" && raw.version === 1) {
|
|
4194
4655
|
return raw;
|
|
4195
4656
|
}
|
|
@@ -5133,7 +5594,7 @@ function readSourceIndex(root) {
|
|
|
5133
5594
|
const files = collectSourceFiles(root, 4e3);
|
|
5134
5595
|
const chunks = files.map((file) => {
|
|
5135
5596
|
try {
|
|
5136
|
-
return
|
|
5597
|
+
return readFileSync3(file, "utf8");
|
|
5137
5598
|
} catch {
|
|
5138
5599
|
return "";
|
|
5139
5600
|
}
|
|
@@ -5193,7 +5654,7 @@ function sortedDirEntries(dir) {
|
|
|
5193
5654
|
function readJson(filePath) {
|
|
5194
5655
|
if (!isFile(filePath)) return null;
|
|
5195
5656
|
try {
|
|
5196
|
-
return JSON.parse(
|
|
5657
|
+
return JSON.parse(readFileSync3(filePath, "utf8"));
|
|
5197
5658
|
} catch {
|
|
5198
5659
|
return null;
|
|
5199
5660
|
}
|
|
@@ -9289,7 +9750,8 @@ function applicationRowsFromCatalog(catalog) {
|
|
|
9289
9750
|
name: entry.name ?? entry.slug ?? entry.appId,
|
|
9290
9751
|
appDir: "-",
|
|
9291
9752
|
packageName: null,
|
|
9292
|
-
|
|
9753
|
+
apiBaseUrl: entry.apiBaseUrl ?? null,
|
|
9754
|
+
launchUrl: entry.launchUrl ?? null,
|
|
9293
9755
|
services: [displayCatalogId(entry)],
|
|
9294
9756
|
deployTargets: 0
|
|
9295
9757
|
})).sort((left, right) => left.id.localeCompare(right.id));
|
|
@@ -9354,7 +9816,7 @@ function recordApplicationEvidence(application, options = {}) {
|
|
|
9354
9816
|
let before = null;
|
|
9355
9817
|
if (existsSync3(evidencePath)) {
|
|
9356
9818
|
try {
|
|
9357
|
-
const raw = JSON.parse(
|
|
9819
|
+
const raw = JSON.parse(readFileSync4(evidencePath, "utf8"));
|
|
9358
9820
|
if (raw && typeof raw === "object" && raw.version === 1) {
|
|
9359
9821
|
before = raw;
|
|
9360
9822
|
}
|
|
@@ -9434,7 +9896,7 @@ function evaluateApplicationAuditGate(payload, failOn) {
|
|
|
9434
9896
|
function formatRows(rows) {
|
|
9435
9897
|
const idWidth = Math.max(2, ...rows.map((r) => r.id.length));
|
|
9436
9898
|
const nameWidth = Math.max(4, ...rows.map((r) => r.name.length));
|
|
9437
|
-
const urlWidth = Math.max(3, ...rows.map((r) => (r
|
|
9899
|
+
const urlWidth = Math.max(3, ...rows.map((r) => (applicationUrl(r) ?? "-").length));
|
|
9438
9900
|
const servicesWidth = Math.max(
|
|
9439
9901
|
8,
|
|
9440
9902
|
...rows.map((r) => r.services.length ? r.services.join(",").length : 1)
|
|
@@ -9449,7 +9911,7 @@ function formatRows(rows) {
|
|
|
9449
9911
|
for (const r of rows) {
|
|
9450
9912
|
const services = r.services.length ? r.services.join(",") : "-";
|
|
9451
9913
|
lines.push(
|
|
9452
|
-
`${r.id.padEnd(idWidth)} ${r.name.padEnd(nameWidth)} ${(r
|
|
9914
|
+
`${r.id.padEnd(idWidth)} ${r.name.padEnd(nameWidth)} ${(applicationUrl(r) ?? "-").padEnd(urlWidth)} ${services.padEnd(servicesWidth)} ${r.deployTargets}`
|
|
9453
9915
|
);
|
|
9454
9916
|
}
|
|
9455
9917
|
return lines.join("\n");
|
|
@@ -9458,10 +9920,14 @@ function formatApplicationRow(application) {
|
|
|
9458
9920
|
return [
|
|
9459
9921
|
`ID: ${application.id}`,
|
|
9460
9922
|
`Name: ${application.name}`,
|
|
9461
|
-
`URL: ${application.
|
|
9923
|
+
`API URL: ${application.apiBaseUrl ?? "-"}`,
|
|
9924
|
+
`Launch URL: ${application.launchUrl ?? application.productionUrl ?? "-"}`,
|
|
9462
9925
|
`Services: ${application.services.length ? application.services.join(", ") : "-"}`
|
|
9463
9926
|
].join("\n");
|
|
9464
9927
|
}
|
|
9928
|
+
function applicationUrl(application) {
|
|
9929
|
+
return application.apiBaseUrl ?? application.launchUrl ?? application.productionUrl ?? null;
|
|
9930
|
+
}
|
|
9465
9931
|
function displayCatalogId(entry) {
|
|
9466
9932
|
for (const value of [entry.slug, entry.name, entry.appId]) {
|
|
9467
9933
|
if (typeof value !== "string" || !value.trim()) continue;
|
|
@@ -9645,7 +10111,7 @@ function statusesAtOrWorse(threshold) {
|
|
|
9645
10111
|
}
|
|
9646
10112
|
|
|
9647
10113
|
// src/commands/platform.ts
|
|
9648
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
10114
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
9649
10115
|
import path3 from "path";
|
|
9650
10116
|
function registerPlatformCommand(program2) {
|
|
9651
10117
|
const platform3 = program2.command("platform").description("Internal Topolo first-party platform and personal app commands");
|
|
@@ -9779,7 +10245,7 @@ function scanStagingProductionUrls(rootInput, allowPatterns) {
|
|
|
9779
10245
|
function scanFile(filePath) {
|
|
9780
10246
|
if (shouldSkipFile(filePath)) return;
|
|
9781
10247
|
const relativePath = path3.relative(root, filePath);
|
|
9782
|
-
const content =
|
|
10248
|
+
const content = readFileSync5(filePath, "utf8");
|
|
9783
10249
|
const lines = content.split(/\r?\n/);
|
|
9784
10250
|
lines.forEach((line, index) => {
|
|
9785
10251
|
const matches = [...line.matchAll(PRODUCTION_TOPOLO_URL_PATTERN)].map((match) => match[0]);
|
|
@@ -10859,6 +11325,34 @@ function registerContextCommand(program2) {
|
|
|
10859
11325
|
});
|
|
10860
11326
|
}
|
|
10861
11327
|
|
|
11328
|
+
// src/commands/resources.ts
|
|
11329
|
+
function registerResourcesCommand(program2) {
|
|
11330
|
+
program2.command("resources <application>").description("List bindable resource ids for one credential-scoped Topolo application").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (application, opts) => {
|
|
11331
|
+
const mode = resolveOutputMode(opts.json);
|
|
11332
|
+
try {
|
|
11333
|
+
const topolo = await buildClient();
|
|
11334
|
+
if (!topolo) throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
11335
|
+
const result = await topolo.client.listBindableResources(application);
|
|
11336
|
+
emit(result, mode, () => formatResources(result));
|
|
11337
|
+
} catch (err) {
|
|
11338
|
+
fail(err, mode);
|
|
11339
|
+
}
|
|
11340
|
+
});
|
|
11341
|
+
}
|
|
11342
|
+
function formatResources(result) {
|
|
11343
|
+
const lines = [
|
|
11344
|
+
`Application: ${result.application.name || result.application.slug || result.application.appId}`,
|
|
11345
|
+
`Source: ${result.source}`
|
|
11346
|
+
];
|
|
11347
|
+
if (result.resources.length === 0) return [...lines, "", "No bindable resources available."].join("\n");
|
|
11348
|
+
lines.push("", "TYPE ID LABEL");
|
|
11349
|
+
for (const resource of result.resources) {
|
|
11350
|
+
lines.push(`${resource.resourceType} ${resource.resourceId} ${resource.label || resource.slug || "-"}`);
|
|
11351
|
+
}
|
|
11352
|
+
if (result.syncError) lines.push("", `Sync warning: ${result.syncError}`);
|
|
11353
|
+
return lines.join("\n");
|
|
11354
|
+
}
|
|
11355
|
+
|
|
10862
11356
|
// src/index.ts
|
|
10863
11357
|
var program = new Command();
|
|
10864
11358
|
var argv;
|
|
@@ -10889,6 +11383,7 @@ registerServicesCommand(program);
|
|
|
10889
11383
|
registerSeedCommands(program);
|
|
10890
11384
|
registerSkillsCommand(program);
|
|
10891
11385
|
registerContextCommand(program);
|
|
11386
|
+
registerResourcesCommand(program);
|
|
10892
11387
|
program.parseAsync(argv).catch((err) => {
|
|
10893
11388
|
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
10894
11389
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/skills/codex/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Topolo — agent guide (Codex / general-purpose agents)
|
|
2
2
|
|
|
3
|
-
This file teaches an agent how to interact with the Topolo platform via the `topolo` CLI and, when available, the Topolo MCP server. It
|
|
3
|
+
This file teaches an agent how to interact with the Topolo platform via the `topolo` CLI and, when available, the Topolo MCP server. It assumes no repository access or private product knowledge; public Topolo docs and credential-scoped runtime discovery are authoritative.
|
|
4
4
|
|
|
5
5
|
## What the platform is
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ Topolo is a multi-app SaaS platform. The CLI and MCP server both use `@topolo/sd
|
|
|
11
11
|
- Use `topolo apps audit <application_id> --json` or the MCP `topolo_audit_applications` tool to inspect current requirement conformance and migration work.
|
|
12
12
|
- Use `topolo services --json` or the MCP `topolo_search_applications` tool to discover callable app ids and effective URLs.
|
|
13
13
|
- Use `topolo actions --service <app> --json` or MCP `topolo_search_actions` to discover callable operations granted to the current credential; invoke automatic reads with `topolo_read_action` and confirmation-gated operations with `topolo_call_action`.
|
|
14
|
+
- Use `topolo actions capabilities`, `actions examples`, `actions validate`, and `actions plan` (or their MCP equivalents) to obtain schemas, examples, verification, and rollback before execution.
|
|
14
15
|
- The generic `api` command and MCP `topolo_api_call` tool can reach bundled SDK services and live Auth-catalog API/technical services when no action contract exists yet.
|
|
15
16
|
|
|
16
17
|
Every call is scoped to the organization embedded in the credential. There is no `--org` flag anywhere — attempting to target another org will fail.
|
|
@@ -94,6 +95,11 @@ URL and the CLI will refuse `topolo api` calls until a staging override is set.
|
|
|
94
95
|
```bash
|
|
95
96
|
topolo actions --service mail --json
|
|
96
97
|
topolo actions get app_topolo_mail.messages.send --json
|
|
98
|
+
topolo actions examples app_topolo_mail.messages.send --json
|
|
99
|
+
topolo actions validate app_topolo_mail.messages.send \
|
|
100
|
+
--data '{"mailboxId":"primary","subject":"Hello","text":"..."}' --json
|
|
101
|
+
topolo actions plan app_topolo_mail.messages.send \
|
|
102
|
+
--data '{"mailboxId":"primary","subject":"Hello","text":"..."}' --json
|
|
97
103
|
topolo actions call app_topolo_mail.messages.send \
|
|
98
104
|
--data '{"mailboxId":"primary","subject":"Hello","text":"..."}' \
|
|
99
105
|
--confirm --json
|
|
@@ -104,6 +110,10 @@ operations. It is filtered by the signed-in organization and the exact service
|
|
|
104
110
|
permissions on the credential. Actions marked `authorizationMode: "runtime"`
|
|
105
111
|
have input-dependent policy and are definitively authorized by the target app
|
|
106
112
|
for the supplied organization, application, and resource at invocation time.
|
|
113
|
+
Never guess a missing action, field, resource id, endpoint, verification step,
|
|
114
|
+
or rollback. If catalog metadata and `https://docs.topolo.app` do not provide a
|
|
115
|
+
complete contract, report the missing publication contract instead of reading
|
|
116
|
+
source code or inferring a private API.
|
|
107
117
|
|
|
108
118
|
### Generic passthrough
|
|
109
119
|
|
|
@@ -151,5 +161,4 @@ Do not batch writes. Do not retry a failed write without re-confirming. The `--c
|
|
|
151
161
|
|
|
152
162
|
- Documentation: `https://docs.topolo.app` (guides, authentication, API reference).
|
|
153
163
|
- Developers console (register apps / OAuth clients): `https://developers.topolo.app`
|
|
154
|
-
- CLI
|
|
155
|
-
- Schemas: `packages/topolo-sdk/src/modules/` in the monorepo.
|
|
164
|
+
- CLI help: `topolo --help` and `topolo <command> --help`.
|
package/skills/topolo/SKILL.md
CHANGED
|
@@ -1,248 +1,213 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: topolo
|
|
3
|
-
description:
|
|
3
|
+
description: Operate the Topolo platform from any computer using only public Topolo documentation and credential-scoped CLI or MCP discovery. Use for Topolo identity, organizations, applications, resources, actions, OAuth, and API keys.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Topolo
|
|
6
|
+
# Topolo clean-room operator
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Use Topolo without repository access, private schemas, or prior product knowledge. The authenticated CLI/MCP catalog and `https://docs.topolo.app` are the only operational sources of truth.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Never inspect or request Topolo source code for ordinary platform work. Never invent an endpoint, action id, field, enum, resource id, permission, verification step, or rollback. If the live contract and public docs do not provide enough information, report the exact missing metadata as a platform contract defect.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## Install and connect
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
- Asks about their Topolo identity ("who am I logged in as", "what org am I in", "what scopes do I have").
|
|
16
|
-
- Wants to call an endpoint under a Topolo service from the shell.
|
|
17
|
-
- Mentions a Topolo OAuth client (`doac_…`) or API key.
|
|
14
|
+
The CLI requires Node 20 or newer:
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
```bash
|
|
17
|
+
command -v topolo || npm install -g @topolo/cli
|
|
18
|
+
topolo skills install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`topolo skills install` installs this skill and registers the Topolo MCP server for Claude Code and Codex. Both use the same credential written by `topolo auth login`.
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
Production is the default. Use `--env staging` on every staging command, or set `TOPOLO_ENV=staging`. Never mix staging discovery with production execution.
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
## Authentication gate
|
|
26
|
+
|
|
27
|
+
Start every session with:
|
|
24
28
|
|
|
25
29
|
```bash
|
|
26
30
|
topolo auth status --json
|
|
31
|
+
topolo whoami --json
|
|
27
32
|
```
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
If `source` is `none`, run `topolo auth login` and wait for browser approval. The built-in first-party OAuth client needs no client id. For a headless session, use `topolo auth login --api-key=<key>` or inject `TOPOLO_API_KEY` for one command. Never store credentials in a project or tracked file.
|
|
35
|
+
|
|
36
|
+
Authentication scopes and effective authorization are different:
|
|
37
|
+
|
|
38
|
+
- OAuth scopes describe what the token may ask the platform to do.
|
|
39
|
+
- `whoami`, the credential-scoped application catalog, action catalog, target policy, and resource binding determine what the current identity may actually do.
|
|
40
|
+
- An action with `authorizationMode: "runtime"` is discoverable, but the target application makes the final authorization decision from the concrete input and credential context.
|
|
41
|
+
- A 403 is authoritative. Do not retry it with guessed fields, another endpoint, or broader behavior.
|
|
42
|
+
|
|
43
|
+
The credential fixes the personal or organization context. There is no `--org` override:
|
|
32
44
|
|
|
33
45
|
```bash
|
|
34
|
-
topolo
|
|
35
|
-
topolo
|
|
46
|
+
topolo context ls --json
|
|
47
|
+
topolo context use <organization-name|personal> --json
|
|
48
|
+
topolo whoami --json
|
|
36
49
|
```
|
|
37
50
|
|
|
38
|
-
|
|
51
|
+
## Mandatory clean-room workflow
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
| --- | --- |
|
|
42
|
-
| `env:TOPOLO_STAGING_API_KEY` / `env:TOPOLO_PRODUCTION_API_KEY` | Environment-specific service-scoped API key in env |
|
|
43
|
-
| `env:TOPOLO_STAGING_ACCESS_TOKEN` / `env:TOPOLO_PRODUCTION_ACCESS_TOKEN` | Environment-specific short-lived access token in env |
|
|
44
|
-
| `env:TOPOLO_API_KEY` | Shared service-scoped API key in env |
|
|
45
|
-
| `env:TOPOLO_ACCESS_TOKEN` | Shared short-lived access token in env (dev/testing) |
|
|
46
|
-
| `config-file` | Token stored in the selected environment profile at `$XDG_CONFIG_HOME/topolo/config.json` |
|
|
47
|
-
| `none` | Not signed in — call `topolo auth login` |
|
|
53
|
+
For every Topolo task, follow this sequence.
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
tell the user to approve the device in the browser tab that opens. The CLI uses
|
|
51
|
-
its **built-in first-party client**, so **no `--client-id` is needed** — any
|
|
52
|
-
Topolo user can sign in; what they can then do is decided by their permissions.
|
|
53
|
-
The CLI blocks until they approve, then stores the token in that environment's
|
|
54
|
-
active-context profile. (`--client-id=<doac_…>` is only for advanced/self-hosted
|
|
55
|
-
setups that register their own OAuth client.)
|
|
55
|
+
### 1. Discover the application and resource boundary
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
```bash
|
|
58
|
+
topolo apps --json
|
|
59
|
+
topolo apps get <application> --json
|
|
60
|
+
topolo services --query <application> --json
|
|
61
|
+
topolo resources <application> --json
|
|
62
|
+
```
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
Use the returned app id or slug. Read `apiKeyResources` to learn whether calls can be scoped to aliases such as a workspace, site, mailbox, brand, or venue. Use `topolo resources` to resolve the actual credential-scoped resource ids. With an API key it returns only the key's bound resources; with OAuth it uses the published Auth resource catalog. Never assume that an organization id is a valid app resource id.
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
MCP equivalents:
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
- `topolo_search_applications`
|
|
69
|
+
- `topolo_get_application`
|
|
70
|
+
- `topolo_get_resource_types`
|
|
71
|
+
- `topolo_list_resources`
|
|
66
72
|
|
|
67
|
-
###
|
|
73
|
+
### 2. Discover credential-scoped actions
|
|
68
74
|
|
|
69
75
|
```bash
|
|
70
|
-
topolo
|
|
76
|
+
topolo actions --service <app-id-or-slug> --query <task-keyword> --limit 25 --json
|
|
77
|
+
topolo actions capabilities --service <app-id-or-slug> --json
|
|
78
|
+
topolo actions get <exact-action-id> --json
|
|
79
|
+
topolo actions examples <exact-action-id> --json
|
|
71
80
|
```
|
|
72
81
|
|
|
73
|
-
|
|
82
|
+
Inspect the published input/output JSON Schemas and agent contract. The agent contract may provide:
|
|
83
|
+
|
|
84
|
+
- public `docsUrl`
|
|
85
|
+
- resource kind and id/version paths
|
|
86
|
+
- effects
|
|
87
|
+
- actionable error codes and recovery
|
|
88
|
+
- verification steps
|
|
89
|
+
- rollback action id
|
|
90
|
+
- next actions
|
|
91
|
+
- runnable examples
|
|
74
92
|
|
|
75
|
-
|
|
93
|
+
MCP equivalents:
|
|
76
94
|
|
|
77
|
-
|
|
95
|
+
- `topolo_search_actions`
|
|
96
|
+
- `topolo_discover_capabilities`
|
|
97
|
+
- `topolo_get_action`
|
|
98
|
+
- `topolo_get_action_examples`
|
|
78
99
|
|
|
79
|
-
|
|
100
|
+
If discovery returns no relevant action, search `https://docs.topolo.app`. Use `topolo api` only when public docs give the exact service, method, path, schema, authorization, and verification behavior. Absence of a catalog action is not permission to infer a private API.
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
### 3. Resolve identifiers through published reads
|
|
103
|
+
|
|
104
|
+
Prefer `topolo resources <application> --json` or `topolo_list_resources` to resolve required resource ids. If the target publishes a more specific read-only resource action, use it for app-local identifiers. Do not guess ids from names or copy ids from a different organization/context.
|
|
105
|
+
|
|
106
|
+
For an app-local resource boundary, pass resource context separately from action input:
|
|
85
107
|
|
|
86
108
|
```bash
|
|
87
|
-
topolo
|
|
88
|
-
|
|
109
|
+
topolo actions call <read-action-id> \
|
|
110
|
+
--resource-type <published-resource-type> \
|
|
111
|
+
--resource-id <resolved-resource-id> \
|
|
112
|
+
--data '<schema-valid-json>' --json
|
|
89
113
|
```
|
|
90
114
|
|
|
91
|
-
|
|
115
|
+
Provide `--resource-type` and `--resource-id` together. MCP read actions use `topolo_read_action` with the same separate resource fields.
|
|
92
116
|
|
|
93
|
-
###
|
|
117
|
+
### 4. Validate locally
|
|
118
|
+
|
|
119
|
+
Before every call, validate exact input against the published JSON Schema:
|
|
94
120
|
|
|
95
121
|
```bash
|
|
96
|
-
topolo
|
|
97
|
-
topolo apps get <application_id> --json
|
|
98
|
-
topolo apps requirements <application_id> --json
|
|
99
|
-
topolo apps audit <application_id> --json
|
|
100
|
-
topolo services --json
|
|
101
|
-
topolo actions --service <application_id_or_slug> --json
|
|
122
|
+
topolo actions validate <action-id> --data '<json>' --json
|
|
102
123
|
```
|
|
103
124
|
|
|
104
|
-
|
|
105
|
-
Worker apps that do not expose a callable API. `topolo services` returns every
|
|
106
|
-
SDK-app id with its default and effective URL. In staging, a
|
|
107
|
-
service with `environmentSafe: false` still resolves to a production SDK URL and
|
|
108
|
-
the CLI refuses `topolo api` calls for it until a staging URL override is set.
|
|
109
|
-
`topolo apps requirements` returns the current versioned app-build contract and should be run
|
|
110
|
-
before creating or expanding a Topolo app. `topolo apps audit` returns
|
|
111
|
-
catalog-backed requirement scores and migration work. Run `topolo services`
|
|
112
|
-
before `topolo api` when you are not using a cataloged action.
|
|
125
|
+
MCP: `topolo_validate_action`.
|
|
113
126
|
|
|
114
|
-
|
|
127
|
+
For large inputs, write one schema-valid JSON object to a file and pass it without shell escaping:
|
|
115
128
|
|
|
116
129
|
```bash
|
|
117
|
-
topolo actions --
|
|
118
|
-
topolo actions
|
|
119
|
-
topolo actions call
|
|
120
|
-
--data '{"mailboxId":"primary","subject":"Hello","text":"..."}' \
|
|
121
|
-
--confirm --json
|
|
130
|
+
topolo actions validate <action-id> --data @payload.json --json
|
|
131
|
+
topolo actions plan <action-id> --data @payload.json --json
|
|
132
|
+
topolo actions call <action-id> --data @payload.json --confirm --json
|
|
122
133
|
```
|
|
123
134
|
|
|
124
|
-
|
|
125
|
-
are filtered by the current organization and permissions. An action with
|
|
126
|
-
`authorizationMode: "runtime"` has input-dependent policy; it remains
|
|
127
|
-
discoverable, and the target app authorizes the concrete organization,
|
|
128
|
-
application, and resource on every call. Prefer catalog actions over `topolo
|
|
129
|
-
api` when the target app has published a machine contract through Topolo
|
|
130
|
-
Developers. Writes require `--confirm`.
|
|
135
|
+
Fix every returned path-level validation error. Never add undeclared fields to work around a failure.
|
|
131
136
|
|
|
132
|
-
|
|
133
|
-
separately from the action input. Resource selection is per invocation: it does
|
|
134
|
-
not switch the personal/organization credential context or the browser session.
|
|
137
|
+
### 5. Plan without execution
|
|
135
138
|
|
|
136
139
|
```bash
|
|
137
|
-
topolo actions
|
|
138
|
-
--resource-type workspace \
|
|
139
|
-
--resource-id <workspace_id> \
|
|
140
|
-
--data '{}' --json
|
|
140
|
+
topolo actions plan <action-id> --data '<json>' --json
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
workspaces. For a single default workspace, use the active organization ID from
|
|
145
|
-
`topolo whoami --json` when that app defines its workspace as the organization
|
|
146
|
-
boundary. Always provide `--resource-type` and `--resource-id` together.
|
|
143
|
+
MCP: `topolo_plan_action`.
|
|
147
144
|
|
|
148
|
-
|
|
145
|
+
The plan must be valid and must expose the resolved request, confirmation requirement, verification steps, and rollback when one is published. If `executable` is false, do not call the action.
|
|
149
146
|
|
|
150
|
-
|
|
147
|
+
### 6. Confirm mutations
|
|
151
148
|
|
|
152
|
-
|
|
153
|
-
topolo api <service> <method> <path> [--data '<json>'] [--query key=value] [--confirm]
|
|
154
|
-
```
|
|
149
|
+
For a mutation, show the user:
|
|
155
150
|
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
-
|
|
151
|
+
- exact action id
|
|
152
|
+
- target method and path from the plan
|
|
153
|
+
- exact input and resource context
|
|
154
|
+
- published effects
|
|
155
|
+
- verification steps
|
|
156
|
+
- rollback action, or that no rollback is published
|
|
160
157
|
|
|
161
|
-
|
|
158
|
+
Obtain explicit approval unless the user already gave unambiguous authorization for this exact production work. The CLI’s `--confirm` is a transport guardrail, not user approval.
|
|
162
159
|
|
|
163
|
-
|
|
164
|
-
# Read current user via auth service
|
|
165
|
-
topolo api auth GET /api/me --json
|
|
160
|
+
Automatic read-only actions use no confirmation. Confirmed actions use:
|
|
166
161
|
|
|
167
|
-
|
|
168
|
-
topolo actions call
|
|
169
|
-
--data '{"mailboxId":"primary","subject":"Hello","text":"..."}' \
|
|
170
|
-
--confirm --json
|
|
162
|
+
```bash
|
|
163
|
+
topolo actions call <action-id> --data '<json>' --confirm --json
|
|
171
164
|
```
|
|
172
165
|
|
|
173
|
-
|
|
166
|
+
MCP uses `topolo_read_action` for automatic reads and `topolo_call_action` with `confirm: true` for approved mutations.
|
|
174
167
|
|
|
175
|
-
|
|
168
|
+
### 7. Verify and recover
|
|
176
169
|
|
|
177
|
-
|
|
178
|
-
| --- | --- | --- |
|
|
179
|
-
| 0 | Success | — |
|
|
180
|
-
| 1 | Generic error | Surface message to user |
|
|
181
|
-
| 2 | Auth error (expired, invalid token) | Run `topolo auth login` and retry |
|
|
182
|
-
| 3 | Permission denied | User's role/scopes lack the capability — point them to the Developers console |
|
|
170
|
+
Run every published verification step immediately after execution. Prefer a credential-scoped read action and compare returned ids, versions, status, or URLs to the mutation result.
|
|
183
171
|
|
|
184
|
-
|
|
172
|
+
If verification fails:
|
|
185
173
|
|
|
186
|
-
|
|
174
|
+
1. Preserve the action result and request id.
|
|
175
|
+
2. Read the published error recovery guidance.
|
|
176
|
+
3. Plan the published rollback action before calling it.
|
|
177
|
+
4. Obtain confirmation for the rollback unless prior authorization explicitly covered recovery.
|
|
178
|
+
5. Execute and verify the rollback.
|
|
187
179
|
|
|
188
|
-
|
|
180
|
+
Never improvise a rollback.
|
|
189
181
|
|
|
190
|
-
|
|
191
|
-
2. **Never write credentials to tracked files.** If a user pastes an API key inline, use it via env var for that invocation (`TOPOLO_API_KEY=… topolo whoami`) rather than committing it.
|
|
192
|
-
3. **Cross-org isolation is built-in** — there is no `--org` flag anywhere, and the backend rederives the org from the credential (it ignores org hints in a request body). To act in a different organization, use `topolo context use <org>` to switch the active context — never try to fake one via the passthrough.
|
|
193
|
-
4. Audit headers (`X-Topolo-Client`, `X-Topolo-Request-Id`, optional `X-Topolo-Agent`) are set by the CLI automatically. If `TOPOLO_AGENT_NAME` is already exported in the user's shell, leave it alone — it's the audit trail.
|
|
182
|
+
## Generic public API escape hatch
|
|
194
183
|
|
|
195
|
-
|
|
184
|
+
Only when public docs provide a complete contract:
|
|
196
185
|
|
|
197
|
-
**"Show me the available Mail actions"**
|
|
198
186
|
```bash
|
|
199
|
-
topolo
|
|
200
|
-
|
|
187
|
+
topolo api <service> <GET|POST|PUT|PATCH|DELETE> <path> \
|
|
188
|
+
[--query key=value] [--data '<json>'] [--confirm] --json
|
|
201
189
|
```
|
|
202
190
|
|
|
203
|
-
|
|
204
|
-
```bash
|
|
205
|
-
topolo actions call app_topolo_mail.messages.send \
|
|
206
|
-
--data '{"mailboxId":"primary","subject":"Hello","text":"..."}' \
|
|
207
|
-
--confirm --json
|
|
208
|
-
```
|
|
191
|
+
Discover `<service>` with `topolo services --json`. Mutating methods require explicit approval and `--confirm`. Prefer catalog actions because they carry schemas, effects, verification, and recovery metadata.
|
|
209
192
|
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
topolo whoami --json | jq '.user.permissions'
|
|
213
|
-
```
|
|
193
|
+
## Errors and recovery
|
|
214
194
|
|
|
215
|
-
|
|
216
|
-
```bash
|
|
217
|
-
topolo apps --json
|
|
218
|
-
topolo apps requirements <application_id> --json
|
|
219
|
-
topolo apps audit <application_id> --json
|
|
220
|
-
topolo services --json
|
|
221
|
-
topolo actions --service mail --json
|
|
222
|
-
```
|
|
195
|
+
In JSON mode, branch on `error.code`, not message text:
|
|
223
196
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
197
|
+
- exit 2 / authentication error: run `topolo auth login`, then repeat identity and discovery.
|
|
198
|
+
- exit 3 / permission denied: surface the missing permission or target denial; do not retry or widen access.
|
|
199
|
+
- `invalid_action_input`: correct only the reported schema paths.
|
|
200
|
+
- `confirmation_required`: plan, obtain approval, then retry once with confirmation.
|
|
201
|
+
- unknown action/service: repeat bounded discovery; never substitute a guessed identifier.
|
|
202
|
+
- missing docs/schema/examples/verification/recovery: stop the affected operation and report the missing publication contract.
|
|
229
203
|
|
|
230
|
-
|
|
231
|
-
```bash
|
|
232
|
-
topolo auth logout --env staging
|
|
233
|
-
topolo auth login --env staging
|
|
234
|
-
topolo whoami --env staging --json
|
|
235
|
-
```
|
|
204
|
+
Use `TOPOLO_DEBUG=1` only when diagnostics are needed. Preserve `X-Topolo-Request-Id` from failures for support. Do not expose tokens or API keys in output.
|
|
236
205
|
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
topolo context ls --json
|
|
240
|
-
topolo context use acme --json # or: topolo context use personal
|
|
241
|
-
```
|
|
206
|
+
## Public references
|
|
242
207
|
|
|
243
|
-
|
|
208
|
+
- Documentation: `https://docs.topolo.app`
|
|
209
|
+
- Staging documentation: `https://docs.stg.topolo.us`
|
|
210
|
+
- Developers console: `https://developers.topolo.app`
|
|
211
|
+
- CLI help: `topolo --help` and `topolo <command> --help`
|
|
244
212
|
|
|
245
|
-
|
|
246
|
-
- Register apps / OAuth clients: the Developers console at https://developers.topolo.app
|
|
247
|
-
- CLI source (for an undocumented flag): `packages/topolo-cli/src/commands/` in the topolo-platform monorepo.
|
|
248
|
-
- SDK package: `packages/topolo-sdk/src/` — authoritative client, dynamic action catalog, and shared response contracts.
|
|
213
|
+
These public surfaces and credential-scoped runtime responses are authoritative. Repository access is neither required nor an accepted fallback.
|