A script is personal: its owner and admins, with admin ownership transfer (#1404) - #1409
Merged
Conversation
…sfer (#1404) A script belongs to one person. Its owner sees it, edits it, runs it, and schedules it; administrators do all four on every script; nobody else is told it exists. Script.OwnedBy is the one definition, and every surface answers through it: the manage_script read and edit paths, the store's list and search predicates, the portal routes, search, fetch, and a prompt's reference. A name is unique within its owner, so every lookup by name names an owner too. An administrator can move a script to another owner from the script's page in the portal (PUT /api/v1/portal/scripts/{id}/owner). Ownership is the whole of what a script is, so the transfer hands over what its owner sees, edits, runs, and schedules, and its history with them. The move is recorded as a new version authored by the administrator making it, which is what re-captures the authority a run presents: from the transfer on, a run carries that administrator's roles, so moving a script to an administrator is how it comes to run with an administrator's reach. A name the receiving owner already keeps refuses the transfer. Every attempt, admitted or refused, is written to the audit log as a script_transfer_owner event of kind admin naming the script and both ends of the move. A prompt may reference any script, and the reference resolves for the script's owner alone. The response to attach_script states who it will resolve for whenever the prompt serves anybody else, so an author learns it where they can act on it rather than from a reader who received less than the prompt reads. A caller may only reference a script they can see. The promotion gate no longer consults referenced scripts, because a reference cannot be narrower than the prompt carrying it. Out: the scope field and its validation, the persona list, the visibility rules built on them, the "Visible to" surface on the administrator's listing, and the Owner column on the owner's listing, where every script is the reader's own. The delete refusal that pointed at deprecation is retired with the audience it protected. Migration 000119 renames a name two rows now collide on, replaces the two partial name indexes with one over (owner_email, name), and drops the columns. A script whose owner could not be established belongs to nobody: it is visible only to administrators, and the transfer is how it gets an owner.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1409 +/- ##
==========================================
- Coverage 90.98% 90.98% -0.01%
==========================================
Files 686 689 +3
Lines 69082 69044 -38
==========================================
- Hits 62852 62817 -35
+ Misses 4077 4075 -2
+ Partials 2153 2152 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1404
A script belongs to one person
Its owner sees it, edits it, runs it, and schedules it. Administrators do all four on every script. Nobody else is told it exists.
Script.OwnedBy(pkg/script/script.go) is the one definition, and every surface answers through it rather than keeping its own: themanage_scriptread and edit paths (internal/platform/scriptlayer/scriptlayer.go), the store's list and search predicates (internal/platform/scriptstore/scriptstore.go,search.go), the portal routes (internal/httpserver/scripthttp/portal.go),searchandfetch(pkg/knowledge/provider_scripts.go), and a prompt's reference (pkg/prompt/attachserve/scripts.go). Both sides of the comparison must be identified, so a script whose owner could not be established belongs to nobody rather than to every caller the platform cannot name either.A name is unique within its owner, so every lookup by name names an owner too:
Store.Get(name)andStore.GetPersonal(owner, name)collapse into oneStore.GetByName(owner, name). Two analysts may each keep adaily-sales, and an administrator addressing somebody else's script by name passesowner_email.knowledge.ScriptsProvidermoves fromScopeSharedtoScopePerUser, so the router skips it entirely for a caller carrying no identity instead of querying a source that can return nothing.An administrator can move a script to another owner
PUT /api/v1/portal/scripts/{id}/owner, on the script page administrators and owners already share, rendered for administrators alone (internal/httpserver/scripthttp/portalowner.go,ui/src/pages/scripts/ScriptOwnerTransfer.tsx).Ownership is the whole of what a script is, so a transfer hands over what its owner sees, edits, runs, and schedules, and the script's history with them: the new owner reads the run records and dry-run accounts the previous owner produced, whose logs are free text those runs printed. That is why the action is an administrator's and not something an owner can do to hand their work along.
The move is recorded as a new applied version authored by the administrator making it. That version is the mechanism, not bookkeeping: a run presents the roles captured on the version it executes (#1403), so writing one is what re-captures the authority.
flowchart LR subgraph before["Before the transfer"] V2["version 2<br/>author: jane<br/>author_roles: analyst"] --> R1["a run presents<br/>analyst"] end subgraph after["After an administrator transfers it"] V3["version 3<br/>author: the administrator<br/>author_roles: admin"] --> R2["a run presents<br/>admin"] end before -->|"same source, new owner"| afterMoving a script to an administrator is therefore how it comes to run with an administrator's reach, which is the ticket's named use.
A name the receiving owner already keeps refuses the transfer with
409andscript.ErrNameTaken; names are unique within an owner, so the receiving side decides whether the move is possible. Every attempt, admitted or refused, is written to the audit log as ascript_transfer_ownerevent of kindadminnaming the script and both ends of the move. This is the first producer ofaudit.EventTypeAdmin, which until now existed only as a constant and an OpenAPI enum value.The control names both ends of the move and asks for confirmation before it acts, since the person losing the script is the half an administrator can overlook. It is captured at
docs/images/screenshots/light/admin-admin-script-owner-light.webpand its dark pair, and referenced from the administrator portal guide.A prompt reference resolves for the script's owner
Any prompt may reference a script. The
attach_scriptresponse carries anaudience_notewhenever the prompt serves somebody other than the script's owner, so the author learns what their readers will receive at the moment they make the reference rather than from a reader who received less than the prompt reads (attachserve.AudienceNote). A caller may only reference a script they can see; referencing somebody else's is refused with the shared attachment sentinel so every surface passes the sentence through verbatim.At use, the existing behaviour stands: a caller who does not own a referenced script receives the prompt with a note that part of its automation was unavailable, never the script's name or parameters.
The promotion gate no longer consults referenced scripts (
internal/platform/promptlayer/attachbind). A reference resolves for one person at every prompt scope, so widening a prompt cannot outrun it, andprompt.AttachKindScriptis retired with the rule that used it.What comes out
Scope,Personas,ValidateScope,scopeVisible, andscopeVisibleToAnyfrompkg/script;ListFilter.Scope,Personas,VisibleTo, andVisiblePersona;SearchQuery.Personas;Contract.ScopeandContract.Personas.scopeandpersonasarguments and response fields onmanage_script.nobodyfor an ownerless script.prompt.AttachKindScriptand the script arm of the attachment promotion gate.Migration 000119
Two rows that were legal apart can collide under the new index: a global
daily-salesand its author's own personaldaily-sales. The migration renames every row but the oldest of each group, suffixing it with the head of its id, then replaces the two partial name indexes with one unique index over(owner_email, name)and drops the columns.A row whose
owner_emailis empty keeps it and so belongs to nobody: visible only to administrators, and the transfer is how it gets an owner.Surfaces
manage_scriptscope/personasgone;listreturns the caller's own scripts, or every script for an administratorrun_scriptPUT /api/v1/portal/scripts/{id}/ownerWhere this differs from the ticket
The ticket lists
ScriptFacetBadges.tsxamong the surfaces to remove. That component renders the category and tags a script is filed under (#1369) and never rendered scope, so it stays; the scope renderers that did (audience()on the administrator's listing,visibility()on the detail page) are the ones removed.Two rules the ticket did not mention became false and are retired rather than carried forward.
manage_script deleterefused a shared script in favour of deprecating it, because it might be executing on a schedule for somebody else; with one owner there is nobody else, so a delete takes the script's schedule and history with it. The separate owner gates in front of the run and schedule commands became unreachable once the read rule is owner-or-admin, so they are gone and a non-owner now readsnot foundrather thanonly the owner of a script can read its runs.Verification
make verifypasses end to end. Each gate was also run on its own command against this tree:make test-realdbandmake migrate-check(pg16 and pg17). The migration gate gainsTestMigrationsAgainstRealPostgres_PersonalScriptCollisions, which migrates to 118, writes the rows only the old rule allowed, applies 119, and asserts the rename, the surviving name, the new index, and the dropped columns. It carries the gate's name prefix somake migrate-checkruns it with no target change.golangci-lint run --new-from-rev=<merge-base>: 0 issues.gosec,semgrep, CodeQL, dead-code: clean.npm run test(1332),tsc --noEmit,npm run lint, and the interactive Playwright suite, which gains a transfer flow driven against the mock server: the note, both ends named before the move, the outcome, and the re-read contract.Docs updated:
docs/scripts/security.md(assets, actors, the three tiers, the definer-rights section),docs/scripts/running.md,docs/server/tools.md,docs/server/admin-portal.md,docs/server/portal-user.md,docs/llms.txt,docs/llms-full.txt, the built-inwriting-managed-scriptsknowledge page,CLAUDE.md, anddev/seed.sql.