Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,22 @@ gkit --profile my-app gsc api call \
Artifacts use no-replace behavior by default. Choose a new output path for a
later run, or add `--force` only after reviewing the existing destination.
DataForSEO operations that can spend money additionally require both
`--allow-spend` and an explicit `--max-spend-usd` limit.
`--allow-spend` and an explicit `--max-spend-usd` limit. Operations that change
provider state, such as `gsc.sitemaps.submit`, require `--allow-write`; without
it gkit fails with `EFFECT_NOT_ALLOWED` before any request:

```bash
gkit --profile my-app gsc api call \
--operation-id gsc.sitemaps.submit \
--input '{"feedpath":"https://example.com/sitemap.xml"}' \
--allow-write \
--out ./gsc-sitemap-submit.json
```

Sitemap submission needs Full or Owner permission for the service account in
Search Console; a Restricted user receives `AUTH_FAILED`. Google returns an
empty body, so the artifact is a gkit receipt with `siteUrl`, `feedpath`, and
`httpStatus`.

### HubSpot read-only example

Expand Down
45 changes: 44 additions & 1 deletion packages/gkit/docs/providers/gsc/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: >
Generated, searchable documentation for the reviewed gsc operations
that gkit is allowed to route and execute.
provider: gsc
manifestRevision: 2026-07-14.slice4.gsc.2
manifestRevision: 2026-10-21.gsc.3
---

# gsc reviewed executable capabilities
Expand Down Expand Up @@ -220,6 +220,49 @@ List sitemaps for one Search Console property.
gkit --profile openclaw-web gsc api call --operation-id gsc.sitemaps.list --input '{}' --out gsc-sitemaps.json --dry-run
```

## gsc.sitemaps.submit

Submit or resubmit one exact sitemap URL for one Search Console property. Changes provider state, so it requires --allow-write, and the service account needs Full or Owner permission on the property; a Restricted user is rejected with AUTH_FAILED. Google returns an empty body, so the artifact is a gkit submission receipt with siteUrl, feedpath, and httpStatus.

- Provider: `gsc`
- Adapter key: `sitemaps.submit`
- Capability revision: `1`
- Effects: `write`

### Input schema

```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": [
"feedpath"
],
"properties": {
"siteUrl": {
"type": "string",
"minLength": 1,
"maxLength": 2048
},
"feedpath": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"pattern": "^[Hh][Tt][Tt][Pp][Ss]?://[^\\s/?#]+"
}
}
}
```

### Invocation

#### Submit the openclaw-web sitemap index

```bash
gkit --profile openclaw-web gsc api call --operation-id gsc.sitemaps.submit --input @request.json --allow-write --out gsc-sitemap-submit.json --dry-run
```

## gsc.url-inspection.inspect

Read Google's indexed URL inspection result for one URL under the selected property.
Expand Down
6 changes: 3 additions & 3 deletions packages/gkit/docs/providers/gsc/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ title: Google Search Console operation inventory
description: >
Generated inventory of pinned Google Search Console operations and their gkit exposure decisions.
provider: gsc
inventoryRevision: 2026-07-14.slice4.gsc.2
inventoryRevision: 2026-10-21.gsc.3
---

# Google Search Console operation inventory

This pinned inventory contains 10 operations: 5 executable and 5 inventory-only.
This pinned inventory contains 10 operations: 6 executable and 4 inventory-only.
Inventory-only operations cannot be routed by `gkit gsc api call`.

| Method | Path | Operation ID | Exposure | Decision |
Expand All @@ -23,4 +23,4 @@ Inventory-only operations cannot be routed by `gkit gsc api call`.
| `GET` | `/webmasters/v3/sites/{siteUrl}/sitemaps` | `sitemaps.list` | `executable` | Reviewed adapter, input, effect, and response contracts are committed.; capability: `gsc.sitemaps.list` |
| `DELETE` | `/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}` | `sitemaps.delete` | `inventory` | Destructive operation is outside Slice 4. |
| `GET` | `/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}` | `sitemaps.get` | `executable` | Reviewed adapter, input, effect, and response contracts are committed.; capability: `gsc.sitemaps.get` |
| `PUT` | `/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}` | `sitemaps.submit` | `inventory` | Write operation is outside Slice 4. |
| `PUT` | `/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}` | `sitemaps.submit` | `executable` | Reviewed adapter, input, effect, and response contracts are committed.; capability: `gsc.sitemaps.submit` |
7 changes: 4 additions & 3 deletions packages/gkit/generated/gsc/inventory.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": 1,
"provider": "gsc",
"revision": "2026-07-14.slice4.gsc.2",
"revision": "2026-10-21.gsc.3",
"source": {
"url": "https://searchconsole.googleapis.com/$discovery/rest?version=v1",
"revision": "2026-07-14-reviewed-contract",
Expand Down Expand Up @@ -80,8 +80,9 @@
"path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}",
"method": "put",
"operationId": "sitemaps.submit",
"exposure": "inventory",
"reason": "Write operation is outside Slice 4."
"exposure": "executable",
"capabilityId": "gsc.sitemaps.submit",
"reason": "Reviewed adapter, input, effect, and response contracts are committed."
}
]
}
45 changes: 43 additions & 2 deletions packages/gkit/generated/gsc/manifest.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"version": 1,
"provider": "gsc",
"revision": "2026-07-14.slice4.gsc.2",
"revision": "2026-10-21.gsc.3",
"source": {
"url": "https://searchconsole.googleapis.com/$discovery/rest?version=v1",
"revision": "2026-07-14-reviewed-contract",
"checksum": "sha256:5b434b16085508148e7fb862070aa24bc6036fae3858f5970376f5e572cc95e0"
},
"reviewedAt": "2026-07-14T06:10:00.000Z",
"reviewedAt": "2026-10-21T00:00:00.000Z",
"capabilities": [
{
"id": "gsc.properties.list",
Expand Down Expand Up @@ -212,6 +212,47 @@
}
]
},
{
"id": "gsc.sitemaps.submit",
"provider": "gsc",
"revision": "1",
"adapterKey": "sitemaps.submit",
"title": "Submit one sitemap to Search Console",
"description": "Submit or resubmit one exact sitemap URL for one Search Console property. Changes provider state, so it requires --allow-write, and the service account needs Full or Owner permission on the property; a Restricted user is rejected with AUTH_FAILED. Google returns an empty body, so the artifact is a gkit submission receipt with siteUrl, feedpath, and httpStatus.",
"effects": [
"write"
],
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": [
"feedpath"
],
"properties": {
"siteUrl": {
"type": "string",
"minLength": 1,
"maxLength": 2048
},
"feedpath": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"pattern": "^[Hh][Tt][Tt][Pp][Ss]?://[^\\s/?#]+"
}
}
},
"examples": [
{
"title": "Submit the openclaw-web sitemap index",
"input": {
"feedpath": "https://openclawai.io/sitemap-index.xml"
},
"command": "gkit --profile openclaw-web gsc api call --operation-id gsc.sitemaps.submit --input @request.json --allow-write --out gsc-sitemap-submit.json --dry-run"
}
]
},
{
"id": "gsc.url-inspection.inspect",
"provider": "gsc",
Expand Down
2 changes: 1 addition & 1 deletion packages/gkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gkit",
"version": "0.2.0",
"version": "0.2.1",
"private": true,
"description": "Profile-bound, agent-first growth provider CLI.",
"bin": {
Expand Down
38 changes: 34 additions & 4 deletions packages/gkit/policy/gsc.reviewed.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"version": 1,
"provider": "gsc",
"manifestRevision": "2026-07-14.slice4.gsc.2",
"reviewedAt": "2026-07-14T06:10:00.000Z",
"manifestRevision": "2026-10-21.gsc.3",
"reviewedAt": "2026-10-21T00:00:00.000Z",
"entries": [
{
"path": "/webmasters/v3/sites",
Expand Down Expand Up @@ -173,8 +173,38 @@
"path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}",
"method": "put",
"operationId": "sitemaps.submit",
"exposure": "inventory",
"reason": "Write operation is outside Slice 4."
"exposure": "executable",
"capability": {
"id": "gsc.sitemaps.submit",
"provider": "gsc",
"revision": "1",
"adapterKey": "sitemaps.submit",
"title": "Submit one sitemap to Search Console",
"description": "Submit or resubmit one exact sitemap URL for one Search Console property. Changes provider state, so it requires --allow-write, and the service account needs Full or Owner permission on the property; a Restricted user is rejected with AUTH_FAILED. Google returns an empty body, so the artifact is a gkit submission receipt with siteUrl, feedpath, and httpStatus.",
"effects": ["write"],
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": ["feedpath"],
"properties": {
"siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 },
"feedpath": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"pattern": "^[Hh][Tt][Tt][Pp][Ss]?://[^\\s/?#]+"
}
}
},
"examples": [
{
"title": "Submit the openclaw-web sitemap index",
"input": { "feedpath": "https://openclawai.io/sitemap-index.xml" },
"command": "gkit --profile openclaw-web gsc api call --operation-id gsc.sitemaps.submit --input @request.json --allow-write --out gsc-sitemap-submit.json --dry-run"
}
]
}
},
{
"path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}",
Expand Down
2 changes: 1 addition & 1 deletion packages/gkit/scripts/generate-contract-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const packageRoot = new URL("..", import.meta.url).pathname;

describe.each([
["bing", 17, "bing.sites.list"],
["gsc", 5, "gsc.properties.list"],
["gsc", 6, "gsc.properties.list"],
] as const)("%s contract generator", (provider, expectedCount, expectedCapability) => {
it("is deterministic and binds reviewed capabilities to the pinned source", async () => {
const first = await generateContractProviderArtifacts({ packageRoot, provider });
Expand Down
32 changes: 32 additions & 0 deletions packages/gkit/src/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,45 @@ describe("gkit argv parser", () => {
profileFlag: "app-a",
operationId,
input: "{}",
...(provider === "gsc" ? { allowWrite: false } : {}),
out: "result.json",
force: false,
dryRun: true,
});
},
);

it("parses --allow-write for GSC calls so the effect gate receives it", () => {
expect(
parseArgs([
"--profile",
"app-a",
"gsc",
"api",
"call",
"--operation-id",
"gsc.sitemaps.submit",
"--input",
"{}",
"--allow-write",
"--out",
"receipt.json",
]),
).toMatchObject({ kind: "gsc-call", operationId: "gsc.sitemaps.submit", allowWrite: true });
expect(() =>
parseArgs(["gsc", "api", "call", "--operation-id", "x", "--input", "{}", "--allow-write=yes"]),
).toThrow("does not accept a value");
});

it.each(["bing", "hubspot", "dataforseo"] as const)(
"rejects --allow-write for %s, which has no write capabilities",
(provider) => {
expect(() =>
parseArgs([provider, "api", "call", "--operation-id", "x", "--input", "{}", "--allow-write"]),
).toThrow("Unknown flag: --allow-write");
},
);

it("rejects unknown or duplicate flags before execution", () => {
expect(() =>
parseArgs([
Expand Down
28 changes: 24 additions & 4 deletions packages/gkit/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export type ParsedCommand =
profileFlag: string | null;
operationId: string;
input: string;
allowWrite: boolean;
out: string | null;
force: boolean;
dryRun: boolean;
Expand Down Expand Up @@ -264,8 +265,28 @@ export function parseArgs(argv: string[]): ParsedCommand {
invalid(`Expected ${provider} doctor or ${provider} api call.`);
}

const booleanNames = new Set(["--allow-spend", "--force", "--dry-run"]);
const booleanNames = new Set(["--allow-spend", "--allow-write", "--force", "--dry-run"]);
const flags = parseFlags(rest.slice(3), booleanNames);
if (provider === "gsc") {
requireOnly(flags, [
"--operation-id",
"--input",
"--allow-write",
"--out",
"--force",
"--dry-run",
]);
return {
kind: "gsc-call",
profileFlag,
operationId: requiredString(flags, "--operation-id"),
input: requiredString(flags, "--input"),
allowWrite: flags.get("--allow-write") === true,
out: optionalString(flags, "--out"),
force: flags.get("--force") === true,
dryRun: flags.get("--dry-run") === true,
};
}
if (provider !== "dataforseo") {
requireOnly(flags, ["--operation-id", "--input", "--out", "--force", "--dry-run"]);
return {
Expand All @@ -276,9 +297,7 @@ export function parseArgs(argv: string[]): ParsedCommand {
? "hubspot-call"
: provider === "google-ads"
? "google-ads-call"
: provider === "gsc"
? "gsc-call"
: "bing-call",
: "bing-call",
profileFlag,
operationId: requiredString(flags, "--operation-id"),
input: requiredString(flags, "--input"),
Expand Down Expand Up @@ -344,6 +363,7 @@ export function renderHelp(): string {
" gkit --profile <app> gsc doctor",
" gkit --profile <app> gsc api call --operation-id <id> --input @request.json --out <path> --dry-run",
" gkit --profile <app> gsc api call --operation-id <id> --input @request.json --out <path>",
" Write capabilities (gsc.sitemaps.submit) also require --allow-write, including --dry-run.",
"",
"HubSpot:",
" gkit --profile <app> hubspot doctor",
Expand Down
Loading
Loading