Scripts through the API¶
The concept behind tools is explained in Tools.
A script is a program of your own that the model calls as one tool. You upload a small package
once — a Python or Node entrypoint, the files it needs and the arguments it takes — and Core offers
it to the model as script__<name>. When the model calls it, Core runs the package on the turn's
computer, with the model's arguments, and answers what it printed and the files
it wrote. The model never sees your code: it sees a name, a description and an argument schema.
Use a script when the model should reach for a tested program instead of writing the code again on
every turn. "Make a presentation in our house style" becomes one call to script__house_deck with
the slides as arguments, run against the template you uploaded once.
What your key needs¶
- The
toolingcapability, to register and offer scripts. - The
computercapability, to run them. Your organisation must permitcomputer. filesif the model should work on a turn's attachments or if you register files by upload, andartifactsif you want the files a script writes back.
A key does not need the system tool computer under tooling.system_tools. That switch offers
the three shell tools of Computers; without it, a key runs your
scripts and is never handed a shell.
{
"name": "Presentations",
"capabilities": {
"tooling": {},
"computer": {},
"files": {},
"artifacts": {}
}
}
Your deployment must run computers for your organisation. Where it does not, a request naming a
script is refused with "This tool is not configured on this deployment." before the model runs,
and the problem body carries "reason_code": "computer_not_configured".
Register a script¶
POST /scripts creates the script and its version 1:
curl -s -X POST "$CORE_URL/scripts" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d @house_deck.json
{
"name": "house_deck",
"description": "Make a PowerPoint presentation in the company house style. Use it whenever the user asks for slides.",
"entrypoint": ["python3", "main.py"],
"parameters": {
"type": "object",
"properties": {
"theme": { "type": "string", "description": "Theme of the deck" },
"title": { "type": "string", "description": "Title of the first slide" },
"slides": {
"type": "array",
"items": {
"type": "object",
"properties": {
"heading": { "type": "string" },
"bullets": { "type": "array", "items": { "type": "string" } }
},
"required": ["heading"]
}
}
},
"required": ["title", "slides"]
},
"files": [
{ "path": "main.py", "content": "import json, os\nfrom pptx import Presentation\n..." },
{ "path": "template.pptx", "file_id": "file_8Kd2..." }
],
"packages": [
{ "ecosystem": "pypi", "name": "python-pptx", "version": "1.0.2" }
],
"timeout_seconds": 60,
"labels": { "team": "marketing" }
}
| Field | What it holds |
|---|---|
name |
lower-case letters, digits, - and _, at most 48 characters. The model sees script__<name>. The name never changes: policy rules, usage rows and the audit trail record it. Unique within your organisation; a duplicate is 409 |
description |
what the tool does and when the model should use it, at most 1024 characters |
entrypoint |
the program to run, as a list of 1 to 16 arguments, such as ["python3", "main.py"] or ["node", "index.js"]. It is never a shell line: pipes, && and redirections do not work |
parameters |
a JSON Schema object (Draft 2020-12, at most 64 KiB) the model's arguments must satisfy. Lists and nested objects are allowed. Leave it out for a script without arguments |
files |
every file of the package, see Files |
packages |
the approved libraries the package needs, see Packages |
timeout_seconds |
optional: how long one call may run. It can only shorten the calling key's own limit (computer.exec_timeout_seconds); leave it out to take that limit |
labels, metadata |
your classification of the script, which policy rules over tools read, and an opaque document Core stores and never reads. For a request that names a person, the rules over script__<name> also decide whether GET /scripts lists the script and whether it may be given a version or deleted (Registrations) |
Core answers 201 with the script and a Location header. GET /scripts lists your scripts, each
with its latest version's fields and number; GET /scripts/{script_id} answers one.
Files¶
Each entry of files has a path and exactly one of two sources:
content: the file's UTF-8 text, written in the body. Use it for source code.file_id: a file you uploaded first withPOST /files(Files). Use it for binary assets such as a template or a font, and for anything large. Core copies the upload into the version, so deleting the upload later leaves the version whole. Your key needsfilesand must be allowed to read that upload.
A path is relative, such as main.py or lib/deck.py, at most 255 characters, without .., a
backslash or a leading /. A package holds at most 200 files and 20 MiB in total. A request body is
capped at 1 MiB, so put larger files in by file_id.
GET /scripts/{script_id}/versions/{version}/files/{path} answers one file's bytes, with the file's
SHA-256 digest as its ETag.
What the script receives¶
The entrypoint runs in the package's own directory, so main.py finds its own modules and files. It
reads the model's arguments as one JSON object, from standard input or from the file the environment
names:
| Variable | What it names |
|---|---|
ARTIFIC_ARGUMENTS |
the file holding the arguments, the same JSON as on standard input |
ARTIFIC_OUTPUTS |
an empty directory for this call. Every file the script writes directly in it is returned |
ARTIFIC_INPUTS |
/workspace/inputs, where the turn's attachments are, as <file id>/<file name> |
A minimal main.py:
import json
import os
from pptx import Presentation
args = json.load(open(os.environ["ARTIFIC_ARGUMENTS"]))
deck = Presentation("template.pptx")
# ... add a slide per entry of args["slides"] ...
deck.save(os.path.join(os.environ["ARTIFIC_OUTPUTS"], "presentation.pptx"))
print(f"{len(args['slides'])} slides written")
A new version¶
A version never changes once written. To change anything — code, files, description, parameters, packages, timeout — post the whole package again:
curl -s -X POST "$CORE_URL/scripts/$SCRIPT_ID/versions" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d @house_deck_v2.json
The body has the fields of POST /scripts without name. Files you leave out are not in the new
version: a version is one package that was tested together. Core answers 201 with the new version.
When the body is identical to the latest version, file by file, nothing is written and Core answers
200 with the latest. labels and metadata in the body replace the script's; leave them out to
keep them.
GET /scripts/{script_id}/versions lists the versions, newest first, and
GET /scripts/{script_id}/versions/{version} answers one.
An administrator can also save a version as a draft, in the console or
its API. Your key does not see a draft: the script reads as its published version, a turn without
a version runs that one, and pinning the draft's number is refused as a version that does not
exist. A script that has only a draft is no script and no tool for your key until it is published.
DELETE /scripts/{script_id} deletes the script with all its versions and files and answers 204.
While an agent names the script, the delete is 409 and lists those agents; remove the script from
them first.
Offer it to the model¶
Name the script like any other tool:
{
"model": "<a model id>",
"input": "Make a deck of five slides on our Q3 results.",
"tools": [{ "type": "tool", "name": "script__house_deck" }]
}
Without version the turn takes the latest version. Pin a version to keep a turn on code you
tested:
{ "type": "tool", "name": "script__house_deck", "version": 2 }
A version that does not exist is refused, naming the entry's version. The version a turn resolved
is recorded with the response, so the later rounds of a background run
call the same version.
A script also fits everywhere else a tool name does:
- A toolset holds it by name or by pattern, such as
script__*. A toolset stays on the latest version. - An alias gives it a second name and fixes arguments the model never
sees, such as
alias__board_deckwith{"theme": "board"}fixed. An alias can also fix an argument to a file of yours: the script then receives the path of a copy on its computer, under/workspace/.calls/, and the copy is removed when the script returns (Fix an argument to a file). - An agent names it in its
tools.
Your key's policy decides by name: the rules over Tools must permit script__<name>, both
when the tool is offered and again when the model calls it. A key without tooling or computer
that names a script is refused; in a toolset the script is left out for that key. A script cannot be
used in a nested agent turn: like the computer tools, it needs a turn of its own.
GET /tools/script__house_deck answers the script as the model sees it: its name, the latest
version's description and parameters, and its labels.
A script can run in the background. Send background with the script or a new version, as for
an API connector, and the model may start it as a task: it then runs on a computer of its own and
its result comes back to the thread later. See
Run a tool in the background.
What a call does¶
- The arguments are checked first. Arguments that do not satisfy
parametersare answeredTool arguments were not valid., with the path of the first argument at fault and the rule it broke, so the model can correct itself. Nothing runs. - The script runs on the turn's computer. The first computer call of a response makes the computer, with the turn's attachments copied in; every later script or shell call in the same response uses the same machine, one call at a time, in the order the model wrote them. So one script can read what another wrote. Core checks before every call that the package on the computer is exactly the version's, and writes it again when anything was changed.
- The entrypoint runs under one timeout: the shortest of the version's
timeout_seconds, your key'scomputer.exec_timeout_secondsand the deployment's limit. The model cannot lengthen it. -
The model is answered the exit code, standard output and standard error, cut at 32 KiB together, followed by the files the call produced:
exit_code: 0 stdout: 5 slides written stderr: files: presentation.pptx: kept as an artifact -
Each file written directly in
ARTIFIC_OUTPUTSbecomes an artifact of your key, at most 10 per call and 25 MiB each. A file past those limits, a subdirectory, a link, and every file for a key withoutartifactsis listed asleft on the computer. Files written anywhere else stay on the computer until the response is done.
The output is untrusted tool content, like every tool result (Tool-result trust). When a call does not come back on its own terms, the model reads one of Core's fixed sentences:
| What happened | What the model is told | Status of the call |
|---|---|---|
the arguments do not meet parameters |
Tool arguments were not valid. with the path and the rule — no computer is made |
error |
| the script exits with a code other than 0 | its exit code and output, as above | error, executor_failed |
| the timeout passes | what it printed so far, headed timed out after <n> seconds |
error, timeout |
| the version names a package that was revoked | This script needs a package that is no longer approved: <ecosystem> <name> <version>. — no computer is made |
error, executor_failed |
| your key may not reach the region the computer runs in | Denied by policy. — no computer is made |
denied |
the computer would reach the internet and your policy limits where data may go, or your key hides personal data, or your organisation has not switched computer_internet on |
Denied by policy. — no computer is made for the call |
denied |
| the computer could not be made or reached | Computer is unavailable. |
error |
None of these ends the turn; the model can carry on.
Call a script directly¶
Test a script before a model uses it, with the arguments a model would send:
curl -s -X POST "$CORE_URL/tools/script__house_deck/call" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"arguments": {"title": "Q3", "slides": [{"heading": "Revenue"}]}, "version": 2}'
The answer is the tool_call object of Call a tool directly:
status, error_code, text with exactly what the model would read, and the files in outputs,
base64-encoded when your key holds artifacts and they fit. Leave out version to run the latest.
A direct call makes a computer for that one call and removes it when the call returns. That computer is billed to your key like any other, so a trial run costs sandbox time.
No network¶
A script runs without network access: it cannot reach the internet, your own systems or a package index. Everything it needs travels with it: its files, the packages Core installs for it, and the turn's attachments. A script that needs data from elsewhere gets it as an argument or an attachment, or works alongside an API connector the model calls first.
A deployment whose operator offers computers that reach the internet is the exception, for an
organisation that switched computer_internet
on, along with every organisation above it: a script there can connect out and send its arguments
and files anywhere. Do not count on it. Without the switch the script runs with no network, or
gets no computer where the deployment has only open ones. Even with it you get no such computer
when your policy names allowed regions or
vendors, narrows the hosts it may reach, or
requires personal data hidden, or when your key hides personal
data. An organisation that has set allowed hosts
is refused too. A residency exception does not change that; every script call answers
Denied by policy..
Where the deployment runs Core's egress proxy, such a script is told to use it, and the traffic that goes through it is logged -- host, bytes, time -- and, where the deployment configures a rate limit, limited per organisation, as for any computer. Traffic that bypasses the proxy still reaches the internet, and is not logged, not limited, and not checked against the block list or the private-address rule. The proxy's credential is in the script's environment; a script that prints it puts it in its output, and from there in the turn and its trace.
Packages¶
A script can import only libraries that were approved. There are three origins, which
GET /packages lists together, each marked by approval:
approval |
What it is | Who decides |
|---|---|---|
image |
a library the sandbox image already ships, such as python-pptx or pandas (What the sandbox image ships) |
the platform: every release ships one image, and the list follows it |
deployment |
a file your operator approved for every organisation on the deployment | your operator; you read it, you cannot change it. The approver reads as operator |
organisation |
a file your organisation approved for its own scripts | an administrator of your organisation, or a key with management |
Libraries in the image can be imported without naming them. Every other package must be named in
the version's packages, as {ecosystem, name, version} with pypi or npm and the exact version.
When the version is written, Core looks each one up — the deployment's list first, then your
organisation's — and records which approval it resolved to and the approved file's SHA-256 digest:
{ "ecosystem": "pypi", "name": "python-pptx", "version": "1.0.2", "approval": "image", "sha256": null }
A script you installed from a bundle can show a fourth value, bundle: a package only the publisher
approved, which you accepted by installing and which holds for that copy alone
(Packages in a bundle).
Name every package, dependencies included. Core installs the files offline and resolves no
dependencies: a library your package needs, and every library that library needs, must each be
approved and named. A dependency you leave out fails at the script's own import, which a
direct call shows at once. A version names at most 32 packages and 128 MiB
of approved files together. A package no list approves, or one named twice, is refused 422 on
packages.<i>.
Naming an image library is optional, and pins it: if a later image drops it, the version stops running instead of failing on import.
On the first call of a version on a computer, Core copies the approved files into the sandbox and
installs them there; the script finds them on its normal import path (PYTHONPATH, and NODE_PATH
for require; an ES module reaches one through createRequire). An install that fails is answered
like a failing script, with the installer's output, and the entrypoint does not run.
Approve a package for your organisation¶
A key with management approves one file per call. The body is the file itself; name, version and
filename go in the query:
curl -s -X POST \
"$CORE_URL/packages?ecosystem=pypi&name=qrcode&version=8.2&filename=qrcode-8.2-py3-none-any.whl" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/octet-stream" \
--data-binary @qrcode-8.2-py3-none-any.whl
- For PyPI, upload the wheel (
.whl), never a source distribution: the sandbox has no compiler and builds nothing. The wheel must load on the sandbox: a pure-Python wheel (py3-none-any) always does; a compiled one must be built for the image's Python and platform, CPython 3.12 on Linux x86-64 (manylinux, Runtimes). A wheel that does not fit, or whose filename does not carry the name and version you gave, is422onfilename. Test a script locally shows how to try a wheel in the image before you upload it. - For npm, upload the tarball
npm packwrites (.tgz). It is installed without running its install scripts, so a package that needs a build step, such as a native addon, does not work. - A file is at most 32 MiB. Core computes and keeps its SHA-256 digest;
GET /packages/{package_id}answers it. - Your organisation's list holds at most 500 approvals; an approval past that is
409. A name and version already on it is409too; to use another version, approve that version separately. - When an organisation above yours keeps key management to itself, approving and revoking are its
administrators' and your own key is answered
403.
An approval belongs to your organisation alone. No other organisation's scripts can name it.
Revoke a package¶
DELETE /packages/{package_id} revokes one of your organisation's approvals and deletes its file;
it answers 204. A package the deployment approved is 403: only your operator revokes those.
A version is never rewritten, so a version that names a revoked package still names it, and its
next call is refused with This script needs a package that is no longer approved: …. Write a
new version without the package, or with an approved replacement. Approving the very same file
again lets those versions run again; another file under the same name and version does not, because
each version pinned the digest of the file it was tested with. The same holds when your operator
revokes a deployment package or moves to an image without a library a version names.
Share a script¶
A script can go into a publication like a prompt
or a connector. Its files are held beside the publication by digest, packages only your
organisation approved travel with the bundle, and the subscriber needs tooling and computer.
When you withhold the content, the subscriber's copy shows the name, the description and the
parameters but not the entrypoint or the files, and it runs on a computer of its own in each turn.
A linked copy cannot get a new version or be deleted (409); only a forked one can.
Usage and audit¶
- The computer is billed as on Computers: one
sandbox_secondsrecord per computer. Installing packages and writing the package are part of the call and count as computer time. - Each call writes one
tool_invocationusage record namedscript__<name>. When your deployment's price list has a price for that tool name, each call is priced; without one, a call costs only its share of the computer. - The audit trail records
script.created,script.version_created,script.updated(a version body that changed only labels or metadata) andscript.deleted;package.approvedandpackage.revoked; and per callcomputer.executedwith kindscript, the script's id, the version, the duration and the exit code. Arguments, output and file names never reach the audit trail.
The same screens in the console are on Scripts in the console.