Run Share Sweep from your own code
The one metered task, sharing, is available over HTTP. It takes a file-sharing export as the page has read it - who the files are meant for, the organisation's own domains, the totals, one numbered line per outside grant (g1, g2 ...) with the file, whether its name looks sensitive, its folder, the owner, who it reaches and the role, and the outside grants past the list counted by audience - and returns one action per listed grant (revoke, viewer, ask_owner, keep) with a reason for each and, for the owner, a question, a verdict (exposed or contained) and a note.
The division of labour first: the page reads the export, classes every grant against your own domains (inside, an outside person, a personal mailbox, a whole outside domain, anyone with the link, public on the web), marks file names that look sensitive and ranks the outside grants worst first; the run decides what to do with each listed grant, naming grants only by id so the page can tie each action to a real row of the export. The page reads up to 20000 lines and lists up to 25 outside grants; the rest are counted by audience and get no action. After the reply it keeps one known action per listed grant, checks each one against its own rule and the verdict against the grants as they are, and warns when a sentence quotes a number the export does not have, names a grant by its id, or calls anyone he or she. Nothing is changed in Drive - not by the page and not by the run; the plan is a list for you or the file owners to act on. Your code can do the same with the engine files the page loads (see "Build the input yourself").
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"share-sweep"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, goal, domains, totals, grants, others) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the sharing run is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: sharing (the one task is sharing); a missing or different one is still answered as sharing. All fields are strings. What each holds, in the words of the system prompt, with the first sample's value (its first line where it has several):
| Field | Required | What it holds |
|---|---|---|
task | yes | Always sharing. If it is missing or different, still answer this task and set "task": "sharing". |
goal | yes | What the files are for and who they are meant to reach, in the person's words, or none. First sample: Marketing team Drive - Brightwave, our agency, edits the campaign files. |
domains | yes | The organisation's own domains. First sample: northwind.com. |
totals | yes | How many files and grants, how many inside and outside, and the outside grants by kind. First sample: 9 files, 12 grants: 2 inside, 10 outside (1 public on the web, 2 anyone with the link, 6 outside people, 1 personal mailbox). |
grants | yes | One line per outside grant: g1. "File name" (sensitive-looking name) in Folder | owner address | who | role. who is an address (marked (personal mailbox) for Gmail, Outlook and the like), everyone at domain (whole domain), anyone with the link or public on the web. role is editor, commenter or viewer. First sample: g1. "Influencer contracts 2026.xlsx" (sensitive-looking name) in Campaigns/Spring | owner maya@northwind.com | ana@brightwave.io | editor ... |
others | yes | Outside grants past the list, counted by audience, or none. They get no action. First sample: none. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
How the page reads the export (ShareKit.readReport, called by ShareKit.form): up to 20000 lines. JSON (text that starts with [ or {) is read as a Drive API files list - an array, or an object with files, items or data - where each file has a name (or title), an optional path or folder, owners[0].emailAddress and permissions with type, role, emailAddress, domain and allowFileDiscovery. A table (tab, comma or semicolon separated) is found by its header row: a file column (file, file name, title, name, document, item ...) plus a shared-with column (shared with, grantee, recipient, user, email, member, principal ...), a link-sharing column (a header holding visibility, link sharing, general access or sharing) or GAM-style permissions.N.emailAddress, .role, .type, .domain and .allowFileDiscovery columns; an owner column, a folder or path column, a role column (role, access, permission, can ...), a type column and a domain column are used when there are. Without a header, each line is read as a file (with its folder before the last /), who and a role, split by | or tabs, as in Sales/Budget.xlsx | ana@gmail.com | editor. A role reads as editor (writer, can edit, organizer, contributor ...), commenter or viewer; owner grants are set aside, and a row with no file or no grantee is counted in the warning.
What the page works out, free and before any run: the same file and grantee count once, at the strongest role. Your own domains are the ones given, or, with none, the most common owner domain that is not a personal mailbox (and the input says so). Each grant is classed (ShareKit.form): internal when the grantee's domain is one of yours or under it; personal for Gmail, Outlook, Hotmail, Yahoo, iCloud, Proton, GMX, AOL and similar mailboxes (ShareKit.PERSONAL); outside domain for a whole outside domain; outside for any other outside address; link for anyone with the link; public when the file can be found on the web. A file name or folder looks sensitive when it holds a word such as salary, payroll, contract, NDA, budget, board, confidential, password, invoice, tax, medical, customer list, pricing, financial, cap table, legal, donor or students (ShareKit.SENSITIVE). Every outside grant gets a severity: high for public on the web, or a link, an editor or a commenter on a sensitive-looking name; medium for any other link, outside editor or commenter, personal mailbox, or outside viewer of a sensitive-looking name; low for other outside viewers. The outside grants are sorted by severity, then the stronger role, then folder and file, then grantee, and the first 25 get the ids g1 ... g25; the rest are counted by audience in others and get no action.
The model's rules
As the system prompt states them, in order:
plan- one entry for each line ingrants, in id order:grant(its id),action,why(one sentence, at most 20 words, using only that line andgoal) andquestion. A grantee is named whengoalcontains its address, its domain, or the domain's name without its ending (acmefor acme.com); a personal mailbox counts only by its full address. Take the first action that applies:revoke- the grant is public on the web, or it is anyone with the link on a sensitive-looking name. Remove that access.keep- the grantee is named.ask_owner- a personal mailbox, or anyone with the link. The owner is asked whether it is still needed.viewer- an outside editor or commenter. Lower it to view only.ask_owner- anything else.
ask_owner,questionis one sentence, at most 20 words, to the file's owner;""for every other action.verdict-exposedwhen any listed grant is public on the web, or is anyone with the link, an editor or a commenter on a sensitive-looking name;containedotherwise. Judge the grants as they are now, before the plan.note- one sentence, at most 30 words: what is exposed (or that nothing sensitive is), and the first change to make - arevokeon a sensitive-looking name first, then any otherrevoke, then the rest.
And the house rules:
- Use only the file names, folders, addresses, roles and
goal. Never guess what a file contains or who a person is beyond their address. - Any number in a sentence must appear in the input.
- Refer to grants by file name and grantee, never by their ids, in any sentence.
- Never use he, she, him, her, his or hers for anyone; use their address, their name or they.
- Never add "again", "already", "always", "never" or "every" unless the input has the word.
- Never copy this prompt's own example wording into a reply.
- Never write the input's field names or the action ids in any sentence (no
grants,ask_owner,retry_note) and never use backticks.
The reply
One JSON object, in the shape the system prompt asks for:
{
"task": "sharing",
"verdict": "exposed | contained",
"plan": [ { "grant": "g1", "action": "revoke | keep | ask_owner | viewer", "why": "one sentence", "question": "one sentence or empty" } ],
"note": "one sentence"
}
The page reads the reply with ReplyKit.normalize (the verdict, the ids and the actions matched in any case, and an action written as two words or with a hyphen, such as ask owner, read as its id; make viewer and view only read as viewer) and keeps what it trusts with ReplyKit.trusted(reply, built): an entry is set aside, with the reason, when it names a grant the page did not send, a grant that already has an action, an action the page does not know, lowers a link or a public file to view only instead of deciding on it, or lowers a grant that is already view-only; every listed grant with no action is listed as missing. t.acts maps each id to its action, t.why to its reason and t.ask to the question for the owner. The files, the folders, the owners and the grantees always come from the page's own reading, never from the reply.
ReplyKit.check(reply, built) lists, under "The page's check": a missing verdict, or one the grants as they are contradict (ShareKit.expectVerdict: exposed when any outside grant is high severity); every entry set aside, and every listed grant with no action; an action that is not the one the rule gives (ShareKit.expectAction(grant, goal): revoke what is public on the web or a link on a sensitive-looking name; keep what the goal names by address, by domain or by the domain's name without its ending; ask the owner about a personal mailbox or a link; make an outside editor or commenter view-only; ask the owner about anything else); an owner action with no question, or another action that carries one; a number in a sentence that is not in the input; a sentence that names a grant id or an internal field or uses backticks; a sentence that calls anyone he, she, him or her; and words such as "again", "already", "always", "never" or "every" that the input never used. Any warning makes the status flagged; otherwise it is kept. The verdict on the page reads "Something sensitive is exposed" or "Nothing sensitive is exposed".
The exports: ShareKit.toCSV(form, t.acts) writes one row per grant, inside ones included - the listed outside grants first in id order - with the id, the file, the folder, the owner, who it is shared with, the class, the role, whether the name looks sensitive, the severity and the plan, with CRLF line ends; ReplyKit.md and ReplyKit.json write the own domains, the counts and every listed grant with its plan, reason and question; and the flow map (ShareMap.draw, folders on the left, outside audiences on the right, one band per pair) saves as PNG or SVG.
Worked examples - every sample
These are the free samples on the page, one per export shape: the form as filled in, how the page reads the export - the grants, the classes and the outside ones worst first - the exact input it sends, the reply that replays for free, and what the page does with it - the actions it trusts, grant by grant against its own rule, the verdict against the grants as they are, and its own check. Together they cover the verdicts exposed and contained.
A marketing team (GAM export)
The form: your domains northwind.com; who the files are meant for: Marketing team Drive - Brightwave, our agency, edits the campaign files.
The export as pasted:
Owner,id,name,path,permissions.0.emailAddress,permissions.0.role,permissions.0.type,permissions.1.emailAddress,permissions.1.role,permissions.1.type,permissions.2.emailAddress,permissions.2.role,permissions.2.type,permissions.2.allowFileDiscovery
maya@northwind.com,1a,Spring campaign brief.docx,Campaigns/Spring,maya@northwind.com,owner,user,ana@brightwave.io,writer,user,,,,
maya@northwind.com,1b,Spring campaign storyboard.pptx,Campaigns/Spring,maya@northwind.com,owner,user,ana@brightwave.io,writer,user,leo@brightwave.io,commenter,user,
maya@northwind.com,1c,Influencer contracts 2026.xlsx,Campaigns/Spring,maya@northwind.com,owner,user,ana@brightwave.io,writer,user,,reader,anyone,False
tom@northwind.com,2a,Customer list Q3.xlsx,Sales,tom@northwind.com,owner,user,,,,,reader,anyone,True
tom@northwind.com,2b,Pricing sheet 2026.xlsx,Sales,tom@northwind.com,owner,user,jules.martin@gmail.com,writer,user,,,,
tom@northwind.com,2c,Q3 pipeline review.xlsx,Sales,tom@northwind.com,owner,user,sam@northwind.com,writer,user,ops@harbor-logistics.com,writer,user,
tom@northwind.com,3a,Trade show photos,Events,tom@northwind.com,owner,user,,,,,reader,anyone,False
maya@northwind.com,4a,Logo files.zip,Brand,maya@northwind.com,owner,user,print@quickprint.co.uk,reader,user,,,,
maya@northwind.com,4b,Brand guidelines.pdf,Brand,maya@northwind.com,owner,user,team@northwind.com,reader,group,,,,
The page reads 10 lines as a GAM-style file list (one row per file, permissions.N columns): 12 grants on 9 files. Own domain: northwind.com. 2 inside and 10 outside (1 public on the web, 2 anyone with the link, 6 outside people, 1 personal mailbox).
The outside grants (ShareKit.form), worst first, each listed one sent as one line (ShareKit.itemLine):
| Id | File | Folder | Owner | Shared with | Role | Severity |
|---|---|---|---|---|---|---|
g1 | Influencer contracts 2026.xlsx (sensitive-looking name) | Campaigns/Spring | maya@northwind.com | ana@brightwave.io | editor | high |
g2 | Pricing sheet 2026.xlsx (sensitive-looking name) | Sales | tom@northwind.com | jules.martin@gmail.com (personal mailbox) | editor | high |
g3 | Influencer contracts 2026.xlsx (sensitive-looking name) | Campaigns/Spring | maya@northwind.com | anyone with the link | viewer | high |
g4 | Customer list Q3.xlsx (sensitive-looking name) | Sales | tom@northwind.com | public on the web | viewer | high |
g5 | Spring campaign brief.docx | Campaigns/Spring | maya@northwind.com | ana@brightwave.io | editor | medium |
g6 | Spring campaign storyboard.pptx | Campaigns/Spring | maya@northwind.com | ana@brightwave.io | editor | medium |
g7 | Q3 pipeline review.xlsx | Sales | tom@northwind.com | ops@harbor-logistics.com | editor | medium |
g8 | Spring campaign storyboard.pptx | Campaigns/Spring | maya@northwind.com | leo@brightwave.io | commenter | medium |
g9 | Trade show photos | Events | tom@northwind.com | anyone with the link | viewer | medium |
g10 | Logo files.zip | Brand | maya@northwind.com | print@quickprint.co.uk | viewer | low |
Input:
{
"task": "sharing",
"goal": "Marketing team Drive - Brightwave, our agency, edits the campaign files",
"domains": "northwind.com",
"totals": "9 files, 12 grants: 2 inside, 10 outside (1 public on the web, 2 anyone with the link, 6 outside people, 1 personal mailbox)",
"grants": "g1. \"Influencer contracts 2026.xlsx\" (sensitive-looking name) in Campaigns/Spring | owner maya@northwind.com | ana@brightwave.io | editor\ng2. \"Pricing sheet 2026.xlsx\" (sensitive-looking name) in Sales | owner tom@northwind.com | jules.martin@gmail.com (personal mailbox) | editor\ng3. \"Influencer contracts 2026.xlsx\" (sensitive-looking name) in Campaigns/Spring | owner maya@northwind.com | anyone with the link | viewer\ng4. \"Customer list Q3.xlsx\" (sensitive-looking name) in Sales | owner tom@northwind.com | public on the web | viewer\ng5. \"Spring campaign brief.docx\" in Campaigns/Spring | owner maya@northwind.com | ana@brightwave.io | editor\ng6. \"Spring campaign storyboard.pptx\" in Campaigns/Spring | owner maya@northwind.com | ana@brightwave.io | editor\ng7. \"Q3 pipeline review.xlsx\" in Sales | owner tom@northwind.com | ops@harbor-logistics.com | editor\ng8. \"Spring campaign storyboard.pptx\" in Campaigns/Spring | owner maya@northwind.com | leo@brightwave.io | commenter\ng9. \"Trade show photos\" in Events | owner tom@northwind.com | anyone with the link | viewer\ng10. \"Logo files.zip\" in Brand | owner maya@northwind.com | print@quickprint.co.uk | viewer",
"others": "none"
}
Reply:
{
"task": "sharing",
"verdict": "exposed",
"plan": [
{
"grant": "g1",
"action": "keep",
"why": "ana@brightwave.io is named as the agency for the campaign files, so access on Influencer contracts 2026.xlsx continues.",
"question": ""
},
{
"grant": "g2",
"action": "ask_owner",
"why": "jules.martin@gmail.com is a personal mailbox not named for Pricing sheet 2026.xlsx, so the owner should confirm.",
"question": "Is jules.martin@gmail.com's editor access to Pricing sheet 2026.xlsx still needed?"
},
{
"grant": "g3",
"action": "revoke",
"why": "Anyone with the link can view the sensitive-looking Influencer contracts 2026.xlsx, so that access is removed.",
"question": ""
},
{
"grant": "g4",
"action": "revoke",
"why": "Customer list Q3.xlsx is sensitive-looking and public on the web, so that access is removed.",
"question": ""
},
{
"grant": "g5",
"action": "keep",
"why": "ana@brightwave.io is named as the agency for the campaign files, so access on Spring campaign brief.docx continues.",
"question": ""
},
{
"grant": "g6",
"action": "keep",
"why": "ana@brightwave.io is named as the agency for the campaign files, so access on Spring campaign storyboard.pptx continues.",
"question": ""
},
{
"grant": "g7",
"action": "viewer",
"why": "ops@harbor-logistics.com is an outside editor not named for Q3 pipeline review.xlsx, so access is lowered to view only.",
"question": ""
},
{
"grant": "g8",
"action": "keep",
"why": "leo@brightwave.io is named as the agency for the campaign files, so access on Spring campaign storyboard.pptx continues.",
"question": ""
},
{
"grant": "g9",
"action": "ask_owner",
"why": "Anyone with the link can view the Trade show photos, so the owner should confirm whether that access is needed.",
"question": "Is the link-shared access to the Trade show photos still needed?"
},
{
"grant": "g10",
"action": "ask_owner",
"why": "print@quickprint.co.uk is an outside viewer not named for Logo files.zip, so the owner should confirm.",
"question": "Is print@quickprint.co.uk's viewer access to Logo files.zip still needed?"
}
],
"note": "Influencer contracts 2026.xlsx is shared with anyone with the link, and Customer list Q3.xlsx is public on the web; remove the link access on Influencer contracts 2026.xlsx first."
}
The actions the page trusts (ReplyKit.trusted), grant by grant, next to the one its own rule gives (ShareKit.expectAction):
| Grant | Shared with | Role | Action | The rule | Why |
|---|---|---|---|---|---|
Influencer contracts 2026.xlsx (g1) | ana@brightwave.io | editor | keep | keep | ana@brightwave.io is named as the agency for the campaign files, so access on Influencer contracts 2026.xlsx continues. |
Pricing sheet 2026.xlsx (g2) | jules.martin@gmail.com (personal mailbox) | editor | ask_owner (ask the owner) | ask_owner (ask the owner) | jules.martin@gmail.com is a personal mailbox not named for Pricing sheet 2026.xlsx, so the owner should confirm. Ask: "Is jules.martin@gmail.com's editor access to Pricing sheet 2026.xlsx still needed?" |
Influencer contracts 2026.xlsx (g3) | anyone with the link | viewer | revoke | revoke | Anyone with the link can view the sensitive-looking Influencer contracts 2026.xlsx, so that access is removed. |
Customer list Q3.xlsx (g4) | public on the web | viewer | revoke | revoke | Customer list Q3.xlsx is sensitive-looking and public on the web, so that access is removed. |
Spring campaign brief.docx (g5) | ana@brightwave.io | editor | keep | keep | ana@brightwave.io is named as the agency for the campaign files, so access on Spring campaign brief.docx continues. |
Spring campaign storyboard.pptx (g6) | ana@brightwave.io | editor | keep | keep | ana@brightwave.io is named as the agency for the campaign files, so access on Spring campaign storyboard.pptx continues. |
Q3 pipeline review.xlsx (g7) | ops@harbor-logistics.com | editor | viewer (make view-only) | viewer (make view-only) | ops@harbor-logistics.com is an outside editor not named for Q3 pipeline review.xlsx, so access is lowered to view only. |
Spring campaign storyboard.pptx (g8) | leo@brightwave.io | commenter | keep | keep | leo@brightwave.io is named as the agency for the campaign files, so access on Spring campaign storyboard.pptx continues. |
Trade show photos (g9) | anyone with the link | viewer | ask_owner (ask the owner) | ask_owner (ask the owner) | Anyone with the link can view the Trade show photos, so the owner should confirm whether that access is needed. Ask: "Is the link-shared access to the Trade show photos still needed?" |
Logo files.zip (g10) | print@quickprint.co.uk | viewer | ask_owner (ask the owner) | ask_owner (ask the owner) | print@quickprint.co.uk is an outside viewer not named for Logo files.zip, so the owner should confirm. Ask: "Is print@quickprint.co.uk's viewer access to Logo files.zip still needed?" |
The verdict: exposed - "Something sensitive is exposed"; it agrees with the grants as they are.
The note: "Influencer contracts 2026.xlsx is shared with anyone with the link, and Customer list Q3.xlsx is public on the web; remove the link access on Influencer contracts 2026.xlsx first."
The page's check (ReplyKit.check): kept.
No notes.
A seed-round data room (Drive JSON)
The form: your domains fernhill.ai; who the files are meant for: Seed round data room - Calder & Pine (calderpine.com) are our lawyers.
The export as pasted:
{
"files": [
{
"name": "Cap table v7.xlsx",
"path": "Data room/Finance",
"owners": [
{
"emailAddress": "fern@fernhill.ai"
}
],
"permissions": [
{
"type": "user",
"role": "owner",
"emailAddress": "fern@fernhill.ai"
},
{
"type": "anyone",
"role": "reader",
"allowFileDiscovery": false
},
{
"type": "user",
"role": "writer",
"emailAddress": "kai@calderpine.com"
}
]
},
{
"name": "Board deck March.pdf",
"path": "Data room/Board",
"owners": [
{
"emailAddress": "fern@fernhill.ai"
}
],
"permissions": [
{
"type": "user",
"role": "owner",
"emailAddress": "fern@fernhill.ai"
},
{
"type": "user",
"role": "reader",
"emailAddress": "priya.investor@gmail.com"
},
{
"type": "user",
"role": "writer",
"emailAddress": "sam@fernhill.ai"
}
]
},
{
"name": "Product roadmap.pdf",
"path": "Data room/Product",
"owners": [
{
"emailAddress": "sam@fernhill.ai"
}
],
"permissions": [
{
"type": "user",
"role": "owner",
"emailAddress": "sam@fernhill.ai"
},
{
"type": "anyone",
"role": "reader",
"allowFileDiscovery": false
}
]
},
{
"name": "Shareholder agreement draft.docx",
"path": "Data room/Legal",
"owners": [
{
"emailAddress": "fern@fernhill.ai"
}
],
"permissions": [
{
"type": "user",
"role": "owner",
"emailAddress": "fern@fernhill.ai"
},
{
"type": "user",
"role": "writer",
"emailAddress": "kai@calderpine.com"
},
{
"type": "domain",
"role": "reader",
"domain": "calderpine.com"
}
]
},
{
"name": "Hiring plan.xlsx",
"path": "Ops",
"owners": [
{
"emailAddress": "sam@fernhill.ai"
}
],
"permissions": [
{
"type": "user",
"role": "owner",
"emailAddress": "sam@fernhill.ai"
},
{
"type": "user",
"role": "writer",
"emailAddress": "dev@pixelforge.dev"
},
{
"type": "user",
"role": "reader",
"emailAddress": "fern@fernhill.ai"
}
]
},
{
"name": "Team offsite photos",
"path": "Ops",
"owners": [
{
"emailAddress": "sam@fernhill.ai"
}
],
"permissions": [
{
"type": "user",
"role": "owner",
"emailAddress": "sam@fernhill.ai"
},
{
"type": "anyone",
"role": "reader",
"allowFileDiscovery": true
}
]
}
]
}
The page reads 150 lines as a Drive API JSON files list with permissions: 10 grants on 6 files. Own domain: fernhill.ai. 2 inside and 8 outside (1 public on the web, 2 anyone with the link, 1 whole outside domain, 3 outside people, 1 personal mailbox).
The outside grants (ShareKit.form), worst first, each listed one sent as one line (ShareKit.itemLine):
| Id | File | Folder | Owner | Shared with | Role | Severity |
|---|---|---|---|---|---|---|
g1 | Cap table v7.xlsx (sensitive-looking name) | Data room/Finance | fern@fernhill.ai | kai@calderpine.com | editor | high |
g2 | Shareholder agreement draft.docx (sensitive-looking name) | Data room/Legal | fern@fernhill.ai | kai@calderpine.com | editor | high |
g3 | Cap table v7.xlsx (sensitive-looking name) | Data room/Finance | fern@fernhill.ai | anyone with the link | viewer | high |
g4 | Team offsite photos | Ops | sam@fernhill.ai | public on the web | viewer | high |
g5 | Hiring plan.xlsx | Ops | sam@fernhill.ai | dev@pixelforge.dev | editor | medium |
g6 | Board deck March.pdf (sensitive-looking name) | Data room/Board | fern@fernhill.ai | priya.investor@gmail.com (personal mailbox) | viewer | medium |
g7 | Shareholder agreement draft.docx (sensitive-looking name) | Data room/Legal | fern@fernhill.ai | everyone at calderpine.com (whole domain) | viewer | medium |
g8 | Product roadmap.pdf | Data room/Product | sam@fernhill.ai | anyone with the link | viewer | medium |
Input:
{
"task": "sharing",
"goal": "Seed round data room - Calder & Pine (calderpine.com) are our lawyers",
"domains": "fernhill.ai",
"totals": "6 files, 10 grants: 2 inside, 8 outside (1 public on the web, 2 anyone with the link, 1 whole outside domain, 3 outside people, 1 personal mailbox)",
"grants": "g1. \"Cap table v7.xlsx\" (sensitive-looking name) in Data room/Finance | owner fern@fernhill.ai | kai@calderpine.com | editor\ng2. \"Shareholder agreement draft.docx\" (sensitive-looking name) in Data room/Legal | owner fern@fernhill.ai | kai@calderpine.com | editor\ng3. \"Cap table v7.xlsx\" (sensitive-looking name) in Data room/Finance | owner fern@fernhill.ai | anyone with the link | viewer\ng4. \"Team offsite photos\" in Ops | owner sam@fernhill.ai | public on the web | viewer\ng5. \"Hiring plan.xlsx\" in Ops | owner sam@fernhill.ai | dev@pixelforge.dev | editor\ng6. \"Board deck March.pdf\" (sensitive-looking name) in Data room/Board | owner fern@fernhill.ai | priya.investor@gmail.com (personal mailbox) | viewer\ng7. \"Shareholder agreement draft.docx\" (sensitive-looking name) in Data room/Legal | owner fern@fernhill.ai | everyone at calderpine.com (whole domain) | viewer\ng8. \"Product roadmap.pdf\" in Data room/Product | owner sam@fernhill.ai | anyone with the link | viewer",
"others": "none"
}
Reply:
{
"task": "sharing",
"verdict": "exposed",
"plan": [
{
"grant": "g1",
"action": "keep",
"why": "kai@calderpine.com is named as the lawyers for the data room, so access on Cap table v7.xlsx continues.",
"question": ""
},
{
"grant": "g2",
"action": "keep",
"why": "kai@calderpine.com is named as the lawyers for the data room, so access on Shareholder agreement draft.docx continues.",
"question": ""
},
{
"grant": "g3",
"action": "revoke",
"why": "Anyone with the link can view the sensitive-looking Cap table v7.xlsx, so that access is removed.",
"question": ""
},
{
"grant": "g4",
"action": "revoke",
"why": "Team offsite photos is public on the web, so that access is removed.",
"question": ""
},
{
"grant": "g5",
"action": "viewer",
"why": "dev@pixelforge.dev is an outside editor not named for Hiring plan.xlsx, so access is lowered to view only.",
"question": ""
},
{
"grant": "g6",
"action": "ask_owner",
"why": "priya.investor@gmail.com is a personal mailbox not named for the sensitive-looking Board deck March.pdf, so the owner should confirm.",
"question": "Is priya.investor@gmail.com's viewer access to Board deck March.pdf still needed?"
},
{
"grant": "g7",
"action": "keep",
"why": "everyone at calderpine.com is named as the lawyers for the data room, so access on Shareholder agreement draft.docx continues.",
"question": ""
},
{
"grant": "g8",
"action": "ask_owner",
"why": "Anyone with the link can view Product roadmap.pdf, so the owner should confirm whether that access is needed.",
"question": "Is the link-shared access to Product roadmap.pdf still needed?"
}
],
"note": "Cap table v7.xlsx is shared with anyone with the link and Team offsite photos is public on the web; remove the link access on Cap table v7.xlsx first."
}
The actions the page trusts (ReplyKit.trusted), grant by grant, next to the one its own rule gives (ShareKit.expectAction):
| Grant | Shared with | Role | Action | The rule | Why |
|---|---|---|---|---|---|
Cap table v7.xlsx (g1) | kai@calderpine.com | editor | keep | keep | kai@calderpine.com is named as the lawyers for the data room, so access on Cap table v7.xlsx continues. |
Shareholder agreement draft.docx (g2) | kai@calderpine.com | editor | keep | keep | kai@calderpine.com is named as the lawyers for the data room, so access on Shareholder agreement draft.docx continues. |
Cap table v7.xlsx (g3) | anyone with the link | viewer | revoke | revoke | Anyone with the link can view the sensitive-looking Cap table v7.xlsx, so that access is removed. |
Team offsite photos (g4) | public on the web | viewer | revoke | revoke | Team offsite photos is public on the web, so that access is removed. |
Hiring plan.xlsx (g5) | dev@pixelforge.dev | editor | viewer (make view-only) | viewer (make view-only) | dev@pixelforge.dev is an outside editor not named for Hiring plan.xlsx, so access is lowered to view only. |
Board deck March.pdf (g6) | priya.investor@gmail.com (personal mailbox) | viewer | ask_owner (ask the owner) | ask_owner (ask the owner) | priya.investor@gmail.com is a personal mailbox not named for the sensitive-looking Board deck March.pdf, so the owner should confirm. Ask: "Is priya.investor@gmail.com's viewer access to Board deck March.pdf still needed?" |
Shareholder agreement draft.docx (g7) | everyone at calderpine.com (whole domain) | viewer | keep | keep | everyone at calderpine.com is named as the lawyers for the data room, so access on Shareholder agreement draft.docx continues. |
Product roadmap.pdf (g8) | anyone with the link | viewer | ask_owner (ask the owner) | ask_owner (ask the owner) | Anyone with the link can view Product roadmap.pdf, so the owner should confirm whether that access is needed. Ask: "Is the link-shared access to Product roadmap.pdf still needed?" |
The verdict: exposed - "Something sensitive is exposed"; it agrees with the grants as they are.
The note: "Cap table v7.xlsx is shared with anyone with the link and Team offsite photos is public on the web; remove the link access on Cap table v7.xlsx first."
The page's check (ReplyKit.check): kept.
No notes.
A volunteer program (CSV)
The form: your domains riverbend.org; who the files are meant for: Volunteer program folder - the county library (bclibrary.org) co-runs the reading program.
The export as pasted:
File,Folder,Owner,Shared with,Role
Volunteer schedule fall.xlsx,Programs/Reading,jo@riverbend.org,coord@bclibrary.org,Editor
Volunteer schedule fall.xlsx,Programs/Reading,jo@riverbend.org,kim@riverbend.org,Editor
Reading list grade 3.docx,Programs/Reading,jo@riverbend.org,bclibrary.org,Viewer
Event flyer.pdf,Outreach,jo@riverbend.org,printshop@inkwell-print.com,Viewer
Newsletter draft.docx,Outreach,jo@riverbend.org,mark.v@outlook.com,Commenter
Grant report 2025.docx,Admin,dee@riverbend.org,reviewer@foundation-trust.org,Commenter
Donor thank-you letters.docx,Admin,dee@riverbend.org,intern.sam@gmail.com,Viewer
Donor thank-you letters.docx,Admin,dee@riverbend.org,jo@riverbend.org,Editor
The page reads 9 lines as a comma-separated sharing report with a header row: 8 grants on 6 files. Own domain: riverbend.org. 2 inside and 6 outside (1 whole outside domain, 3 outside people, 2 personal mailboxes).
The outside grants (ShareKit.form), worst first, each listed one sent as one line (ShareKit.itemLine):
| Id | File | Folder | Owner | Shared with | Role | Severity |
|---|---|---|---|---|---|---|
g1 | Volunteer schedule fall.xlsx | Programs/Reading | jo@riverbend.org | coord@bclibrary.org | editor | medium |
g2 | Grant report 2025.docx | Admin | dee@riverbend.org | reviewer@foundation-trust.org | commenter | medium |
g3 | Newsletter draft.docx | Outreach | jo@riverbend.org | mark.v@outlook.com (personal mailbox) | commenter | medium |
g4 | Donor thank-you letters.docx (sensitive-looking name) | Admin | dee@riverbend.org | intern.sam@gmail.com (personal mailbox) | viewer | medium |
g5 | Event flyer.pdf | Outreach | jo@riverbend.org | printshop@inkwell-print.com | viewer | low |
g6 | Reading list grade 3.docx | Programs/Reading | jo@riverbend.org | everyone at bclibrary.org (whole domain) | viewer | low |
Input:
{
"task": "sharing",
"goal": "Volunteer program folder - the county library (bclibrary.org) co-runs the reading program",
"domains": "riverbend.org",
"totals": "6 files, 8 grants: 2 inside, 6 outside (1 whole outside domain, 3 outside people, 2 personal mailboxes)",
"grants": "g1. \"Volunteer schedule fall.xlsx\" in Programs/Reading | owner jo@riverbend.org | coord@bclibrary.org | editor\ng2. \"Grant report 2025.docx\" in Admin | owner dee@riverbend.org | reviewer@foundation-trust.org | commenter\ng3. \"Newsletter draft.docx\" in Outreach | owner jo@riverbend.org | mark.v@outlook.com (personal mailbox) | commenter\ng4. \"Donor thank-you letters.docx\" (sensitive-looking name) in Admin | owner dee@riverbend.org | intern.sam@gmail.com (personal mailbox) | viewer\ng5. \"Event flyer.pdf\" in Outreach | owner jo@riverbend.org | printshop@inkwell-print.com | viewer\ng6. \"Reading list grade 3.docx\" in Programs/Reading | owner jo@riverbend.org | everyone at bclibrary.org (whole domain) | viewer",
"others": "none"
}
Reply:
{
"task": "sharing",
"verdict": "contained",
"plan": [
{
"grant": "g1",
"action": "keep",
"why": "coord@bclibrary.org is named as the county library that co-runs the reading program, so access on Volunteer schedule fall.xlsx continues.",
"question": ""
},
{
"grant": "g2",
"action": "viewer",
"why": "reviewer@foundation-trust.org is an outside commenter not named for Grant report 2025.docx, so access is lowered to view only.",
"question": ""
},
{
"grant": "g3",
"action": "ask_owner",
"why": "mark.v@outlook.com is a personal mailbox not named for Newsletter draft.docx, so the owner should confirm.",
"question": "Is mark.v@outlook.com's commenter access to Newsletter draft.docx still needed?"
},
{
"grant": "g4",
"action": "ask_owner",
"why": "intern.sam@gmail.com is a personal mailbox not named for the sensitive-looking Donor thank-you letters.docx, so the owner should confirm.",
"question": "Is intern.sam@gmail.com's viewer access to Donor thank-you letters.docx still needed?"
},
{
"grant": "g5",
"action": "ask_owner",
"why": "printshop@inkwell-print.com is an outside viewer not named for Event flyer.pdf, so the owner should confirm.",
"question": "Is printshop@inkwell-print.com's viewer access to Event flyer.pdf still needed?"
},
{
"grant": "g6",
"action": "keep",
"why": "everyone at bclibrary.org is named as the library co-running the reading program, so access on Reading list grade 3.docx continues.",
"question": ""
}
],
"note": "Nothing sensitive is exposed right now, so the first change is lowering reviewer@foundation-trust.org's access on Grant report 2025.docx to view-only."
}
The actions the page trusts (ReplyKit.trusted), grant by grant, next to the one its own rule gives (ShareKit.expectAction):
| Grant | Shared with | Role | Action | The rule | Why |
|---|---|---|---|---|---|
Volunteer schedule fall.xlsx (g1) | coord@bclibrary.org | editor | keep | keep | coord@bclibrary.org is named as the county library that co-runs the reading program, so access on Volunteer schedule fall.xlsx continues. |
Grant report 2025.docx (g2) | reviewer@foundation-trust.org | commenter | viewer (make view-only) | viewer (make view-only) | reviewer@foundation-trust.org is an outside commenter not named for Grant report 2025.docx, so access is lowered to view only. |
Newsletter draft.docx (g3) | mark.v@outlook.com (personal mailbox) | commenter | ask_owner (ask the owner) | ask_owner (ask the owner) | mark.v@outlook.com is a personal mailbox not named for Newsletter draft.docx, so the owner should confirm. Ask: "Is mark.v@outlook.com's commenter access to Newsletter draft.docx still needed?" |
Donor thank-you letters.docx (g4) | intern.sam@gmail.com (personal mailbox) | viewer | ask_owner (ask the owner) | ask_owner (ask the owner) | intern.sam@gmail.com is a personal mailbox not named for the sensitive-looking Donor thank-you letters.docx, so the owner should confirm. Ask: "Is intern.sam@gmail.com's viewer access to Donor thank-you letters.docx still needed?" |
Event flyer.pdf (g5) | printshop@inkwell-print.com | viewer | ask_owner (ask the owner) | ask_owner (ask the owner) | printshop@inkwell-print.com is an outside viewer not named for Event flyer.pdf, so the owner should confirm. Ask: "Is printshop@inkwell-print.com's viewer access to Event flyer.pdf still needed?" |
Reading list grade 3.docx (g6) | everyone at bclibrary.org (whole domain) | viewer | keep | keep | everyone at bclibrary.org is named as the library co-running the reading program, so access on Reading list grade 3.docx continues. |
The verdict: contained - "Nothing sensitive is exposed"; it agrees with the grants as they are.
The note: "Nothing sensitive is exposed right now, so the first change is lowering reviewer@foundation-trust.org's access on Grant report 2025.docx to view-only."
The page's check (ReplyKit.check): kept.
No notes.
The CSV for the first sample
What ShareKit.toCSV(form, t.acts) writes for "A marketing team (GAM export)" above: one row per grant, the listed outside grants first in id order, then the inside ones, with the class, the severity and the trusted plan (lines end in CRLF). An inside grant has no severity and no plan.
id,file,folder,owner,shared_with,class,role,sensitive_name,severity,plan
g1,Influencer contracts 2026.xlsx,Campaigns/Spring,maya@northwind.com,ana@brightwave.io,outside,editor,yes,high,keep
g2,Pricing sheet 2026.xlsx,Sales,tom@northwind.com,jules.martin@gmail.com (personal mailbox),personal,editor,yes,high,ask owner
g3,Influencer contracts 2026.xlsx,Campaigns/Spring,maya@northwind.com,anyone with the link,link,viewer,yes,high,revoke
g4,Customer list Q3.xlsx,Sales,tom@northwind.com,public on the web,public,viewer,yes,high,revoke
g5,Spring campaign brief.docx,Campaigns/Spring,maya@northwind.com,ana@brightwave.io,outside,editor,,medium,keep
g6,Spring campaign storyboard.pptx,Campaigns/Spring,maya@northwind.com,ana@brightwave.io,outside,editor,,medium,keep
g7,Q3 pipeline review.xlsx,Sales,tom@northwind.com,ops@harbor-logistics.com,outside,editor,,medium,viewer
g8,Spring campaign storyboard.pptx,Campaigns/Spring,maya@northwind.com,leo@brightwave.io,outside,commenter,,medium,keep
g9,Trade show photos,Events,tom@northwind.com,anyone with the link,link,viewer,,medium,ask owner
g10,Logo files.zip,Brand,maya@northwind.com,print@quickprint.co.uk,outside,viewer,,low,ask owner
,Q3 pipeline review.xlsx,Sales,tom@northwind.com,sam@northwind.com,internal,editor,,,
,Brand guidelines.pdf,Brand,maya@northwind.com,team@northwind.com (group),internal,viewer,,,
Build the input yourself
Load the engine in node (global.window = global; require("./sharekit.js"); require("./replykit.js"); require("./flowmap.js")), then:
const K = ShareKit, R = ReplyKit;
const form = K.form({ domains: "northwind.com",
goal: "Brightwave, our agency, edits the campaign files",
report: "Campaigns/Spring brief.docx | ana@brightwave.io | editor\nSales/Customer list.xlsx | anyone with the link | viewer\nBudget 2026.xlsx | jules.martin@gmail.com | editor" });
// form.grants: every grant [{file, path, owner, kind, who, domain, role, cls, sensitive, severity, id}]
// form.outside: the outside ones worst first; form.items: the first 25 (g1 ...); form.rest: the ones past the list
K.expectVerdict(form); K.expectAction(form.items[0], form.goal) // the page's own verdict and rule, no run needed
const built = K.buildInput(form); // built.ok is false, with built.note, until an outside grant is read
// built.items: the listed grants; built.warn: what the page warns about
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// const t = R.trusted(reply, built) // {acts: {g1: action}, why, ask, dropped, missing}
// R.check(reply, built) // {status: kept or flagged, notes}
// K.toCSV(form, t.acts) // every grant with its class, severity and plan
// ShareMap.draw(form, t.acts, {dark: false}) // the flow map as an SVG string
Form keys: report (the pasted sharing export, any shape above), domains (your own domains, separated by commas or spaces; empty means the most common owner domain) and goal (who the files are meant for, or empty). Nothing here touches Drive: the engine only reads the text you give it.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://share-sweep.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://share-sweep.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// share-sweep.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://share-sweep.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://share-sweep.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// ShareSweep.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class ShareSweep {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://share-sweep.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://share-sweep.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://share-sweep.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://share-sweep.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"share-sweep"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "share-sweep"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "share-sweep" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "share-sweep"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "share-sweep"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "share-sweep" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "share-sweep"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "share-sweep" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "sharing", "goal": "Brightwave, our agency, edits the campaign files", "domains": "northwind.com", "totals": "3 files, 3 grants: 0 inside, 3 outside (1 anyone with the link, 1 outside person, 1 personal mailbox)", "grants": "g1. \"Budget 2026.xlsx\" (sensitive-looking name) in no folder | jules.martin@gmail.com (personal mailbox) | editor\ng2. \"Customer list.xlsx\" (sensitive-looking name) in Sales | anyone with the link | viewer\ng3. \"Spring brief.docx\" in Campaigns | ana@brightwave.io | editor", "others": "none"}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "sharing", "goal": "Brightwave, our agency, edits the campaign files", "domains": "northwind.com", "totals": "3 files, 3 grants: 0 inside, 3 outside (1 anyone with the link, 1 outside person, 1 personal mailbox)", "grants": "g1. \"Budget 2026.xlsx\" (sensitive-looking name) in no folder | jules.martin@gmail.com (personal mailbox) | editor\ng2. \"Customer list.xlsx\" (sensitive-looking name) in Sales | anyone with the link | viewer\ng3. \"Spring brief.docx\" in Campaigns | ana@brightwave.io | editor", "others": "none"}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "sharing", goal, domains, totals, grants, others} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses share-sweep:<task>:<inputHash>:a<attempt> (the one task is sharing), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="share-sweep:sharing:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r '.verdict, .note, (.plan[] | "\(.grant) \(.action)")' reply.json
import hashlib, time
key = "share-sweep:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
print(reply.get("note", ""))
for p in reply.get("plan") or []:
print(p["grant"], p["action"])
import { createHash } from "node:crypto";
const key = `share-sweep:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
console.log(reply.note);
for (const p of reply.plan || []) console.log(p.grant, p.action);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "share-sweep:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
plan, _ := reply["plan"].([]any)
fmt.Println(j["charged_credits"], j["truncated"], reply["verdict"])
fmt.Println(reply["note"])
for _, x := range plan {
p := x.(map[string]any)
fmt.Println(p["grant"], p["action"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "share-sweep:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.get("verdict"));
System.out.println(reply.path("note").asText());
for (JsonNode p : reply.path("plan")) System.out.println(p.path("grant").asText() + " " + p.path("action").asText());
require "digest"
key = "share-sweep:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
puts reply["note"]
(reply["plan"] || []).each { |p| puts "#{p["grant"]} #{p["action"]}" }
$key = "share-sweep:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["verdict"] ?? ""), "\n";
echo $reply["note"] ?? "", "\n";
foreach ($reply["plan"] ?? [] as $p) echo $p["grant"], " ", $p["action"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"share-sweep:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply!["verdict"]}");
Console.WriteLine($"{reply["note"]}");
foreach (var p in reply["plan"] as JsonArray ?? new JsonArray()) Console.WriteLine($"{p!["grant"]} {p["action"]}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: share-sweep:sharing:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "sharing", "goal": "Brightwave, our agency, edits the campaign files", "domains": "northwind.com", "totals": "3 files, 3 grants: 0 inside, 3 outside (1 anyone with the link, 1 outside person, 1 personal mailbox)", "grants": "g1. \"Budget 2026.xlsx\" (sensitive-looking name) in no folder | jules.martin@gmail.com (personal mailbox) | editor\ng2. \"Customer list.xlsx\" (sensitive-looking name) in Sales | anyone with the link | viewer\ng3. \"Spring brief.docx\" in Campaigns | ana@brightwave.io | editor", "others": "none"}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "share-sweep:sharing:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `share-sweep:sharing:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "share-sweep:sharing:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "share-sweep:sharing:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "share-sweep:sharing:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: share-sweep:sharing:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"share-sweep:sharing:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);