@innerskylabs/safety
v0.0.1
Published
Check text and images against named safety policies.
Readme
@innerskylabs/safety
Check text and images against named safety policies.
npm install @innerskylabs/safetyNode 18+. Ships ESM and CJS with full types. Every call is plain fetch, and
there are no other dependencies.
The surface is deliberately small: save/get/list/remove on policies, and
checkText/checkImage on the client. That is all.
60-second tour
Write a policy once, offline — a setup script, an admin panel, CI. Anywhere with an API key, run once, not per request:
import { Policies } from '@innerskylabs/safety';
const creds = { apiKey: process.env.SAFETY_API_KEY! };
// A policy SET is one string. Write it however you like — a numbered list, a
// paragraph — and the service works out the distinct rules in it.
await new Policies(creds).save('uploads', `
1) No threats of violence, weapons, or firearms — shown, described, or implied.
2) No sharing of personal phone numbers or home addresses.
`);
// One rule needs no numbering.
await new Policies(creds).save('comments', 'Block harassment and threats.');Sets never interact. A check names one set and is judged against that set alone,
so uploads and comments can disagree with each other freely — only rules
within one set have to agree.
The key is the whole credential. A key belongs to one project, and the
project is what scopes everything it can reach: its policy sets and its rate limit.
So uploads in your staging project and uploads in production
are two different sets, and you point a service at one or the other by changing
its key — there is no second id to keep in step.
Then check either input type against it, online, as many times as you want:
import { SafetyClient } from '@innerskylabs/safety';
const client = new SafetyClient(creds);
const text = await client.checkText('I am going to find him and beat him', 'uploads');
text.verdict; // 'block' | 'pass'
const image = await client.checkImage('./loading-dock.jpg', 'uploads');
image.verdict; // 'block' | 'pass' | 'unknown'The shape of it
checkText(text, policySet) and checkImage(image, policySet) take their
arguments in the same order and return the same verdict field. Pass
{ showReason: true } as a third argument to get a reason too; on text that
changes how some checks are judged, so it's off by default.
policies.save() upserts, so there is no create-or-update branch to write. It
waits while the service splits, checks and compiles the set (tens of seconds),
and throws if that fails or takes over five minutes. It resolves with
{ savedForTextUse, savedForImageUse }: whether text and image checks can use
the set. The set is prepared for each kind of check separately, so one can fail
while the other works; a check of a kind that failed throws, saying why.
Credentials go to a constructor once rather than to every call. By default the
client talks to https://api.safety.innerskylabs.ai; override with baseUrl on
Credentials, or set SAFETY_BASE_URL for every instance in one process.
Every check names a saved policy. There is no free-text question mode, and that is measured rather than tidy: the ad-hoc route picks a polarity per input and answers YES measurably more often than a compiled policy does on the same content. One route means every verdict is produced the same way and traces back to prose somebody reviewed.
Reading a result
import { isSafe } from '@innerskylabs/safety';
if (!isSafe(result)) await reject();isSafe returns true only for pass. unknown reads as unsafe, and so does
any verdict a future deployment adds.
A result carries two fields and no more — verdict and reason.
The route it took, the model calls it made and the facts it asked never change
what a caller does, so they aren't on it.
Saving a set
Whatever you write is saved as you wrote it. Nothing lints it and nothing
rewrites it. A save can be refused, with an Error saying why — for the rules contradicting each other (one allowing what another blocks), nothing in it being a rule about content, too many rules or conditions, the text or name being too long, another save of it still in progress, or the save taking over five minutes. The one worth knowing about in advance is two rules in the set that
contradict each other — one allowing what another blocks:
try {
await policies.save('house-rules',
'1) Block content about firearms. 2) Allow content about firearms.');
} catch (err) {
console.error(err.message); // names the two rules
}It catches more than exact opposites: "block religion" plus "allow questions about Jesus" is a contradiction too, because every Jesus question is religion.
Your string is split into its distinct rules by a model before anything compares them, so a paragraph works as well as a numbered list — "No weapons. This includes replicas." stays one rule, while "No weapons. No doxxing." becomes two. Numbering is still the clearest way to say what you mean, and it is honoured exactly when you use it.
That refusal is why no check ever returns a contradiction verdict — a set that argues with itself cannot get into the system in the first place.
Saving does real work, so it is not a cheap call. Run it from a deploy script or an admin tool, not per request.
Errors
Every failure is a plain Error whose message says what went wrong — the
service's own description when it answered, or why it could not be reached.
catch (err) {
logger.error(err.message);
}A public upload endpoint
A browser must never hold an API key. The page calls your endpoint, and your server calls this SDK:
app.post('/api/check-upload', upload.single('file'), async (req, res) => {
const result = await client.checkImage(req.file.buffer, 'upload-content-standards');
res.json({ allowed: isSafe(result) });
});Notes on what is not here
No profanity check. The design doc sketched one as a local word list. It was a demonstration of a shape rather than a usable filter, and it is not part of the SDK's job: the SDK checks content against policies you wrote.
No Policies.lint() and no preview(). Whatever you write is saved as you
wrote it. How the service prepares a set to judge with is an implementation
detail — there is nothing a caller can act on there, and putting it on the wire
would be a promise to keep it stable.
No client.report(). It was surface nothing called.
No isUserDefined on a policy set. Every set is one you wrote, so the flag
was true on every row it could ever appear on. A field with one possible value
tells a caller nothing and is one more thing to keep in step.
