stillmade-sdk 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +15 -0
- package/.cursor/hooks.json +11 -0
- package/.gitignore +2 -0
- package/.stillmade/sdk-baseline.json +65 -0
- package/AGENTS.md +29 -7
- package/AGENT_CONTROL.md +2 -1
- package/BROWSER_APPLICATIONS.md +128 -3
- package/GITHUB_APPLICATIONS.md +184 -1
- package/NATIVE_APPLICATION_ADAPTATION.md +565 -0
- package/PASTE_TO_LLM.md +29 -10
- package/PORTABLE.md +5 -4
- package/REMIX.md +16 -10
- package/SDK.md +511 -46
- package/UI_AESTHETIC.md +37 -6
- package/api-index.json +90 -0
- package/blocks/connected-app/versions/{1.2.0 → 1.5.0}/task-selection.js +5 -3
- package/blocks/connected-app/versions/{1.4.0 → 1.5.0}/view.js +4 -2
- package/blocks/native/sm.ai-decide/versions/1.0.0/package.js +149 -0
- package/blocks/native/sm.ai-write/versions/1.0.0/package.js +147 -0
- package/blocks/native/sm.connect-app/versions/1.0.0/package.js +55 -0
- package/blocks/native/sm.contact-fields/versions/1.0.0/package.js +197 -0
- package/blocks/native/sm.continue-if/versions/1.0.0/package.js +168 -0
- package/blocks/native/sm.generate-image/versions/2.1.0/package.js +101 -0
- package/blocks/native/sm.meta-creative/versions/1.0.0/package.js +206 -0
- package/blocks/native/sm.new-items/versions/1.0.0/package.js +191 -0
- package/blocks/native/sm.voiceover/versions/2.1.5/package.js +179 -0
- package/blocks/native/sm.voiceover/versions/2.1.6/package.js +179 -0
- package/blocks/native/sm.when/versions/1.0.0/package.js +129 -0
- package/blocks/native/sm.write-summary/versions/1.0.0/package.js +133 -0
- package/blocks/occasion-story/package.js +1 -1
- package/blocks/occasion-story/versions/1.0.2/package.js +1682 -0
- package/blocks/photo-reel-composer/versions/1.0.1/package.js +3026 -0
- package/blocks/release-visuals/package.js +1 -1
- package/blocks/release-visuals/versions/1.0.2/package.js +1500 -0
- package/blocks/short-form-finish/package.js +1 -1
- package/blocks/short-form-finish/versions/1.0.3/package.js +1026 -0
- package/examples/original-photo-editor.stillmade-block +0 -0
- package/examples/portable-regexp.stillmade-block +0 -0
- package/package.json +1 -1
- package/packages/block-cli/cli.js +16 -1
- package/packages/block-cli/format.js +2 -0
- package/packages/block-cli/ready-gate.js +164 -0
- package/packages/block-platform/block-readiness.js +3 -2
- package/packages/block-platform/canvas.js +1 -1
- package/packages/block-platform/catalog.js +6 -4
- package/packages/block-platform/chat-requirements.js +8 -3
- package/packages/block-platform/collaboration.js +5 -5
- package/packages/block-platform/first-revenue-project-types.js +2 -2
- package/packages/block-platform/flow-inspection.js +2 -2
- package/packages/block-platform/native-application-validation-context.js +20 -0
- package/packages/block-platform/plain-descriptions.js +67 -0
- package/packages/block-platform/product-ad-variants-project-type.js +6 -3
- package/packages/block-platform/project-context.js +4 -4
- package/packages/block-platform/project-type-composer.js +21 -12
- package/packages/block-platform/project-type-draft.js +2 -2
- package/packages/block-platform/project-type-recommendations.js +1 -1
- package/packages/block-platform/project-types.js +9 -4
- package/packages/block-platform/revenue-review-project-types.js +6 -5
- package/packages/block-platform/segment-runner.js +35 -11
- package/packages/block-platform/step-groups.js +78 -0
- package/packages/block-platform/steps.js +12 -8
- package/packages/block-platform/type-workspace.js +4 -4
- package/packages/block-platform/workflow-loop.js +4 -0
- package/packages/block-platform/workspace-input-readset.js +18 -4
- package/packages/block-sdk/admission.js +5 -17
- package/packages/block-sdk/agent-inputs.js +2 -0
- package/packages/block-sdk/agent-inspection.js +4 -2
- package/packages/block-sdk/browser-application-artifacts.js +1 -1
- package/packages/block-sdk/browser-application-execution.d.ts +2 -2
- package/packages/block-sdk/browser-application-execution.js +30 -12
- package/packages/block-sdk/browser-application-guest.d.ts +2 -0
- package/packages/block-sdk/browser-application-guest.js +16 -9
- package/packages/block-sdk/browser-application.d.ts +12 -4
- package/packages/block-sdk/browser-application.js +27 -13
- package/packages/block-sdk/builder-contract.js +25 -17
- package/packages/block-sdk/capabilities.js +38 -19
- package/packages/block-sdk/connected-app.js +26 -6
- package/packages/block-sdk/connection-adapters.js +19 -2
- package/packages/block-sdk/contracts-core.js +8 -0
- package/packages/block-sdk/contracts.js +9 -5
- package/packages/block-sdk/document-drafts.js +7 -1
- package/packages/block-sdk/document-store-binding.js +2 -1
- package/packages/block-sdk/file-bundle.js +4 -1
- package/packages/block-sdk/first-party-packages.js +18 -2
- package/packages/block-sdk/generation-catalog.js +3 -2
- package/packages/block-sdk/index.d.ts +20 -1
- package/packages/block-sdk/index.js +1 -0
- package/packages/block-sdk/native-application-package.d.ts +22 -0
- package/packages/block-sdk/native-application-package.js +77 -0
- package/packages/block-sdk/native_adapter.py +76 -0
- package/packages/block-sdk/package.json +1 -0
- package/packages/block-sdk/pipeline-runtime-host.d.ts +2 -0
- package/packages/block-sdk/project-flow-contract.js +2 -0
- package/packages/block-sdk/remix-policy.js +3 -1
- package/packages/block-sdk/resolver-admission.js +17 -0
- package/packages/block-sdk/runtime.js +4 -1
- package/packages/block-sdk/schema-fields.js +34 -2
- package/packages/block-sdk/semantic-relationships.js +14 -0
- package/packages/block-sdk/shared-controls.js +3 -1
- package/packages/block-sdk/shared-view.js +6 -1
- package/packages/block-sdk/source-rights.js +6 -5
- package/packages/block-sdk/testing.js +3 -0
- package/packages/design-system/theme.css +12 -3
|
@@ -42,12 +42,24 @@ export async function captureWorkspaceInputReadSet({manifest,input,project,scope
|
|
|
42
42
|
documents.set(nativeScope,document);ports.push({key,context:port.context,scope:nativeScope,inputDigest:await digest(input[key])});}
|
|
43
43
|
}
|
|
44
44
|
assert(Array.isArray(consumedOutputs)&&consumedOutputs.length<=128,'CONFLICT','Too many consumed output receipts.');
|
|
45
|
-
const collectionOutputs=collections.flatMap(row=>row.consumedOutputs);
|
|
45
|
+
const browserMaterials=[],collectionOutputs=collections.flatMap(row=>row.consumedOutputs);
|
|
46
46
|
assert(collectionOutputs.length+consumedOutputs.length<=128,'CONFLICT','Too many consumed output receipts.');
|
|
47
47
|
for(const consumed of [...consumedOutputs,...collectionOutputs]){
|
|
48
48
|
const {key,record,port,placementId,value,consumedValue}=consumed;
|
|
49
49
|
// Context resolution, user answers and optional branches can replace a wire.
|
|
50
50
|
if((manifest.inputs?.[key]?.context&&!collectionOutputs.includes(consumed))||input[key]===undefined||stableStringify(input[key])!==stableStringify(consumedValue))continue;
|
|
51
|
+
// This is a source selector, never an execution attestation. Acceptance
|
|
52
|
+
// independently checks the completed project ledger under its transaction.
|
|
53
|
+
if(record?.applicationReceipt){
|
|
54
|
+
const node=project.canvas?.nodes?.find(node=>node.id===placementId),data=node?.data,receipt=record.applicationReceipt;
|
|
55
|
+
const matches=data?.blockPackage?.manifest?.runtime==='browser-application'&&data.blockStale!==true&&bounded(port)&&bounded(record.runId)&&
|
|
56
|
+
receipt.phase==='completed'&&receipt.packageDigest===data.blockDigest&&receipt.inputDigest===record.inputDigest&&receipt.outputDigest===await digest(data.blockOutputs??{})&&
|
|
57
|
+
(record.packageDigest??record.digest)===data.blockDigest&&stableStringify(data.blockLastRun?.applicationReceipt??null)===stableStringify(receipt)&&
|
|
58
|
+
data.blockLastRun?.runId===record.runId&&stableStringify(data.blockLastRun?.applicationCheckpoint??null)===stableStringify(record.applicationCheckpoint??null)&&
|
|
59
|
+
stableStringify(data.blockApplicationDraft??null)===stableStringify(record.applicationCheckpoint?.draft??null)&&stableStringify(data.blockOutputs?.[port]??null)===stableStringify(value);
|
|
60
|
+
if(matches){browserMaterials.push({key,value:input[key],readSet:createWorkspaceReadSet([await workspaceSourceRead('canvas',project.canvas,['nodes',{id:placementId},'data'])]),documents:{canvas:project.canvas}});continue;}
|
|
61
|
+
unprotected.push({key,context:'upstream',reason:'missing-original-output-receipt'});continue;
|
|
62
|
+
}
|
|
51
63
|
const original=record?.workspaceInputReads;
|
|
52
64
|
if(original?.version!==2||!original.outputDigests||hasUnprovenStateOutput(record)){unprotected.push({key,context:'upstream',reason:'missing-original-output-receipt'});continue;}
|
|
53
65
|
validateWorkspaceInputReadSet(original,{runId:record.runId,packageDigest:record.packageDigest??record.digest,inputDigest:record.inputDigest});
|
|
@@ -58,15 +70,16 @@ export async function captureWorkspaceInputReadSet({manifest,input,project,scope
|
|
|
58
70
|
const reads=[];for(const [nativeScope,document] of documents)reads.push(await workspaceSourceRead(nativeScope,document,[]));
|
|
59
71
|
const directReadSet=reads.length?createWorkspaceReadSet(reads):null;
|
|
60
72
|
for(const collection of collections)for(const [scope,document] of Object.entries(collection.documents)){if(documents.has(scope))assert(stableStringify(documents.get(scope))===stableStringify(document),'CONFLICT','Original collection and context documents disagree.');else documents.set(scope,structuredClone(document));}
|
|
61
|
-
const materialReads=[];assert(Array.isArray(materialSources)&&materialSources.length<=128,'CONFLICT','Too many material input sources.');
|
|
62
|
-
for(const material of materialSources){const {key,value,readSet,documents:originalDocuments}=material;assert(bounded(key)&&Object.hasOwn(manifest.inputs,key)&&input[key]!==undefined&&stableStringify(input[key])===stableStringify(value)&&plain(originalDocuments),'CONFLICT','The material source no longer matches its consumed input.');validateWorkspaceReadSet(readSet);await assertWorkspaceReadSet(new Map(Object.entries(originalDocuments)),readSet);
|
|
73
|
+
const materialReads=[];assert(Array.isArray(materialSources)&&materialSources.length+browserMaterials.length<=128,'CONFLICT','Too many material input sources.');
|
|
74
|
+
for(const material of [...materialSources,...browserMaterials]){const {key,value,readSet,documents:originalDocuments}=material;assert(bounded(key)&&Object.hasOwn(manifest.inputs,key)&&input[key]!==undefined&&stableStringify(input[key])===stableStringify(value)&&plain(originalDocuments),'CONFLICT','The material source no longer matches its consumed input.');validateWorkspaceReadSet(readSet);await assertWorkspaceReadSet(new Map(Object.entries(originalDocuments)),readSet);
|
|
63
75
|
const usedScopes=new Set(readSet.reads.map(read=>read.scope));assert(Object.keys(originalDocuments).every(nativeScope=>usedScopes.has(nativeScope)),'CONFLICT','Material documents include an unused source.');
|
|
64
76
|
for(const [nativeScope,document] of Object.entries(originalDocuments)){if(documents.has(nativeScope))assert(stableStringify(documents.get(nativeScope))===stableStringify(document),'CONFLICT','Original material and context documents disagree.');else documents.set(nativeScope,structuredClone(document));}
|
|
65
77
|
materialReads.push({key,inputDigest:await digest(input[key]),readSet:structuredClone(readSet)});
|
|
66
78
|
}
|
|
67
79
|
const memory=await captureWorkspaceStateSource({manifest,state,source:stateSource,packageDigest});if(memory)unprotected.push(...memory.unprotected);else if(manifest.state)unprotected.push({key:'memory',context:'state',reason:'missing-original-state-receipt'});
|
|
68
80
|
const collectionReads=collections.map(({documents:ignored,consumedOutputs:ignoredOutputs,...row})=>row);
|
|
69
|
-
const
|
|
81
|
+
const inputDigests=Object.fromEntries(await Promise.all(Object.keys(manifest.inputs??{}).filter(key=>input[key]!==undefined).map(async key=>[key,await digest(input[key])])));
|
|
82
|
+
const evidence={version:2,runId,packageDigest,inputDigest,inputDigests,...(memory?{stateSource:memory}:{}),...(collectionReads.length?{collections:collectionReads}:{}),dispatchInputDigest:await digest(input),ports,unprotected,inherited,...(materialReads.length?{materialSources:materialReads}:{}),directReadSet,readSet:unionReads([directReadSet,memory?.readSet,...inherited.map(row=>row.readSet),...materialReads.map(row=>row.readSet),...collectionReads.map(row=>row.readSet)])};
|
|
70
83
|
validateWorkspaceInputReadSet(evidence,{runId,packageDigest,inputDigest});return {evidence,documents:Object.fromEntries(documents)};
|
|
71
84
|
}
|
|
72
85
|
function validateDerivedPorts(ports){
|
|
@@ -83,6 +96,7 @@ export function validateWorkspaceInputReadSet(evidence,{runId,packageDigest,inpu
|
|
|
83
96
|
if(direct)validateWorkspaceReadSet(direct);
|
|
84
97
|
assert(scopes.size===(direct?.reads.length||0)&&new Set((direct?.reads||[]).map(read=>read.scope)).size===scopes.size&&(direct?.reads||[]).every(read=>scopes.has(read.scope)&&read.path.length===0),'CONFLICT','The native workspace source reads are incomplete.');
|
|
85
98
|
if(evidence.version===2){
|
|
99
|
+
if(evidence.inputDigests!==undefined)assert(plain(evidence.inputDigests)&&Object.keys(evidence.inputDigests).length<=128&&Object.entries(evidence.inputDigests).every(([key,value])=>bounded(key)&&hash(value)),'CONFLICT','Dispatched input digests are invalid.');
|
|
86
100
|
assert(Array.isArray(evidence.inherited)&&evidence.inherited.length<=128,'CONFLICT','Inherited source receipts are invalid.');
|
|
87
101
|
for(const row of evidence.inherited){const source=row?.source;assert(plain(row)&&bounded(row.key)&&hash(row.inputDigest)&&plain(source)&&['placementId','port','runId'].every(key=>bounded(source[key]))&&['packageDigest','inputDigest','outputDigest','receiptDigest'].every(key=>hash(source[key]))&&Array.isArray(row.unprotected)&&row.unprotected.length<=128,'CONFLICT','Inherited output identity is invalid.');if(source.batchInventoryDigest!==undefined)assert(hash(source.batchInventoryDigest),'CONFLICT','Invalid inherited batch inventory identity.');if(row.readSet)validateWorkspaceReadSet(row.readSet);}
|
|
88
102
|
if(evidence.stateSource){validateWorkspaceStateSource(evidence.stateSource);assert(evidence.stateSource.unprotected.every(marker=>evidence.unprotected.some(row=>stableStringify(row)===stableStringify(marker))),'CONFLICT','Memory uncertainty cannot be removed from its output evidence.');}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {reviewResolverContract} from './resolver-admission.js';
|
|
2
|
+
export {reviewResolverContract} from './resolver-admission.js';
|
|
1
3
|
import {checkExportPolicy} from './export-admission.js';
|
|
2
4
|
import {blockRemixPolicy} from './remix-policy.js';
|
|
3
5
|
import {initialBlockState,validateBlockState} from './state.js';
|
|
@@ -12,6 +14,7 @@ import {reviewModelPortability} from './model-portability.js';
|
|
|
12
14
|
import {sharedAgentControls} from './shared-agent-controls.js';
|
|
13
15
|
import {assert,safeDocument,digest,sdkCompatibility,HOSTED_OPERATION_NAMES} from './contracts.js';
|
|
14
16
|
import {validatePackage,testPackage} from './testing.js';
|
|
17
|
+
import {NATIVE_APPLICATION_RUNTIME_REQUIREMENT} from './native-application-package.js';
|
|
15
18
|
|
|
16
19
|
const hostOperations=HOSTED_OPERATION_NAMES;
|
|
17
20
|
const credential=/\b(?:API_KEY|ACCESS_TOKEN|AUTH_TOKEN|CLIENT_SECRET|SECRET_KEY|[A-Z][A-Z0-9_]{1,}_(?:API_KEY|ACCESS_TOKEN|AUTH_TOKEN|TOKEN|SECRET|SECRET_KEY))\b/g;
|
|
@@ -70,27 +73,12 @@ function reviewAgentControlContract(pkg){
|
|
|
70
73
|
const hosted=['capability','comfyui'].includes(manifest.runtime),desktopTools=(manifest.desktopTools||[]).map(tool=>({id:tool.id,adapterVersion:tool.adapterVersion,actions:[...tool.actions]}));
|
|
71
74
|
return {...base,passed:true,manual:{schemaVersion:1,contract:'block-agent-json/1',block:{id:manifest.id,version:manifest.version,runtime:manifest.runtime,description:manifest.description},inputs:{json,media,context,fields:inputFields},outputs:{names:outputs,fields:outputFields},sharedView:{fields:sharedFields,dynamicFields:'Existing SDK-authored shared fields are discoverable at runtime and retain their current JSON shape.'},controls:{inspect:'inspect_app',setJsonInput:'set_input',setSharedControl:'set_shared_control',selectMedia:'select_media',selectMediaList:'select_media_list',execute:hosted?'review_generation':'run',receipt:'get_app_action',...(desktopTools.length?{desktopTools}: {})},execution:{reviewRequired:true,hostedIntegrationReview:hosted,desktopRequired:desktopTools.length>0}},message:`The host can derive exact chat and MCP controls from the pinned manifest: ${json.length} JSON input${json.length===1?'':'s'}, ${media.length} permission-checked media input${media.length===1?'':'s'}, ${context.length} host-supplied context input${context.length===1?'':'s'}, ${sharedFields.length} declared shared view control${sharedFields.length===1?'':'s'}, ${desktopTools.length} approved desktop tool${desktopTools.length===1?'':'s'}, and ${outputs.length} described output${outputs.length===1?'':'s'}. Runtime validation remains authoritative; package code cannot add hidden tools.`};
|
|
72
75
|
}
|
|
73
|
-
|
|
74
|
-
const base={name:'Input/output resolver contract',runtimeVerified:false,contract:'semantic-resolver/1'},manifest=pkg?.manifest||{},issues=[];
|
|
75
|
-
for(const [name,port] of Object.entries(manifest.inputs||{})){
|
|
76
|
-
if(typeof port?.semantic!=='string')issues.push(`${name} input needs an exact semantic meaning`);
|
|
77
|
-
if(typeof port?.required!=='boolean')issues.push(`${name} input must declare whether it is required`);
|
|
78
|
-
if(typeof port?.description!=='string'||!port.description.trim())issues.push(`${name} input needs a purpose/format description`);
|
|
79
|
-
}
|
|
80
|
-
for(const [name,port] of Object.entries(manifest.outputs||{})){
|
|
81
|
-
if(typeof port?.semantic!=='string')issues.push(`${name} output needs an exact semantic meaning`);
|
|
82
|
-
if(typeof port?.description!=='string'||!port.description.trim())issues.push(`${name} output needs a result description`);
|
|
83
|
-
if(['json','object'].includes(port?.type?.replace(/\[\]$/,''))){if(port.schema==null)issues.push(`${name} output needs a declared field schema for safe resolution`);else if(JSON.stringify(port.schema).length>4000)issues.push(`${name} output schema exceeds the resolver manual limit`);}
|
|
84
|
-
}
|
|
85
|
-
if(issues.length)return {...base,passed:false,code:'RESOLVER_CONTRACT',issues:issues.slice(0,20),message:`Import is blocked until every input and output can participate in typed resolution: ${issues.slice(0,8).join('; ')}${issues.length>8?`; ${issues.length-8} more`:''}. Physical JSON types alone do not establish compatible meaning.`};
|
|
86
|
-
const inputs=Object.entries(manifest.inputs||{}).map(([name,port])=>({name,type:port.type,semantic:port.semantic,required:port.required,sources:port.sources||[]}));
|
|
87
|
-
const outputs=Object.entries(manifest.outputs||{}).map(([name,port])=>({name,type:port.type,semantic:port.semantic}));
|
|
88
|
-
return {...base,passed:true,manual:{inputs,outputs},message:`All ${inputs.length} input${inputs.length===1?'':'s'} and ${outputs.length} output${outputs.length===1?'':'s'} declare physical types, semantic meanings and descriptions. Workflow composition must still validate each exact output-to-input pair, schema, adapter pin and required permission.`};
|
|
89
|
-
}
|
|
76
|
+
|
|
90
77
|
|
|
91
78
|
// Conservative admission checks supplement (never replace) the isolated runtime.
|
|
92
79
|
// Parse source as data; no module loader, package manager, eval or shell is used.
|
|
93
80
|
export function scanPackageSource(pkg) {
|
|
81
|
+
assert(pkg?.manifest?.runtime!=='native-application','NATIVE_APPLICATION_RUNTIME_UNAVAILABLE',NATIVE_APPLICATION_RUNTIME_REQUIREMENT);
|
|
94
82
|
const exports=checkExportPolicy(pkg);
|
|
95
83
|
if(!exports.passed)return {passed:false,results:[exports]};
|
|
96
84
|
validatePackage(pkg);
|
|
@@ -21,6 +21,8 @@ export function parseAgentInput(manifest,args,{locked=[],busy=false,disabled=fal
|
|
|
21
21
|
const port=manifest.inputs?.[args.field];
|
|
22
22
|
if(busy||disabled||!port||port.context||locked.includes(args.field))throw new Error('This input is locked or the Block is busy.');
|
|
23
23
|
if(!editable.has(port.type))throw new Error('Use the matching media or project-item control for this input.');
|
|
24
|
+
// A script or brief takes plain text too, stored the way its text control stores it.
|
|
25
|
+
if(['script','brief'].includes(port.type)&&typeof args.value==='string')args={...args,value:{schemaVersion:1,text:args.value}};
|
|
24
26
|
safeDocument(args.value);
|
|
25
27
|
if(JSON.stringify(args.value).length>20000||Array.isArray(args.value)&&args.value.length>100||!valueMatches(args.value,port))throw new Error('Use a compatible value with at most 100 list items and 20,000 characters.');
|
|
26
28
|
const control=manifest.ui?.find(row=>row.port===args.field);
|
|
@@ -26,7 +26,9 @@ export function inspectAgentValue(value, port = {type:'json'}, {mediaVerified =
|
|
|
26
26
|
return {status:text !== undefined ? (text.trim()?'filled':'empty') : keys.length?'filled':'empty', filled:text !== undefined ? !!text.trim() : keys.length>0, fields:keys.length};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// `verifiedMedia` names ports whose media StillMade itself recorded for the
|
|
30
|
+
// current run (a media receipt), so their availability reads as verified.
|
|
31
|
+
export function inspectAgentPorts(ports = {}, values = {}, {offset=0, limit=30, locked=[],canonicalOrder=false,verifiedMedia=[]} = {}) {
|
|
30
32
|
const entries = canonicalOrder?agentInputEntries({inputs:plain(ports)?ports:{}}):Object.entries(plain(ports) ? ports : {});
|
|
31
33
|
const start = Math.max(0,Math.trunc(offset)||0), count = Math.max(1,Math.min(50,Math.trunc(limit)||30));
|
|
32
34
|
let schemaBudget=12000;
|
|
@@ -34,7 +36,7 @@ export function inspectAgentPorts(ports = {}, values = {}, {offset=0, limit=30,
|
|
|
34
36
|
const declared = plain(port) ? port : {type:'json'};
|
|
35
37
|
const hasValue = Object.hasOwn(values || {},id), hasDefault = Object.hasOwn(declared,'default');
|
|
36
38
|
const value = hasValue ? values[id] : hasDefault ? declared.default : undefined;
|
|
37
|
-
const state = inspectAgentValue(value,declared);
|
|
39
|
+
const state = inspectAgentValue(value,declared,{mediaVerified:verifiedMedia.includes(id)});
|
|
38
40
|
const schema=declared.schema==null?null:structuredClone(declared.schema),schemaText=schema==null?'':JSON.stringify(schema);
|
|
39
41
|
const includeSchema=schemaText&&schemaText.length<=Math.min(4000,schemaBudget);if(includeSchema)schemaBudget-=schemaText.length;
|
|
40
42
|
return {id,type:declared.type || 'unknown',required:declared.required!==false,...(typeof declared.description==='string'&&declared.description.trim()?{description:declared.description.slice(0,240)}:{}),...(typeof declared.semantic==='string'?{semantic:declared.semantic.slice(0,80)}:{}),...(typeof declared.role==='string'?{role:declared.role.slice(0,80)}:{}),...(Array.isArray(declared.accepts)?{accepts:declared.accepts.filter(item=>typeof item==='string').slice(0,12)}:{}),...(Number.isFinite(declared.min)?{min:declared.min}:{}),...(Number.isFinite(declared.max)?{max:declared.max}:{}),...(declared.primary===true?{primary:true}:{}),...(includeSchema?{schema}:schemaText?{schemaPartial:true}:{}),source:declared.context?'project':hasValue?'provided':hasDefault?'default':'unset',locked:!!declared.context || locked.includes(id),...state};
|
|
@@ -47,7 +47,7 @@ function sourceManifest(manifest){
|
|
|
47
47
|
check(plain(manifest)&&manifest.runtime==='browser-application','APPLICATION_SOURCE_REFERENCE','Use a browser application manifest.');
|
|
48
48
|
const errors=validateManifest(manifest).errors.filter(error=>error.path!=='runtime');
|
|
49
49
|
check(errors.length===0,'APPLICATION_SOURCE_REFERENCE','Invalid application manifest: '+JSON.stringify(errors));
|
|
50
|
-
check(!manifest.id.startsWith('sm.')&&manifest.platforms?.desktop?.supported===true&&manifest.platforms?.browser?.supported===
|
|
50
|
+
check(!manifest.id.startsWith('sm.')&&manifest.platforms?.desktop?.supported===true&&typeof manifest.platforms?.browser?.supported==='boolean'&&manifest.platforms?.phone?.supported===false,'APPLICATION_SOURCE_REFERENCE','This reference profile requires a non-platform browser application with explicit browser availability and no phone build.');
|
|
51
51
|
check(manifest.entry===undefined||manifest.entry==='src/application.json','APPLICATION_SOURCE_REFERENCE','Use the canonical application source entry.');
|
|
52
52
|
check(!manifest.desktopTools&&!manifest.permissions.desktop?.length&&!manifest.permissions.capabilities?.length&&!manifest.permissions.actions?.length,'APPLICATION_SOURCE_REFERENCE','Application references cannot add desktop, paid integration or arbitrary action access.');
|
|
53
53
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {Manifest} from './index.js';
|
|
2
|
-
import type {BrowserApplicationBundle,BrowserApplicationOperation,BrowserApplicationDeepReadonly
|
|
2
|
+
import type {BrowserApplicationBundle,BrowserApplicationOperation,BrowserApplicationDeepReadonly} from './browser-application.js';
|
|
3
3
|
|
|
4
4
|
/** Deliberately separate from the installable Manifest/BlockPackage unions. */
|
|
5
5
|
export type BrowserApplicationManifest=Omit<Manifest,'runtime'|'state'|'jobs'|'desktopTools'|'permissions'|'platforms'>&{
|
|
@@ -11,7 +11,7 @@ export interface BrowserApplicationSource {manifest:BrowserApplicationManifest;a
|
|
|
11
11
|
export interface BrowserApplicationExecutionContext {operation:string;state:Record<string,unknown>;stateRevision:string;interactive?:boolean}
|
|
12
12
|
export type BrowserApplicationMountContext=Pick<BrowserApplicationExecutionContext,'state'|'stateRevision'>;
|
|
13
13
|
export interface BrowserApplicationRequest {
|
|
14
|
-
schemaVersion:1;phase:'execute'|'mount';requestId:string;profile:
|
|
14
|
+
schemaVersion:1;phase:'execute'|'mount';requestId:string;profile:BrowserApplicationBundle['profile'];
|
|
15
15
|
packageDigest:string;bundleDigest:string;inputDigest:string;stateDigest:string;stateRevision:string;
|
|
16
16
|
operation:BrowserApplicationOperation|null;lifecycle:'interactive'|'headless';
|
|
17
17
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {assert,plain,safeDocument,validateManifest,validateValues,validateBlockOutputs,digest,stableStringify} from './contracts.js';
|
|
2
|
-
import {defineBrowserApplication,
|
|
2
|
+
import {defineBrowserApplication,BROWSER_APPLICATION_LIMITS} from './browser-application.js';
|
|
3
3
|
|
|
4
4
|
const exact=(value,keys)=>plain(value)&&Object.keys(value).length===keys.length&&keys.every(key=>Object.hasOwn(value,key));
|
|
5
5
|
const bounded=(value,label)=>{safeDocument(value);assert(new TextEncoder().encode(JSON.stringify(value)).length<=BROWSER_APPLICATION_LIMITS.messageBytes,'LIMIT',`${label} exceeds 4 MiB.`);};
|
|
@@ -8,9 +8,10 @@ const text=value=>typeof value==='string'&&value.trim()===value&&value.length>0&
|
|
|
8
8
|
const freeze=value=>{if(value&&typeof value==='object'){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
|
|
9
9
|
const verifiedSources=new WeakSet(),preparedRequests=new WeakSet(),sourceDigests=new WeakMap();
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* still
|
|
13
|
-
export async function defineBrowserApplicationSource(value){
|
|
11
|
+
/** Application source contract. Platform declarations do not grant execution:
|
|
12
|
+
* hosts still require exact reviewed pins and a trusted execution transport. */
|
|
13
|
+
export async function defineBrowserApplicationSource(value,{signal}={}){
|
|
14
|
+
signal?.throwIfAborted();
|
|
14
15
|
if(verifiedSources.has(value))return value;
|
|
15
16
|
safeDocument(value);check(plain(value)&&Object.keys(value).every(key=>['manifest','application','tests'].includes(key)),'Use manifest, application and optional source fixtures only.');
|
|
16
17
|
check(value.manifest?.runtime==='browser-application','Use the explicit, not-yet-installed browser-application runtime.');
|
|
@@ -19,33 +20,48 @@ export async function defineBrowserApplicationSource(value){
|
|
|
19
20
|
// is its deliberately unavailable runtime enum; never rewrite it to JS.
|
|
20
21
|
const errors=validateManifest(manifest).errors.filter(error=>error.path!=='runtime');
|
|
21
22
|
check(errors.length===0,JSON.stringify(errors));
|
|
23
|
+
check(Object.keys(manifest.outputs).length>0,'Application operations must declare at least one typed output.');
|
|
22
24
|
check(!manifest.id.startsWith('sm.'),'First-party identities require the platform release registry.');
|
|
23
|
-
check(manifest.platforms?.desktop?.supported===true&&manifest.platforms?.browser?.supported===
|
|
25
|
+
check(manifest.platforms?.desktop?.supported===true&&typeof manifest.platforms?.browser?.supported==='boolean'&&manifest.platforms?.phone?.supported===false,'Declare desktop and browser availability explicitly; this profile has no verified phone build.');
|
|
24
26
|
check(manifest.entry===undefined||manifest.entry==='src/application.json','Use the canonical src/application.json source entry.');
|
|
25
27
|
check(!manifest.desktopTools&&!manifest.permissions.desktop?.length&&!manifest.permissions.capabilities?.length&&!manifest.permissions.actions?.length,'The initial application contract grants no desktop, paid integration or arbitrary action access.');
|
|
26
28
|
const fixtures=value.tests===undefined?undefined:(bounded(value.tests,'Application source fixtures'),structuredClone(value.tests));
|
|
27
|
-
const source={manifest,application:await defineBrowserApplication(value.application)};
|
|
29
|
+
const source={manifest,application:await defineBrowserApplication(value.application,{signal})};
|
|
30
|
+
signal?.throwIfAborted();const mapped=source.application.operations.filter(item=>Array.isArray(item.outputs));
|
|
31
|
+
if(mapped.length){
|
|
32
|
+
check(mapped.length===source.application.operations.length,'Every operation must map its output ports when any operation does.');
|
|
33
|
+
const exposed=new Set(mapped.flatMap(item=>item.outputs));
|
|
34
|
+
check(exposed.size===Object.keys(manifest.outputs).length&&[...exposed].every(name=>Object.hasOwn(manifest.outputs,name)),'Operation outputs must cover exactly the manifest output ports.');
|
|
35
|
+
for(const [name,port] of Object.entries(manifest.outputs))if(mapped.some(item=>!item.outputs.includes(name)))check(port.required===false,`Output ${name} must be optional because some operations do not produce it.`);
|
|
36
|
+
}
|
|
28
37
|
if(fixtures!==undefined){
|
|
29
38
|
check(Array.isArray(fixtures)&&fixtures.length>0&&fixtures.length<=50,'Supply 1–50 source fixtures when present.');
|
|
30
39
|
source.tests=fixtures;
|
|
31
40
|
for(const fixture of source.tests){
|
|
32
41
|
check(exact(fixture,['name','operation','input','state','stateRevision','expected'])&&text(fixture.name),'Application fixtures declare name, operation, input, state, stateRevision and expected outputs.');
|
|
33
|
-
operation(source,fixture.operation);contextState(fixture);validateValues(manifest.inputs,fixture.input);validateBlockOutputs(manifest,fixture.expected);
|
|
42
|
+
const selected=operation(source,fixture.operation);contextState(fixture);validateValues(manifest.inputs,fixture.input);validateBlockOutputs(manifest,fixture.expected);
|
|
43
|
+
if(selected.outputs)check(stableStringify(Object.keys(fixture.expected).sort())===stableStringify([...selected.outputs].sort()),'Fixture output ports differ from their operation contract.');
|
|
34
44
|
}
|
|
35
45
|
}
|
|
36
|
-
freeze(source);verifiedSources.add(source);return source;
|
|
46
|
+
freeze(source);signal?.throwIfAborted();verifiedSources.add(source);return source;
|
|
37
47
|
}
|
|
38
48
|
function operation(source,id){const declared=source.application.operations.find(item=>item.id===id);check(declared,'Choose an operation declared by this exact application bundle.');return declared;}
|
|
39
49
|
function contextState(options){check(plain(options)&&plain(options.state)&&text(options.stateRevision),'Supply host-owned saved state and its exact revision.');bounded(options.state,'Application state');return structuredClone(options.state);}
|
|
50
|
+
export async function getBrowserApplicationSourceDigests(value,{signal}={}){
|
|
51
|
+
signal?.throwIfAborted();
|
|
52
|
+
const source=await defineBrowserApplicationSource(value,{signal});
|
|
53
|
+
// Only the SDK's detached, deeply frozen source can key this private cache.
|
|
54
|
+
if(!sourceDigests.has(source))sourceDigests.set(source,Promise.all([digest(source),digest(source.application)]).then(([packageDigest,bundleDigest])=>Object.freeze({packageDigest,bundleDigest})));
|
|
55
|
+
const result=await sourceDigests.get(source);
|
|
56
|
+
signal?.throwIfAborted();return result;
|
|
57
|
+
}
|
|
40
58
|
async function prepare(value,values,options,phase){
|
|
41
59
|
bounded(values,'Application inputs');const inputCopy=structuredClone(values),state=contextState(options),revision=options.stateRevision,operationId=options.operation,interactive=options.interactive;
|
|
42
60
|
const source=await defineBrowserApplicationSource(value),input=validateValues(source.manifest.inputs,inputCopy);
|
|
43
61
|
const selected=phase==='execute'?operation(source,operationId):null;
|
|
44
62
|
if(phase==='execute'&&source.application.lifecycle==='interactive')check(interactive===true,'Interactive application operations require an active interactive host.');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const [packageDigest,bundleDigest]=await sourceDigests.get(source);
|
|
48
|
-
const request={schemaVersion:1,phase,requestId:globalThis.crypto.randomUUID(),profile:BROWSER_APPLICATION_PROFILE,packageDigest,bundleDigest,inputDigest:await digest(input),stateDigest:await digest(state),stateRevision:revision,operation:selected?structuredClone(selected):null,lifecycle:source.application.lifecycle};
|
|
63
|
+
const {packageDigest,bundleDigest}=await getBrowserApplicationSourceDigests(source);
|
|
64
|
+
const request={schemaVersion:1,phase,requestId:globalThis.crypto.randomUUID(),profile:source.application.profile,packageDigest,bundleDigest,inputDigest:await digest(input),stateDigest:await digest(state),stateRevision:revision,operation:selected?structuredClone(selected):null,lifecycle:source.application.lifecycle};
|
|
49
65
|
const prepared=freeze({package:source,input,state,request});preparedRequests.add(prepared);return prepared;
|
|
50
66
|
}
|
|
51
67
|
export function prepareBrowserApplicationExecution(source,values,options){return prepare(source,values,options,'execute');}
|
|
@@ -68,6 +84,8 @@ export async function validateBrowserApplicationExecutionResult(prepared,result)
|
|
|
68
84
|
const receipt=snapshot.applicationReceipt,request=validated.request;
|
|
69
85
|
check(exact(receipt,['schemaVersion','phase','requestId','profile','packageDigest','bundleDigest','inputDigest','stateDigest','stateRevision','operation','lifecycle','outputDigest','executionId'])&&receipt.schemaVersion===1&&receipt.phase==='completed'&&text(receipt.executionId),'A mount or preview is not a completed application operation.');
|
|
70
86
|
for(const key of Object.keys(request).filter(key=>key!=='phase'))check(stableStringify(receipt[key])===stableStringify(request[key]),'Application receipt does not match this exact execution request.');
|
|
87
|
+
check(plain(snapshot.outputs)&&Object.keys(snapshot.outputs).length>0,'A completed application operation must produce at least one declared output.');
|
|
88
|
+
if(request.operation.outputs)check(stableStringify(Object.keys(snapshot.outputs).sort())===stableStringify([...request.operation.outputs].sort()),'The operation returned different output ports than its host-declared mapping.');
|
|
71
89
|
const outputs=validateBlockOutputs(validated.package.manifest,snapshot.outputs);
|
|
72
90
|
check(receipt.outputDigest===await digest(outputs),'Application receipt does not match returned outputs.');
|
|
73
91
|
return {outputs,applicationReceipt:receipt,independentlyVerified:false};
|
|
@@ -29,4 +29,6 @@ export function installBrowserApplicationGuest(options:{
|
|
|
29
29
|
operations:ReadonlyArray<BrowserApplicationOperation>;
|
|
30
30
|
/** May lower, never raise, the 4 MiB per-message limit. */
|
|
31
31
|
messageBytes?:number;
|
|
32
|
+
/** Host-selected output-only ceiling, at most 8 MiB. Inputs and state keep messageBytes. */
|
|
33
|
+
outputBytes?:number;
|
|
32
34
|
}):BrowserApplicationGuestController;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Object controls and cancellation are cooperative in the guest realm. The
|
|
5
5
|
* trusted transport must enforce isolation, deadlines, UI-input freezing,
|
|
6
6
|
* checkpoint identity, source bindings and actual worker termination itself. */
|
|
7
|
-
export function installBrowserApplicationGuest({target=globalThis,operations,messageBytes=4194304}={}){
|
|
7
|
+
export function installBrowserApplicationGuest({target=globalThis,operations,messageBytes=4194304,outputBytes=messageBytes}={}){
|
|
8
8
|
const fail=(code,message)=>Object.assign(new Error(message),{code});
|
|
9
9
|
const check=(condition,code,message)=>{if(!condition)throw fail(code,message);};
|
|
10
10
|
const plain=value=>value!==null&&typeof value==='object'&&!Array.isArray(value)&&[Object.prototype,null].includes(Object.getPrototypeOf(value));
|
|
@@ -20,23 +20,29 @@ export function installBrowserApplicationGuest({target=globalThis,operations,mes
|
|
|
20
20
|
};
|
|
21
21
|
check(target!==null&&['object','function'].includes(typeof target)&&!('stillmadeApplication' in target),'APPLICATION_GUEST_INSTALLED','Application registration is already installed or the target is invalid.');
|
|
22
22
|
check(Number.isSafeInteger(messageBytes)&&messageBytes>0&&messageBytes<=4194304,'APPLICATION_GUEST_LIMIT','Application messages must be bounded by at most 4 MiB.');
|
|
23
|
+
check(Number.isSafeInteger(outputBytes)&&outputBytes>=messageBytes&&outputBytes<=8*1048576,'APPLICATION_GUEST_LIMIT','Application output candidates need a bounded host-selected limit.');
|
|
23
24
|
check(Array.isArray(operations)&&operations.length>0&&operations.length<=32,'APPLICATION_GUEST_OPERATION','Declare 1–32 application operations.');
|
|
24
|
-
const declared=new Set();
|
|
25
|
+
const declared=new Set(),mapped=new Map();
|
|
25
26
|
for(const operation of operations){
|
|
26
|
-
check(exact(operation,['id','kind']),'APPLICATION_GUEST_OPERATION','Declare
|
|
27
|
+
check(exact(operation,['id','kind'])||exact(operation,['id','kind','outputs']),'APPLICATION_GUEST_OPERATION','Declare an operation id, kind and optional output-port map.');
|
|
27
28
|
const id=field(operation,'id'),kind=field(operation,'kind');
|
|
28
29
|
check(typeof id==='string'&&/^[a-z][a-z0-9.-]{0,79}$/.test(id)&&!declared.has(id)&&['export','execute'].includes(kind),'APPLICATION_GUEST_OPERATION','Application operations must have distinct valid ids and kinds.');
|
|
30
|
+
if(Object.hasOwn(operation,'outputs')){
|
|
31
|
+
const names=field(operation,'outputs');
|
|
32
|
+
check(Array.isArray(names)&&names.length>0&&names.length<=64&&new Set(names).size===names.length&&names.every(name=>typeof name==='string'&&/^[a-z][a-zA-Z0-9_]*$/.test(name)),'APPLICATION_GUEST_OPERATION','Map the declared operation to distinct output ports.');
|
|
33
|
+
mapped.set(id,[...names]);
|
|
34
|
+
}
|
|
29
35
|
declared.add(id);
|
|
30
36
|
}
|
|
31
37
|
// Copy JSON using data descriptors, never JSON.stringify on an adapter object
|
|
32
38
|
// with accessors/toJSON. These checks catch mistakes; hostile guest code can
|
|
33
39
|
// still change realm intrinsics, so the host must revalidate every message.
|
|
34
|
-
function document(value,label){
|
|
40
|
+
function document(value,label,limit=messageBytes){
|
|
35
41
|
let nodes=0;const ancestors=new Set();
|
|
36
42
|
function visit(item,depth){
|
|
37
43
|
check(depth<=40&&++nodes<=500000,'APPLICATION_GUEST_LIMIT',`${label} exceeds the JSON complexity limit.`);
|
|
38
44
|
if(item===null||typeof item==='boolean')return item;
|
|
39
|
-
if(typeof item==='string'){check(item.length<=
|
|
45
|
+
if(typeof item==='string'){check(item.length<=limit,'APPLICATION_GUEST_LIMIT',`${label} exceeds its message limit.`);return item;}
|
|
40
46
|
if(typeof item==='number'){check(Number.isFinite(item),'APPLICATION_GUEST_DATA',`${label} needs finite numbers.`);return item;}
|
|
41
47
|
check((Array.isArray(item)||plain(item))&&!ancestors.has(item),'APPLICATION_GUEST_DATA',`${label} needs acyclic plain JSON data.`);
|
|
42
48
|
ancestors.add(item);let result;
|
|
@@ -47,7 +53,7 @@ export function installBrowserApplicationGuest({target=globalThis,operations,mes
|
|
|
47
53
|
result={};
|
|
48
54
|
for(const key of Reflect.ownKeys(item)){
|
|
49
55
|
check(typeof key==='string'&&!['__proto__','prototype','constructor'].includes(key),'APPLICATION_GUEST_DATA',`${label} contains an unsupported field.`);
|
|
50
|
-
check(key.length<=
|
|
56
|
+
check(key.length<=limit,'APPLICATION_GUEST_LIMIT',`${label} exceeds its message limit.`);
|
|
51
57
|
result[key]=visit(field(item,key),depth+1);
|
|
52
58
|
}
|
|
53
59
|
}
|
|
@@ -55,7 +61,7 @@ export function installBrowserApplicationGuest({target=globalThis,operations,mes
|
|
|
55
61
|
}
|
|
56
62
|
check(plain(value),'APPLICATION_GUEST_DATA',`${label} must be a JSON object.`);
|
|
57
63
|
const copied=visit(value,0);
|
|
58
|
-
check(new TextEncoder().encode(JSON.stringify(copied)).length<=
|
|
64
|
+
check(new TextEncoder().encode(JSON.stringify(copied)).length<=limit,'APPLICATION_GUEST_LIMIT',`${label} exceeds its message limit.`);
|
|
59
65
|
return copied;
|
|
60
66
|
}
|
|
61
67
|
const stable=value=>JSON.stringify((function sort(item){return Array.isArray(item)?item.map(sort):plain(item)?Object.fromEntries(Object.keys(item).sort().map(key=>[key,sort(item[key])])):item;})(value));
|
|
@@ -111,9 +117,10 @@ export function installBrowserApplicationGuest({target=globalThis,operations,mes
|
|
|
111
117
|
const payload=document({operation,inputs,stateRevision},'Application operation');
|
|
112
118
|
check(plain(payload.inputs)&&stable(payload.inputs)===mountedInputs,'APPLICATION_GUEST_INPUT','Operation inputs must match the mounted inputs.');
|
|
113
119
|
return run('execute',signal,inner=>adapter.execute({...payload,signal:inner}),raw=>{
|
|
114
|
-
const outputs=document(raw,'Application outputs');
|
|
120
|
+
const outputs=document(raw,'Application outputs',outputBytes);
|
|
115
121
|
check(!['applicationReceipt','desktopReceipt','independentlyVerified'].some(key=>Object.hasOwn(outputs,key)),'APPLICATION_GUEST_RESULT','Adapters return output values only, never host receipt or verification fields.');
|
|
116
|
-
|
|
122
|
+
if(mapped.has(operation))check(JSON.stringify(Object.keys(outputs).sort())===JSON.stringify([...mapped.get(operation)].sort()),'APPLICATION_GUEST_RESULT','The adapter returned different output ports than this operation declares.');
|
|
123
|
+
return document({outputs},'Application result',outputBytes);
|
|
117
124
|
});
|
|
118
125
|
},
|
|
119
126
|
dispose(){
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/** Source format only; this constant does not advertise an installed runtime. */
|
|
2
2
|
export const BROWSER_APPLICATION_PROFILE:'desktop-browser-application/1';
|
|
3
|
-
export const
|
|
4
|
-
export
|
|
3
|
+
export const BROWSER_APPLICATION_EMBEDDED_PROFILE:'desktop-browser-application/2';
|
|
4
|
+
export const BROWSER_APPLICATION_LOCAL_WORKER_PROFILE:'desktop-browser-application/3';
|
|
5
|
+
export const BROWSER_APPLICATION_LIMITS:Readonly<{assets:number;assetBytes:number;totalBytes:number;pathBytes:number;operations:number;messageBytes:number;outputCandidateBytes:number;timeoutMs:number}>;
|
|
6
|
+
export type BrowserApplicationMime='text/html'|'text/javascript'|'text/css'|'application/json'|'application/manifest+json'|'application/xml'|'text/xml'|'application/wasm'|'application/octet-stream'|'text/plain'|'image/png'|'image/jpeg'|'image/gif'|'image/webp'|'image/avif'|'image/svg+xml'|'image/x-icon'|'font/woff2'|'font/woff'|'font/ttf'|'font/otf'|'application/vnd.ms-fontobject'|'audio/mpeg'|'audio/ogg'|'audio/wav'|'video/mp4'|'video/webm';
|
|
5
7
|
export interface BrowserApplicationAsset {path:string;mime:BrowserApplicationMime;sha256:string;encoding:'base64';bytes:number;data:string}
|
|
6
8
|
export interface BrowserApplicationOperation {id:string;kind:'export'|'execute'}
|
|
7
|
-
export
|
|
9
|
+
export type BrowserApplicationBundle={
|
|
8
10
|
schemaVersion:1;profile:typeof BROWSER_APPLICATION_PROFILE;appearance:'original';entry:string;adapter:string;
|
|
9
11
|
lifecycle:'interactive'|'headless';operations:BrowserApplicationOperation[];assets:BrowserApplicationAsset[];features:{wasm:boolean};
|
|
10
|
-
}
|
|
12
|
+
}|{
|
|
13
|
+
schemaVersion:1;profile:typeof BROWSER_APPLICATION_EMBEDDED_PROFILE;appearance:'original';entry:string;adapter:string;
|
|
14
|
+
lifecycle:'interactive'|'headless';operations:BrowserApplicationOperation[];assets:BrowserApplicationAsset[];features:{wasm:boolean;srcdocFrames:boolean};
|
|
15
|
+
}|{
|
|
16
|
+
schemaVersion:1;profile:typeof BROWSER_APPLICATION_LOCAL_WORKER_PROFILE;appearance:'original';entry:string;adapter:string;
|
|
17
|
+
lifecycle:'interactive'|'headless';operations:BrowserApplicationOperation[];assets:BrowserApplicationAsset[];features:{wasm:boolean;srcdocFrames:boolean;localWorkers:true};
|
|
18
|
+
};
|
|
11
19
|
export type BrowserApplicationDeepReadonly<T>=T extends object?{readonly [P in keyof T]:BrowserApplicationDeepReadonly<T[P]>}:T;
|
|
12
20
|
/** Strict canonical relative regular-file path. No URL, traversal or decoding. */
|
|
13
21
|
export function browserApplicationPath(path:string):string;
|
|
@@ -3,15 +3,17 @@ import {assert,plain,safeDocument,digest} from './contracts.js';
|
|
|
3
3
|
// A bundle/evidence format, not an installed runtime or an admission decision.
|
|
4
4
|
// The existing restricted view and portable archive formats are unchanged.
|
|
5
5
|
export const BROWSER_APPLICATION_PROFILE='desktop-browser-application/1';
|
|
6
|
+
export const BROWSER_APPLICATION_EMBEDDED_PROFILE='desktop-browser-application/2';
|
|
7
|
+
export const BROWSER_APPLICATION_LOCAL_WORKER_PROFILE='desktop-browser-application/3';
|
|
6
8
|
// One large WASM engine plus fonts, icons and editor resources, with explicit
|
|
7
9
|
// aggregate bounds. These are not the existing portable archive's 4 MB limit.
|
|
8
|
-
export const BROWSER_APPLICATION_LIMITS=Object.freeze({assets:2048,assetBytes:64*1024*1024,totalBytes:128*1024*1024,pathBytes:256,operations:32,messageBytes:4*1024*1024,timeoutMs:120000});
|
|
10
|
+
export const BROWSER_APPLICATION_LIMITS=Object.freeze({assets:2048,assetBytes:64*1024*1024,totalBytes:128*1024*1024,pathBytes:256,operations:32,messageBytes:4*1024*1024,outputCandidateBytes:8*1024*1024,timeoutMs:120000});
|
|
9
11
|
const mimeExtensions=Object.freeze({
|
|
10
12
|
'text/html':['html'],'text/javascript':['js','mjs'],'text/css':['css'],'application/json':['json','map'],
|
|
11
13
|
'application/manifest+json':['webmanifest'],'application/xml':['xml'],'text/xml':['xml'],
|
|
12
14
|
'application/wasm':['wasm'],'application/octet-stream':['bin','data','dat'],'text/plain':['txt'],
|
|
13
15
|
'image/png':['png'],'image/jpeg':['jpg','jpeg'],'image/gif':['gif'],'image/webp':['webp'],'image/avif':['avif'],'image/svg+xml':['svg'],'image/x-icon':['ico'],
|
|
14
|
-
'font/woff2':['woff2'],'font/woff':['woff'],'font/ttf':['ttf'],'font/otf':['otf'],
|
|
16
|
+
'font/woff2':['woff2'],'font/woff':['woff'],'font/ttf':['ttf'],'font/otf':['otf'],'application/vnd.ms-fontobject':['eot'],
|
|
15
17
|
'audio/mpeg':['mp3'],'audio/ogg':['ogg'],'audio/wav':['wav'],'video/mp4':['mp4'],'video/webm':['webm'],
|
|
16
18
|
});
|
|
17
19
|
const verifiedBundles=new WeakSet();
|
|
@@ -20,20 +22,23 @@ const hash=value=>typeof value==='string'&&/^[a-f0-9]{64}$/.test(value);
|
|
|
20
22
|
const freeze=value=>{if(value&&typeof value==='object'){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
|
|
21
23
|
const check=(condition,message)=>assert(condition,'BROWSER_APPLICATION',message);
|
|
22
24
|
export function browserApplicationPath(path){
|
|
23
|
-
check(typeof path==='string'&&path.length>0&&path.length<=BROWSER_APPLICATION_LIMITS.pathBytes&&/^[A-Za-z0-9_
|
|
25
|
+
check(typeof path==='string'&&path.length>0&&path.length<=BROWSER_APPLICATION_LIMITS.pathBytes&&/^[A-Za-z0-9_@$+-][A-Za-z0-9_@$./&+-]*$/.test(path),'Use a bounded relative application asset path, never a URL.');
|
|
24
26
|
check(path.split('/').every(part=>part&&part!=='.'&&part!=='..'&&!part.endsWith('.')&&!/^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\.|$)/i.test(part)),'Application paths must be canonical regular-file paths.');
|
|
25
27
|
return path;
|
|
26
28
|
}
|
|
27
29
|
function metadata(value){
|
|
28
30
|
safeDocument(value);
|
|
29
|
-
check(exact(value,['schemaVersion','profile','appearance','entry','adapter','lifecycle','operations','assets','features'])&&value.schemaVersion===1&&value.profile
|
|
31
|
+
check(exact(value,['schemaVersion','profile','appearance','entry','adapter','lifecycle','operations','assets','features'])&&value.schemaVersion===1&&[BROWSER_APPLICATION_PROFILE,BROWSER_APPLICATION_EMBEDDED_PROFILE,BROWSER_APPLICATION_LOCAL_WORKER_PROFILE].includes(value.profile),'Use a supported desktop browser application bundle profile.');
|
|
30
32
|
check(value.appearance==='original','This profile preserves original styling; host theming requires a separately declared future adapter.');
|
|
31
33
|
check(['interactive','headless'].includes(value.lifecycle),'Declare an interactive or headless application lifecycle.');
|
|
32
|
-
|
|
34
|
+
const embedded=value.profile===BROWSER_APPLICATION_EMBEDDED_PROFILE;
|
|
35
|
+
const localWorkers=value.profile===BROWSER_APPLICATION_LOCAL_WORKER_PROFILE;
|
|
36
|
+
check(exact(value.features,localWorkers?['wasm','srcdocFrames','localWorkers']:embedded?['wasm','srcdocFrames']:['wasm'])&&typeof value.features.wasm==='boolean'&&(!embedded&&!localWorkers||typeof value.features.srcdocFrames==='boolean')&&(!localWorkers||value.features.localWorkers===true),'Declare browser features for the exact profile; ambient networking is unavailable.');
|
|
33
37
|
check(Array.isArray(value.operations)&&value.operations.length>0&&value.operations.length<=BROWSER_APPLICATION_LIMITS.operations,'Declare 1–32 output-producing adapter operations.');
|
|
34
38
|
const operations=new Set();for(const operation of value.operations){
|
|
35
|
-
check(exact(operation,['id','kind'])&&typeof operation.id==='string'&&/^[a-z][a-z0-9.-]{0,79}$/.test(operation.id)&&!operations.has(operation.id),'Use distinct named application operations.');
|
|
39
|
+
check((exact(operation,['id','kind'])||exact(operation,['id','kind','outputs']))&&typeof operation.id==='string'&&/^[a-z][a-z0-9.-]{0,79}$/.test(operation.id)&&!operations.has(operation.id),'Use distinct named application operations.');
|
|
36
40
|
check(['export','execute'].includes(operation.kind)&&(value.lifecycle!=='headless'||operation.kind==='execute'),'A headless application must execute without an interactive export.');operations.add(operation.id);
|
|
41
|
+
if(Object.hasOwn(operation,'outputs'))check(Array.isArray(operation.outputs)&&operation.outputs.length>0&&operation.outputs.length<=64&&new Set(operation.outputs).size===operation.outputs.length&&operation.outputs.every(name=>typeof name==='string'&&/^[a-z][a-zA-Z0-9_]*$/.test(name)),'Map each operation to distinct bounded output ports.');
|
|
37
42
|
}
|
|
38
43
|
check(Array.isArray(value.assets)&&value.assets.length>=2&&value.assets.length<=BROWSER_APPLICATION_LIMITS.assets,'Declare 2–2048 bundled regular-file assets.');
|
|
39
44
|
const paths=new Set();let total=0,wasm=false;
|
|
@@ -64,16 +69,25 @@ function decode(asset){
|
|
|
64
69
|
async function bytesDigest(bytes){return Array.from(new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256',bytes)),byte=>byte.toString(16).padStart(2,'0')).join('');}
|
|
65
70
|
/** Checks hashes and returns immutable detached data. This never inspects code
|
|
66
71
|
* for safety, performs a build, starts a browser, or grants any capability. */
|
|
67
|
-
export async function defineBrowserApplication(value){
|
|
72
|
+
export async function defineBrowserApplication(value,{signal}={}){
|
|
73
|
+
signal?.throwIfAborted();
|
|
68
74
|
if(verifiedBundles.has(value))return value;
|
|
69
75
|
metadata(value);const bundle=structuredClone(value);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
try{
|
|
76
|
+
let nextAsset=0,failed=false,firstFailure;
|
|
77
|
+
const verifyAssets=async()=>{
|
|
78
|
+
while(!failed&&nextAsset<bundle.assets.length){
|
|
79
|
+
try{
|
|
80
|
+
signal?.throwIfAborted();const asset=bundle.assets[nextAsset++],bytes=decode(asset);signal?.throwIfAborted();check(await bytesDigest(bytes)===asset.sha256,`Application asset integrity failed: ${asset.path}`);signal?.throwIfAborted();
|
|
81
|
+
if(['text/html','text/javascript','text/css','application/json','application/manifest+json','application/xml','text/xml','text/plain','image/svg+xml'].includes(asset.mime)){
|
|
82
|
+
try{new TextDecoder('utf-8',{fatal:true}).decode(bytes);}catch{check(false,`Application text asset is not UTF-8: ${asset.path}`);}
|
|
83
|
+
}
|
|
84
|
+
}catch(error){if(!failed){failed=true;firstFailure=error;}}
|
|
74
85
|
}
|
|
75
|
-
}
|
|
76
|
-
|
|
86
|
+
};
|
|
87
|
+
// Drain every started verification before rejection or immutable publication.
|
|
88
|
+
await Promise.all(Array.from({length:Math.min(4,bundle.assets.length)},verifyAssets));
|
|
89
|
+
if(failed)throw firstFailure;
|
|
90
|
+
signal?.throwIfAborted();freeze(bundle);verifiedBundles.add(bundle);return bundle;
|
|
77
91
|
}
|
|
78
92
|
/** Exact bundle lookup only: no URL resolution, filesystem access or fetch.
|
|
79
93
|
* Hosts must independently deny navigation, network and unlisted resources. */
|