@markii/lua 0.10.0 → 0.12.0
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/dist/doc.d.ts +89 -0
- package/dist/doc.js +131 -0
- package/dist/executor.d.ts +6 -3
- package/dist/executor.js +7 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/require.d.ts +20 -16
- package/dist/sandbox.d.ts +9 -0
- package/dist/sandbox.js +29 -0
- package/package.json +3 -3
package/dist/doc.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { DocView } from '@markii/runtime';
|
|
2
|
+
import { type MarshalLimits } from './marshal.js';
|
|
3
|
+
/**
|
|
4
|
+
* The `doc` table (GitHub issue #33): a script's read-only view of the
|
|
5
|
+
* note it is written in.
|
|
6
|
+
*
|
|
7
|
+
* `doc.directives(filter)` lists the note's directives in document order,
|
|
8
|
+
* and `doc.value(name)` reads what a script ABOVE this one produced. Both
|
|
9
|
+
* are pure reads of content the note already holds, so unlike `net`,
|
|
10
|
+
* `cache` and `bundle` this table is NOT tier-gated and needs no grant: it
|
|
11
|
+
* is wired identically for a manual run and an auto/scheduled one. There
|
|
12
|
+
* is no write side to gate.
|
|
13
|
+
*
|
|
14
|
+
* ## Why the listing crosses as JSON text
|
|
15
|
+
*
|
|
16
|
+
* For the same reason `net.fetch_json` does (see `./json-decode`): a plain
|
|
17
|
+
* JS object handed to `global.set` arrives in Lua as a wasmoon `js_proxy`
|
|
18
|
+
* userdata, not a genuine table, so `type()`, `#`, `ipairs` and marshaling
|
|
19
|
+
* a piece of it back out all misbehave. A string is a scalar and crosses
|
|
20
|
+
* cleanly, and `__smd_json_decode` rebuilds it into an ordinary Lua table
|
|
21
|
+
* the script cannot tell from one it wrote itself.
|
|
22
|
+
*
|
|
23
|
+
* The decode is repeated on every `doc.directives()` call rather than
|
|
24
|
+
* cached in a Lua local, and that is deliberate: each call therefore hands
|
|
25
|
+
* back FRESH tables. A script that scribbles on an entry it was given
|
|
26
|
+
* cannot make the next call in the same script see its edits, and it never
|
|
27
|
+
* had a way to reach the next script at all, since each script runs in its
|
|
28
|
+
* own engine (`./sandbox`'s `runScript` builds and closes one per script).
|
|
29
|
+
* The JSON itself is built once, on the JS side, and reused.
|
|
30
|
+
*
|
|
31
|
+
* ## What a rejected read looks like
|
|
32
|
+
*
|
|
33
|
+
* Reading a name that belongs to a script further down the note is a
|
|
34
|
+
* script-authoring mistake, not a permission or resource problem. It is
|
|
35
|
+
* therefore NOT recorded on `./capabilities`' `CapabilityDenials` handle:
|
|
36
|
+
* recording it there would classify the run as `'capability-denied'` and
|
|
37
|
+
* the host would tell the user their note needs a permission it does not
|
|
38
|
+
* need. It stays a `'runtime'` failure, which `./executor` maps to
|
|
39
|
+
* `'script-error'`, so the marker reads "script error: reads "quiz", which
|
|
40
|
+
* runs later in the note".
|
|
41
|
+
*
|
|
42
|
+
* It does get its own out-of-band record ({@link DocRejections}), for the
|
|
43
|
+
* same reason the capability denials have one: the message that comes back
|
|
44
|
+
* OUT of Lua is wrapped ("Error: ..." plus a stack traceback) and may have
|
|
45
|
+
* been rewritten by the script's own `pcall`/`error` games, so the host
|
|
46
|
+
* would otherwise put a traceback in a tooltip. `./sandbox` reads the
|
|
47
|
+
* recorded sentence instead, which the script can neither see nor forge.
|
|
48
|
+
*/
|
|
49
|
+
export interface DocConfig {
|
|
50
|
+
/**
|
|
51
|
+
* This script's view of its note, built by `@markii/runtime`'s
|
|
52
|
+
* `runDocumentScripts` from the listing the host handed it. Absent, the
|
|
53
|
+
* `doc` table is still wired, with an empty listing and a `value` that
|
|
54
|
+
* answers nil for every name: a script must never meet a `doc` that is
|
|
55
|
+
* nil, or the first thing an author writes fails as "attempt to index a
|
|
56
|
+
* nil value" with nothing to explain it.
|
|
57
|
+
*/
|
|
58
|
+
doc?: DocView;
|
|
59
|
+
/**
|
|
60
|
+
* The depth/node budget a value read through `doc.value` is checked
|
|
61
|
+
* against before it is handed to Lua, exactly as `net.fetch_json` and
|
|
62
|
+
* `cache.get` check theirs. A stored value has already passed this same
|
|
63
|
+
* budget on its way OUT of the script that produced it, so this is a
|
|
64
|
+
* second, defensive reading rather than the first one.
|
|
65
|
+
*/
|
|
66
|
+
marshalLimits?: MarshalLimits;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Non-spoofable record of the LAST `doc` read this run refused. A plain JS
|
|
70
|
+
* closure, exactly like `./capabilities`' `CapabilityDenials`: no Lua
|
|
71
|
+
* value, no metatable, nothing a script can read or write. `./sandbox`
|
|
72
|
+
* consults it (after the capability handle, which outranks it) to report
|
|
73
|
+
* the clean sentence rather than whatever came back through Lua.
|
|
74
|
+
*/
|
|
75
|
+
export interface DocRejections {
|
|
76
|
+
last(): string | undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds the raw host functions and the trusted Lua prelude defining the
|
|
80
|
+
* `doc` table. Shaped like `./capabilities`' `buildCapabilities` (raw flat
|
|
81
|
+
* globals captured into prelude locals, then nil'd out) so the two follow
|
|
82
|
+
* one pattern; see that function's doc comment for why the ergonomic
|
|
83
|
+
* wrappers must be Lua-native tables rather than JS objects.
|
|
84
|
+
*/
|
|
85
|
+
export declare function buildDoc(config: DocConfig): {
|
|
86
|
+
rawGlobals: Record<string, (...args: never[]) => Promise<unknown>>;
|
|
87
|
+
preludeLua: string;
|
|
88
|
+
rejections: DocRejections;
|
|
89
|
+
};
|
package/dist/doc.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { buildJsonDecodePrelude } from './json-decode.js';
|
|
2
|
+
import { checkJsonWithinLimits, DEFAULT_MARSHAL_LIMITS, } from './marshal.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the raw host functions and the trusted Lua prelude defining the
|
|
5
|
+
* `doc` table. Shaped like `./capabilities`' `buildCapabilities` (raw flat
|
|
6
|
+
* globals captured into prelude locals, then nil'd out) so the two follow
|
|
7
|
+
* one pattern; see that function's doc comment for why the ergonomic
|
|
8
|
+
* wrappers must be Lua-native tables rather than JS objects.
|
|
9
|
+
*/
|
|
10
|
+
export function buildDoc(config) {
|
|
11
|
+
const view = config.doc;
|
|
12
|
+
const limits = config.marshalLimits ?? DEFAULT_MARSHAL_LIMITS;
|
|
13
|
+
let lastRejection;
|
|
14
|
+
/** Records the clean sentence, THEN throws it — same order every denial site in `./capabilities` uses. */
|
|
15
|
+
function reject(message) {
|
|
16
|
+
lastRejection = message;
|
|
17
|
+
return new Error(message);
|
|
18
|
+
}
|
|
19
|
+
let listingJson;
|
|
20
|
+
const rawGlobals = {};
|
|
21
|
+
rawGlobals.__smd_doc_listing_raw = (async () => {
|
|
22
|
+
if (listingJson === undefined) {
|
|
23
|
+
// The listing is already capped, sanitized plain data
|
|
24
|
+
// (`@markii/runtime`'s `buildDirectiveListing`), so this cannot
|
|
25
|
+
// throw on a cycle or a non-serializable member. The `?? '[]'` is
|
|
26
|
+
// the belt for a host that supplied something stranger than a
|
|
27
|
+
// listing: an empty list is always a truthful answer, an exception
|
|
28
|
+
// never is.
|
|
29
|
+
try {
|
|
30
|
+
listingJson = JSON.stringify(view?.directives.directives ?? []) ?? '[]';
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
listingJson = '[]';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return listingJson;
|
|
37
|
+
});
|
|
38
|
+
rawGlobals.__smd_doc_value_raw = (async (name) => {
|
|
39
|
+
if (!view)
|
|
40
|
+
return undefined;
|
|
41
|
+
const read = view.value(typeof name === 'string' ? name : '');
|
|
42
|
+
if (!read.ok) {
|
|
43
|
+
// `@markii/runtime` owns this sentence; this module only carries it.
|
|
44
|
+
throw reject(read.message);
|
|
45
|
+
}
|
|
46
|
+
const value = read.value;
|
|
47
|
+
if (value === undefined || value === null)
|
|
48
|
+
return undefined;
|
|
49
|
+
const budget = checkJsonWithinLimits(value, limits);
|
|
50
|
+
if (!budget.ok) {
|
|
51
|
+
throw reject(`doc.value("${name}") ${budget.message}`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const encoded = JSON.stringify(value);
|
|
55
|
+
return encoded === undefined ? undefined : encoded;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw reject(`doc.value("${name}") could not be read as a value`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const truncated = view?.directives.truncated === true;
|
|
62
|
+
// `buildJsonDecodePrelude` is emitted here rather than assumed: this
|
|
63
|
+
// module is exercised standalone in its own tests, and `./capabilities`
|
|
64
|
+
// only emits it when `net`/`cache` are wired. Emitting it twice in one
|
|
65
|
+
// run simply redefines the same idempotent trusted function, which is
|
|
66
|
+
// the same reasoning `./capabilities`' own `ensureMarshalPrelude` uses.
|
|
67
|
+
//
|
|
68
|
+
// It defines `__smd_json_decode` as a GLOBAL, though, and `doc` is wired
|
|
69
|
+
// for every run, so emitting it unconditionally would leave that private
|
|
70
|
+
// name reachable from user code in runs that never wire `net`/`cache` --
|
|
71
|
+
// exactly the residue `require-pass3.probe.test.ts` fails the suite for.
|
|
72
|
+
// So the previous value is saved before and put back after: this module
|
|
73
|
+
// pins its own copy in a local and leaves the globals table exactly as
|
|
74
|
+
// it found it. Every other consumer pins its own copy at ITS prelude
|
|
75
|
+
// time too (`./capabilities`), so restoring a nil here can never take a
|
|
76
|
+
// decoder away from anyone.
|
|
77
|
+
const preludeLua = `local __smd_doc_prev_decode = __smd_json_decode
|
|
78
|
+
${buildJsonDecodePrelude(limits)}
|
|
79
|
+
local __smd_doc_listing = __smd_doc_listing_raw
|
|
80
|
+
local __smd_doc_value = __smd_doc_value_raw
|
|
81
|
+
-- Every primitive this table needs is pinned HERE, at prelude-definition
|
|
82
|
+
-- time, never resolved as a global inside the wrappers -- the same
|
|
83
|
+
-- discipline \`./json-decode\` and \`./capabilities\` follow (adversarial
|
|
84
|
+
-- findings A1/A2). Without it, a script could rebind \`__smd_json_decode\`
|
|
85
|
+
-- or \`type\` before calling \`doc.directives\` and change what its own
|
|
86
|
+
-- guards do.
|
|
87
|
+
local __smd_doc_decode = __smd_json_decode
|
|
88
|
+
local __smd_doc_type, __smd_doc_error = type, error
|
|
89
|
+
__smd_json_decode = __smd_doc_prev_decode
|
|
90
|
+
__smd_doc_listing_raw = nil
|
|
91
|
+
__smd_doc_value_raw = nil
|
|
92
|
+
|
|
93
|
+
doc = {}
|
|
94
|
+
doc.truncated = ${truncated ? 'true' : 'false'}
|
|
95
|
+
|
|
96
|
+
doc.directives = function(filter)
|
|
97
|
+
local wanted = nil
|
|
98
|
+
if filter ~= nil then
|
|
99
|
+
if __smd_doc_type(filter) ~= "table" then
|
|
100
|
+
__smd_doc_error("doc.directives(filter): filter must be a table")
|
|
101
|
+
end
|
|
102
|
+
wanted = filter.name
|
|
103
|
+
if wanted ~= nil and __smd_doc_type(wanted) ~= "string" then
|
|
104
|
+
__smd_doc_error("doc.directives(filter): filter.name must be a string")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
local entries = __smd_doc_decode(__smd_doc_listing():await())
|
|
108
|
+
if wanted == nil then return entries end
|
|
109
|
+
local out = {}
|
|
110
|
+
local n = 0
|
|
111
|
+
for i = 1, #entries do
|
|
112
|
+
local entry = entries[i]
|
|
113
|
+
if entry.name == wanted then
|
|
114
|
+
n = n + 1
|
|
115
|
+
out[n] = entry
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
return out
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
doc.value = function(name)
|
|
122
|
+
if __smd_doc_type(name) ~= "string" then
|
|
123
|
+
__smd_doc_error("doc.value(name): name must be a string")
|
|
124
|
+
end
|
|
125
|
+
local text = __smd_doc_value(name):await()
|
|
126
|
+
if text == nil then return nil end
|
|
127
|
+
return __smd_doc_decode(text)
|
|
128
|
+
end
|
|
129
|
+
`;
|
|
130
|
+
return { rawGlobals, preludeLua, rejections: { last: () => lastRejection } };
|
|
131
|
+
}
|
package/dist/executor.d.ts
CHANGED
|
@@ -16,13 +16,16 @@ import { type RunScriptOptions } from './sandbox.js';
|
|
|
16
16
|
* cycle.
|
|
17
17
|
*/
|
|
18
18
|
/**
|
|
19
|
-
* Everything `runScript` accepts except `code`/`tier` — those
|
|
20
|
-
* supplied per call by `runDocumentScripts` (`@markii/runtime`)
|
|
19
|
+
* Everything `runScript` accepts except `code`/`tier`/`doc` — those three
|
|
20
|
+
* are supplied per call by `runDocumentScripts` (`@markii/runtime`): the
|
|
21
|
+
* first two per the `ScriptExecutor` contract, and `doc` because a note
|
|
22
|
+
* view is per-SCRIPT (it says which values are already available), so it
|
|
23
|
+
* can never be captured once at construction time; everything
|
|
21
24
|
* else (net/netGrants/cache/bundle/maxFetchBytes/limits/marshalLimits) is
|
|
22
25
|
* this note's fixed capability/resource configuration, closed over once at
|
|
23
26
|
* executor-construction time.
|
|
24
27
|
*/
|
|
25
|
-
export type LuaExecutorConfig = Omit<RunScriptOptions, 'code' | 'tier'>;
|
|
28
|
+
export type LuaExecutorConfig = Omit<RunScriptOptions, 'code' | 'tier' | 'doc'>;
|
|
26
29
|
/**
|
|
27
30
|
* Builds a `ScriptExecutor` (`@markii/runtime`) backed by this package's
|
|
28
31
|
* `runScript`. `config` is captured once and reused for every script the
|
package/dist/executor.js
CHANGED
|
@@ -42,8 +42,13 @@ function toRuntimeFailureKind(failure) {
|
|
|
42
42
|
* the stored error).
|
|
43
43
|
*/
|
|
44
44
|
export function createLuaExecutor(config = {}) {
|
|
45
|
-
return async ({ code, tier }) => {
|
|
46
|
-
const result = await runScript({
|
|
45
|
+
return async ({ code, tier, doc }) => {
|
|
46
|
+
const result = await runScript({
|
|
47
|
+
...config,
|
|
48
|
+
code,
|
|
49
|
+
tier,
|
|
50
|
+
...(doc ? { doc } : {}),
|
|
51
|
+
});
|
|
47
52
|
if (result.ok) {
|
|
48
53
|
return { ok: true, value: result.value };
|
|
49
54
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export type { LimitHandle, ScriptLimits } from './limits.js';
|
|
|
6
6
|
export { DEFAULT_LIMITS, installLimits } from './limits.js';
|
|
7
7
|
export type { CacheEntry, CacheProvider, CapabilityConfig, CapabilityDenial, CapabilityDenials, CapabilityTier, NetGrants, NetProvider, NetResponse, } from './capabilities.js';
|
|
8
8
|
export { DEFAULT_MAX_FETCH_BYTES, bytesToLuaString, buildCapabilities, isNetProviderDenial, luaStringToBytes, netProviderDenial, } from './capabilities.js';
|
|
9
|
+
export type { DocConfig } from './doc.js';
|
|
10
|
+
export { buildDoc } from './doc.js';
|
|
9
11
|
export type { MarshalLimits } from './marshal.js';
|
|
10
12
|
export { DEFAULT_MARSHAL_LIMITS, buildMarshalPrelude, checkJsonWithinLimits, finalizeMarshaledValue, wrapUserCode, } from './marshal.js';
|
|
11
13
|
export type { PackModuleResolver, RequireConfig } from './require.js';
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { CAPABILITY_ERROR_TAG, FETCH_DECODE_ERROR_TAG, MARSHAL_ERROR_TAG, Script
|
|
|
7
7
|
export { ALLOWED_GLOBALS, DENIED_GLOBALS, createEmptyLuaEngine, } from './globals.js';
|
|
8
8
|
export { DEFAULT_LIMITS, installLimits } from './limits.js';
|
|
9
9
|
export { DEFAULT_MAX_FETCH_BYTES, bytesToLuaString, buildCapabilities, isNetProviderDenial, luaStringToBytes, netProviderDenial, } from './capabilities.js';
|
|
10
|
+
export { buildDoc } from './doc.js';
|
|
10
11
|
export { DEFAULT_MARSHAL_LIMITS, buildMarshalPrelude, checkJsonWithinLimits, finalizeMarshaledValue, wrapUserCode, } from './marshal.js';
|
|
11
12
|
export { buildRequire } from './require.js';
|
|
12
13
|
export { runScript } from './sandbox.js';
|
package/dist/require.d.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { ScriptView } from '@markii/bundle';
|
|
2
2
|
/**
|
|
3
3
|
* Sandboxed `require` (spec §8 "Long scripts and shared code", docs/
|
|
4
|
-
* scripting.md): exactly
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
4
|
+
* scripting.md): there are exactly TWO sources of shared Lua, and this
|
|
5
|
+
* module implements both — bundle-local modules, and pack modules (the
|
|
6
|
+
* `PackModuleResolver` seam below, which a host wires to its own installed
|
|
7
|
+
* packs, e.g. `@markii/host`'s `createPackModuleResolver`). There is no
|
|
8
|
+
* third source: a folder of shared Lua with nothing to render is simply a
|
|
9
|
+
* pack whose manifest declares no components (`"components": {}`) and
|
|
10
|
+
* carries its own `scripts/*.lua` — it is a pack module like any other, not
|
|
11
|
+
* a separate mechanism. Both sources share ONE property: every require
|
|
12
|
+
* target this module resolves is PURE LUA SOURCE TEXT, loaded as a fresh
|
|
13
|
+
* PROTECTED CHUNK on the SAME thread as the rest of the run, so it shares
|
|
14
|
+
* that run's globals, capabilities, and instruction/wall-clock/memory
|
|
15
|
+
* budget — a module can never grant itself more than the script that
|
|
16
|
+
* required it already had.
|
|
15
17
|
*
|
|
16
18
|
* ## Two sources, told apart by the first path segment (docs/scripting.md)
|
|
17
19
|
*
|
|
@@ -28,11 +30,13 @@ import type { ScriptView } from '@markii/bundle';
|
|
|
28
30
|
* no separate tier gate to apply on top of what `bundle.read` already
|
|
29
31
|
* enforces.
|
|
30
32
|
* - **Pack-namespaced**: `require "ana/http"` (first segment anything
|
|
31
|
-
* else). This
|
|
32
|
-
* `PackModuleResolver`.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* else). This package only defines the seam: an optional injected
|
|
34
|
+
* `PackModuleResolver`. A host that has wired up pack installation
|
|
35
|
+
* (`@markii/host`'s `discoverPacks` + `loadPackModules` +
|
|
36
|
+
* `createPackModuleResolver`) passes a real resolver in; with none
|
|
37
|
+
* configured, a pack-namespaced `require` fails cleanly as a capability
|
|
38
|
+
* denial — never a crash, never a fallthrough into filesystem or network
|
|
39
|
+
* access.
|
|
36
40
|
*
|
|
37
41
|
* ## Why a real `require` needs `load` back, carefully
|
|
38
42
|
*
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ScriptView } from '@markii/bundle';
|
|
2
|
+
import type { DocView } from '@markii/runtime';
|
|
2
3
|
import { type CacheProvider, type CapabilityTier, type NetGrants, type NetProvider } from './capabilities.js';
|
|
3
4
|
import type { ScriptFailure } from './errors.js';
|
|
4
5
|
import { type ScriptLimits } from './limits.js';
|
|
@@ -13,6 +14,14 @@ export interface RunScriptOptions {
|
|
|
13
14
|
cache?: CacheProvider;
|
|
14
15
|
/** Bundle-scoped filesystem (spec §11), already capability-restricted — see `@markii/bundle`'s `createScriptView`. Also backs bundle-local `require "scripts/..."` (`./require`) — the SAME `ScriptView`, so a module require goes through the identical path-jail and read-permission check as `bundle.read`. */
|
|
15
16
|
bundle?: ScriptView;
|
|
17
|
+
/**
|
|
18
|
+
* This script's read-only view of the note it runs in (GitHub issue #33,
|
|
19
|
+
* `./doc`), supplied per script by `@markii/runtime`'s
|
|
20
|
+
* `runDocumentScripts`. Omitted (a standalone `runScript` call), the
|
|
21
|
+
* `doc` table is still defined, with an empty listing and a `value` that
|
|
22
|
+
* answers nil: a script never meets a nil `doc`.
|
|
23
|
+
*/
|
|
24
|
+
doc?: DocView;
|
|
16
25
|
/** Optional pack-module `require` seam (`./require`'s `PackModuleResolver`) — resolves `require "packName/modulePath"`. Omitted (the default: no host wires packs yet), every pack-namespaced `require` fails as a clean capability denial, never a crash. */
|
|
17
26
|
packModuleResolver?: PackModuleResolver;
|
|
18
27
|
maxFetchBytes?: number;
|
package/dist/sandbox.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LuaReturn } from 'wasmoon';
|
|
2
2
|
import { buildCapabilities, } from './capabilities.js';
|
|
3
|
+
import { buildDoc } from './doc.js';
|
|
3
4
|
import { MARSHAL_ERROR_TAG, ScriptLimitError } from './errors.js';
|
|
4
5
|
import { createEmptyLuaEngine } from './globals.js';
|
|
5
6
|
import { DEFAULT_LIMITS, installLimits } from './limits.js';
|
|
@@ -260,6 +261,19 @@ export async function runScript(options) {
|
|
|
260
261
|
throw new Error(`sandbox assembly left a code-loading primitive reachable (${String(loadResidue)}); refusing to run`);
|
|
261
262
|
}
|
|
262
263
|
await engine.doString(buildMarshalPrelude(marshalLimits));
|
|
264
|
+
// ./doc: the note-scoped read-only view (GitHub issue #33). Wired
|
|
265
|
+
// AFTER the marshal prelude and, like `require`, wired unconditionally
|
|
266
|
+
// — with no `options.doc` it is an empty listing, never an absent
|
|
267
|
+
// global. It records nothing on `denials`: nothing here is a
|
|
268
|
+
// capability, so nothing here may classify a run as capability-denied.
|
|
269
|
+
const docBuild = buildDoc({
|
|
270
|
+
...(options.doc ? { doc: options.doc } : {}),
|
|
271
|
+
marshalLimits,
|
|
272
|
+
});
|
|
273
|
+
for (const [name, fn] of Object.entries(docBuild.rawGlobals)) {
|
|
274
|
+
engine.global.set(name, fn);
|
|
275
|
+
}
|
|
276
|
+
await engine.doString(docBuild.preludeLua);
|
|
263
277
|
thread = engine.global.newThread();
|
|
264
278
|
threadStackIndex = engine.global.getTop();
|
|
265
279
|
limitHandle = installLimits(thread, limits);
|
|
@@ -386,6 +400,21 @@ export async function runScript(options) {
|
|
|
386
400
|
},
|
|
387
401
|
};
|
|
388
402
|
}
|
|
403
|
+
// A refused `doc` read (`./doc`, GitHub issue #33), recorded the
|
|
404
|
+
// same out-of-band way and checked AFTER the capability handle,
|
|
405
|
+
// which outranks it: a genuine denial is the more important thing to
|
|
406
|
+
// tell the user about. This is NOT a capability failure, so it stays
|
|
407
|
+
// a `'runtime'` kind (`'script-error'` once `./executor` maps it);
|
|
408
|
+
// the handle exists only so the reported MESSAGE is the clean
|
|
409
|
+
// sentence rather than the Lua-wrapped one, which arrives prefixed
|
|
410
|
+
// with "Error: " and trailing a stack traceback. It carries the same
|
|
411
|
+
// accepted edge as the denial handle above: a script that provokes a
|
|
412
|
+
// refusal, swallows it with its own `pcall`, and then fails for an
|
|
413
|
+
// unrelated reason is still reported against the refusal.
|
|
414
|
+
const rejection = docBuild.rejections.last();
|
|
415
|
+
if (rejection) {
|
|
416
|
+
return { ok: false, error: { kind: 'runtime', message: rejection } };
|
|
417
|
+
}
|
|
389
418
|
return { ok: false, error: classifyRuntimeError(runResult.err) };
|
|
390
419
|
}
|
|
391
420
|
const finalized = finalizeMarshaledValue(runResult.value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/lua",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Sandboxed Lua 5.4 (wasmoon) execution runtime for Mark's document scripting: an empty-env global whitelist, two-tier capability-gated net/cache/bundle access, instruction-count/wall-clock/memory limits, and depth/size-capped Lua<->JS marshaling.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"lint": "eslint ."
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@markii/bundle": "0.
|
|
48
|
-
"@markii/runtime": "0.
|
|
47
|
+
"@markii/bundle": "0.12.0",
|
|
48
|
+
"@markii/runtime": "0.12.0",
|
|
49
49
|
"wasmoon": "^1.16.0"
|
|
50
50
|
}
|
|
51
51
|
}
|