@juspay/neurolink 13.8.17 → 13.8.19
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/CHANGELOG.md +5 -2
- package/README.md +1 -1
- package/dist/action/actionExecutor.d.ts +33 -3
- package/dist/action/actionExecutor.js +71 -15
- package/dist/action/actionInputs.js +4 -1
- package/dist/action/cliVersion.d.ts +25 -0
- package/dist/action/cliVersion.js +26 -0
- package/dist/action/index.d.ts +2 -1
- package/dist/action/index.js +2 -1
- package/dist/adapters/video/directorPipeline.js +46 -36
- package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
- package/dist/adapters/video/ffmpegAdapter.js +32 -5
- package/dist/adapters/video/frameExtractor.js +1 -1
- package/dist/adapters/video/videoMerger.js +5 -2
- package/dist/agent/arithmeticEvaluator.d.ts +32 -0
- package/dist/agent/arithmeticEvaluator.js +349 -0
- package/dist/agent/directTools.d.ts +6 -18
- package/dist/agent/directTools.js +17 -56
- package/dist/artifacts/artifactStore.d.ts +22 -1
- package/dist/artifacts/artifactStore.js +66 -6
- package/dist/auth/anthropicOAuth.d.ts +26 -1
- package/dist/auth/anthropicOAuth.js +81 -5
- package/dist/auth/secureFiles.d.ts +37 -0
- package/dist/auth/secureFiles.js +95 -0
- package/dist/auth/tokenStore.d.ts +43 -6
- package/dist/auth/tokenStore.js +260 -79
- package/dist/avatar/providers/DIDAvatar.js +49 -45
- package/dist/avatar/providers/ReplicateAvatar.js +33 -40
- package/dist/browser/neurolink.min.js +485 -558
- package/dist/cli/commands/auth.js +112 -70
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +128 -13
- package/dist/cli/commands/serve.d.ts +10 -1
- package/dist/cli/commands/serve.js +98 -17
- package/dist/cli/commands/server.js +17 -5
- package/dist/cli/index.js +146 -5
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/envManager.js +17 -3
- package/dist/cli/utils/serverUtils.d.ts +39 -3
- package/dist/cli/utils/serverUtils.js +63 -6
- package/dist/core/baseProvider.js +7 -1
- package/dist/core/modules/MessageBuilder.js +5 -0
- package/dist/core/modules/ToolsManager.d.ts +46 -1
- package/dist/core/modules/ToolsManager.js +190 -43
- package/dist/files/fileReferenceRegistry.d.ts +2 -23
- package/dist/files/fileReferenceRegistry.js +55 -5
- package/dist/hitl/hitlManager.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/mcp/auth/tokenStorage.d.ts +7 -0
- package/dist/mcp/auth/tokenStorage.js +23 -4
- package/dist/mcp/externalServerManager.d.ts +13 -3
- package/dist/mcp/externalServerManager.js +154 -136
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/mcpClientFactory.d.ts +14 -0
- package/dist/mcp/mcpClientFactory.js +70 -7
- package/dist/mcp/projectConfigTrust.d.ts +72 -0
- package/dist/mcp/projectConfigTrust.js +237 -0
- package/dist/mcp/transportUrlPolicy.d.ts +39 -0
- package/dist/mcp/transportUrlPolicy.js +172 -0
- package/dist/music/providers/BeatovenMusic.js +35 -46
- package/dist/music/providers/ReplicateMusic.js +33 -40
- package/dist/neurolink.d.ts +16 -1
- package/dist/neurolink.js +64 -11
- package/dist/processors/archive/ArchiveProcessor.js +15 -1
- package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
- package/dist/processors/archive/zipEntryIndex.js +33 -0
- package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
- package/dist/processors/base/BaseFileProcessor.js +40 -111
- package/dist/processors/data/YamlProcessor.d.ts +13 -1
- package/dist/processors/data/YamlProcessor.js +93 -2
- package/dist/processors/document/OpenDocumentProcessor.js +7 -2
- package/dist/processors/document/PptxProcessor.js +12 -7
- package/dist/processors/media/VideoProcessor.js +93 -15
- package/dist/providers/openaiChatCompletionsClient.js +12 -3
- package/dist/proxy/accountLedger.js +6 -8
- package/dist/proxy/geminiFormat.d.ts +3 -11
- package/dist/proxy/geminiFormat.js +11 -2
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/openaiFormat.d.ts +3 -9
- package/dist/proxy/openaiFormat.js +21 -15
- package/dist/proxy/proxyTranslationEngine.d.ts +2 -6
- package/dist/proxy/proxyTranslationEngine.js +25 -14
- package/dist/proxy/routingPolicy.d.ts +3 -1
- package/dist/proxy/routingPolicy.js +7 -8
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +13 -1
- package/dist/proxy/updateState.js +4 -4
- package/dist/rag/document/loaders.d.ts +0 -6
- package/dist/rag/document/loaders.js +16 -10
- package/dist/rag/ragIntegration.d.ts +2 -2
- package/dist/rag/ragIntegration.js +35 -10
- package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
- package/dist/rag/retrieval/vectorQueryTool.js +54 -17
- package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
- package/dist/server/abstract/baseServerAdapter.js +119 -6
- package/dist/server/adapters/expressAdapter.d.ts +3 -0
- package/dist/server/adapters/expressAdapter.js +78 -6
- package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
- package/dist/server/adapters/fastifyAdapter.js +165 -70
- package/dist/server/adapters/honoAdapter.d.ts +9 -1
- package/dist/server/adapters/honoAdapter.js +99 -21
- package/dist/server/adapters/koaAdapter.d.ts +1 -0
- package/dist/server/adapters/koaAdapter.js +153 -74
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/middleware/auth.js +6 -2
- package/dist/server/middleware/cache.js +6 -1
- package/dist/server/middleware/rateLimit.js +29 -4
- package/dist/server/routes/agentRoutes.js +24 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +273 -84
- package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
- package/dist/server/routes/codexProxyRoutes.js +125 -40
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +35 -4
- package/dist/server/routes/mcpRoutes.d.ts +6 -2
- package/dist/server/routes/mcpRoutes.js +21 -7
- package/dist/server/routes/memoryRoutes.js +7 -6
- package/dist/server/routes/toolRoutes.d.ts +12 -2
- package/dist/server/routes/toolRoutes.js +33 -3
- package/dist/server/routes/transcribeRoutes.js +60 -13
- package/dist/server/utils/clientAddress.d.ts +23 -0
- package/dist/server/utils/clientAddress.js +65 -0
- package/dist/server/utils/clientError.d.ts +35 -0
- package/dist/server/utils/clientError.js +129 -0
- package/dist/server/utils/pathMatch.d.ts +21 -0
- package/dist/server/utils/pathMatch.js +41 -0
- package/dist/server/utils/validation.d.ts +9 -0
- package/dist/server/utils/validation.js +9 -0
- package/dist/server/websocket/WebSocketHandler.js +22 -3
- package/dist/server/websocket/frameQueue.d.ts +19 -0
- package/dist/server/websocket/frameQueue.js +41 -3
- package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
- package/dist/server/websocket/transcribeWebSocket.js +223 -34
- package/dist/skills/skillStores.js +5 -0
- package/dist/tasks/store/fileTaskStore.d.ts +2 -0
- package/dist/tasks/store/fileTaskStore.js +11 -4
- package/dist/types/attachments.d.ts +117 -0
- package/dist/types/attachments.js +15 -0
- package/dist/types/auth.d.ts +6 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/config.d.ts +56 -0
- package/dist/types/file.d.ts +8 -0
- package/dist/types/hitl.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/mcp.d.ts +98 -0
- package/dist/types/multimodal.js +0 -41
- package/dist/types/openaiCompatible.d.ts +1 -1
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +71 -16
- package/dist/types/proxyClient.d.ts +2 -0
- package/dist/types/rag.d.ts +9 -0
- package/dist/types/safeFetch.d.ts +40 -0
- package/dist/types/server.d.ts +206 -10
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/transcribe.d.ts +6 -0
- package/dist/utils/attachmentPolicy.d.ts +89 -0
- package/dist/utils/attachmentPolicy.js +327 -0
- package/dist/utils/dotenvBootstrap.d.ts +48 -19
- package/dist/utils/dotenvBootstrap.js +172 -31
- package/dist/utils/fileDetector.js +115 -65
- package/dist/utils/json/coerce.js +11 -1
- package/dist/utils/logSanitize.d.ts +5 -0
- package/dist/utils/logSanitize.js +8 -0
- package/dist/utils/logger.js +73 -1
- package/dist/utils/messageBuilder.d.ts +2 -2
- package/dist/utils/messageBuilder.js +118 -105
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +2 -0
- package/dist/utils/redis.d.ts +12 -0
- package/dist/utils/redis.js +72 -0
- package/dist/utils/retryAfter.d.ts +32 -0
- package/dist/utils/retryAfter.js +135 -11
- package/dist/utils/safeFetch.d.ts +22 -1
- package/dist/utils/safeFetch.js +132 -38
- package/dist/utils/safeStoreId.d.ts +19 -0
- package/dist/utils/safeStoreId.js +29 -0
- package/dist/utils/schemaConversion.d.ts +30 -3
- package/dist/utils/schemaConversion.js +457 -84
- package/dist/utils/sizeGuard.js +2 -2
- package/dist/utils/ssrfGuard.d.ts +3 -3
- package/dist/utils/ssrfGuard.js +71 -39
- package/dist/voice/eval/sttEvalStore.d.ts +26 -3
- package/dist/voice/eval/sttEvalStore.js +77 -12
- package/dist/voice/livekit/agentDefinition.d.ts +18 -0
- package/dist/voice/livekit/agentDefinition.js +22 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
- package/dist/voice/livekit/realtimeMcpTools.js +8 -7
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
- package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
- package/dist/voice/livekit/voiceAgent.d.ts +2 -2
- package/dist/voice/livekit/voiceAgent.js +2 -1
- package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
- package/dist/voice/livekit/xaiSessionWatch.js +30 -0
- package/dist/voice/providers/GeminiLive.js +21 -6
- package/dist/voice/providers/GoogleSTT.js +8 -5
- package/dist/voice/transcribe.d.ts +2 -2
- package/dist/voice/transcribe.js +34 -18
- package/dist/voice/whistle/audio.js +11 -5
- package/dist/workflow/workflows/adaptiveWorkflow.d.ts +2 -2
- package/dist/workflow/workflows/adaptiveWorkflow.js +10 -10
- package/dist/workflow/workflows/consensusWorkflow.d.ts +4 -4
- package/dist/workflow/workflows/consensusWorkflow.js +12 -12
- package/dist/workflow/workflows/fallbackWorkflow.d.ts +3 -3
- package/dist/workflow/workflows/fallbackWorkflow.js +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.d.ts +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.js +31 -31
- package/docs-site/static/search-index.json +74 -52
- package/package.json +173 -156
- package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
## [13.8.
|
|
1
|
+
## [13.8.19](https://github.com/juspay/neurolink/compare/v13.8.18...v13.8.19) (2026-10-27)
|
|
2
2
|
|
|
3
3
|
### Bug Fixes
|
|
4
4
|
|
|
5
|
-
- **(
|
|
5
|
+
- **(providers):** keep parallel tool calls apart when SSE deltas carry no index ([e410864](https://github.com/juspay/neurolink/commit/e41086491f546e1bbc350b7961bcdd30341891aa))
|
|
6
|
+
- **(proxy):** correct cached usage accounting ([776ed29](https://github.com/juspay/neurolink/commit/776ed29386dd6f555fc20df4b57e55d6745d46b7))
|
|
7
|
+
- **(retry-after):** prevent parseInt date and duration truncation, handle delta seconds ([#2047](https://github.com/juspay/neurolink/issues/2047)) ([62290aa](https://github.com/juspay/neurolink/commit/62290aa62a80e00b836bdaee1b0023db856b33c2))
|
|
8
|
+
- **(workflow):** replace the retired Gemini 2.0, Claude 3 and GPT-3.5 models in the preset workflows ([3324430](https://github.com/juspay/neurolink/commit/33244306fe72b1343efcbc6784e4f9761644fda5))
|
|
6
9
|
|
|
7
10
|
## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
|
|
8
11
|
|
package/README.md
CHANGED
|
@@ -1167,7 +1167,7 @@ node your-app.js
|
|
|
1167
1167
|
Run AI-powered workflows directly in GitHub Actions with broad provider support and automatic PR/issue commenting.
|
|
1168
1168
|
|
|
1169
1169
|
```yaml
|
|
1170
|
-
- uses: juspay/neurolink@
|
|
1170
|
+
- uses: juspay/neurolink@e42b4db89fa87946e2dd766cf2ab43929c689b8b # v13.8.16
|
|
1171
1171
|
with:
|
|
1172
1172
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
1173
1173
|
prompt: "Review this PR for security issues and code quality"
|
|
@@ -12,17 +12,47 @@ export declare function transformCliResponse(cliResponse: CliResponse): Omit<Act
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function buildCliArgs(inputs: ActionInputs): string[];
|
|
14
14
|
/**
|
|
15
|
-
* Install NeuroLink CLI
|
|
15
|
+
* Install the NeuroLink CLI into a private directory under RUNNER_TEMP and
|
|
16
|
+
* return the path of its `neurolink` executable.
|
|
17
|
+
*
|
|
18
|
+
* Three things changed from the original `npm install -g @juspay/neurolink`:
|
|
19
|
+
*
|
|
20
|
+
* - The version is exact unless the consumer opts into `latest`. The action
|
|
21
|
+
* runs with the consumer's provider keys in its environment, so a moving tag
|
|
22
|
+
* would hand one bad npm publish to every consumer within minutes; pinning
|
|
23
|
+
* the action itself by SHA cannot protect against that, only pinning what it
|
|
24
|
+
* installs can. `latest` still works, with a warning that names the trade.
|
|
25
|
+
* - `--ignore-scripts`: no lifecycle script of any package in the tree runs on
|
|
26
|
+
* the consumer's runner. NeuroLink has none of its own, and the packages
|
|
27
|
+
* whose scripts matter (ffmpeg-static's binary download, sharp's prebuilt
|
|
28
|
+
* lookup) are optional dependencies the action's text generation path does
|
|
29
|
+
* not use. Video inputs need an ffmpeg on the runner's PATH, which GitHub's
|
|
30
|
+
* Ubuntu images provide.
|
|
31
|
+
* - `npm audit signatures` after the install: every package in the tree must
|
|
32
|
+
* carry a valid registry signature, and the ones published with provenance
|
|
33
|
+
* (this package is) must carry an attestation that matches. A tarball that
|
|
34
|
+
* was swapped or re-signed fails here instead of running.
|
|
35
|
+
*
|
|
36
|
+
* A project directory rather than `-g` because `npm audit signatures` reads a
|
|
37
|
+
* project's lockfile, and because it keeps the install out of the runner's
|
|
38
|
+
* global prefix, where a later step could find a stale copy.
|
|
16
39
|
*/
|
|
17
|
-
export declare function installNeurolink(version: string): Promise<
|
|
40
|
+
export declare function installNeurolink(version: string, installRoot?: string): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* The exact `npm install` spec for a `neurolink_version` input. Anything that
|
|
43
|
+
* is neither `latest` nor an exact `x.y.z` is refused: a range or a dist-tag
|
|
44
|
+
* would quietly reintroduce the moving target the pin exists to remove.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveInstallSpec(version: string): string;
|
|
18
47
|
/**
|
|
19
48
|
* Execute NeuroLink CLI command
|
|
20
49
|
* @param args - CLI arguments
|
|
21
50
|
* @param env - Environment variables
|
|
22
51
|
* @param workingDirectory - Working directory for execution
|
|
23
52
|
* @param timeout - Timeout in milliseconds (defaults to 5 minutes)
|
|
53
|
+
* @param cliPath - The executable to run; defaults to `neurolink` on PATH
|
|
24
54
|
*/
|
|
25
|
-
export declare function executeNeurolink(args: string[], env: Record<string, string>, workingDirectory?: string, timeout?: number): Promise<ActionExecutionResult>;
|
|
55
|
+
export declare function executeNeurolink(args: string[], env: Record<string, string>, workingDirectory?: string, timeout?: number, cliPath?: string): Promise<ActionExecutionResult>;
|
|
26
56
|
/**
|
|
27
57
|
* Run complete NeuroLink action
|
|
28
58
|
*/
|
|
@@ -8,9 +8,12 @@ import * as core from "@actions/core";
|
|
|
8
8
|
import * as fs from "fs";
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import { buildEnvironmentVariables } from "./actionInputs.js";
|
|
11
|
+
import { EXACT_VERSION_PATTERN } from "./cliVersion.js";
|
|
11
12
|
import { withTimeout } from "../utils/errorHandling.js";
|
|
12
13
|
/** Default timeout for CLI execution (5 minutes) */
|
|
13
14
|
const DEFAULT_EXECUTION_TIMEOUT_MS = 300000;
|
|
15
|
+
/** The package the action installs and runs. */
|
|
16
|
+
const CLI_PACKAGE = "@juspay/neurolink";
|
|
14
17
|
/**
|
|
15
18
|
* Transform CLI token usage format to action format
|
|
16
19
|
*/
|
|
@@ -130,17 +133,69 @@ export function buildCliArgs(inputs) {
|
|
|
130
133
|
return args;
|
|
131
134
|
}
|
|
132
135
|
/**
|
|
133
|
-
* Install NeuroLink CLI
|
|
136
|
+
* Install the NeuroLink CLI into a private directory under RUNNER_TEMP and
|
|
137
|
+
* return the path of its `neurolink` executable.
|
|
138
|
+
*
|
|
139
|
+
* Three things changed from the original `npm install -g @juspay/neurolink`:
|
|
140
|
+
*
|
|
141
|
+
* - The version is exact unless the consumer opts into `latest`. The action
|
|
142
|
+
* runs with the consumer's provider keys in its environment, so a moving tag
|
|
143
|
+
* would hand one bad npm publish to every consumer within minutes; pinning
|
|
144
|
+
* the action itself by SHA cannot protect against that, only pinning what it
|
|
145
|
+
* installs can. `latest` still works, with a warning that names the trade.
|
|
146
|
+
* - `--ignore-scripts`: no lifecycle script of any package in the tree runs on
|
|
147
|
+
* the consumer's runner. NeuroLink has none of its own, and the packages
|
|
148
|
+
* whose scripts matter (ffmpeg-static's binary download, sharp's prebuilt
|
|
149
|
+
* lookup) are optional dependencies the action's text generation path does
|
|
150
|
+
* not use. Video inputs need an ffmpeg on the runner's PATH, which GitHub's
|
|
151
|
+
* Ubuntu images provide.
|
|
152
|
+
* - `npm audit signatures` after the install: every package in the tree must
|
|
153
|
+
* carry a valid registry signature, and the ones published with provenance
|
|
154
|
+
* (this package is) must carry an attestation that matches. A tarball that
|
|
155
|
+
* was swapped or re-signed fails here instead of running.
|
|
156
|
+
*
|
|
157
|
+
* A project directory rather than `-g` because `npm audit signatures` reads a
|
|
158
|
+
* project's lockfile, and because it keeps the install out of the runner's
|
|
159
|
+
* global prefix, where a later step could find a stale copy.
|
|
134
160
|
*/
|
|
135
|
-
export async function installNeurolink(version) {
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
export async function installNeurolink(version, installRoot = process.env.RUNNER_TEMP || "/tmp") {
|
|
162
|
+
const spec = resolveInstallSpec(version);
|
|
163
|
+
const installDir = path.join(installRoot, `neurolink-cli-${process.pid}-${Date.now()}`);
|
|
164
|
+
fs.mkdirSync(installDir, { recursive: true, mode: 0o700 });
|
|
165
|
+
fs.writeFileSync(path.join(installDir, "package.json"), JSON.stringify({ name: "neurolink-action-cli", private: true }, null, 2), { mode: 0o600 });
|
|
166
|
+
core.info(`Installing ${spec}...`);
|
|
167
|
+
const npmOptions = { cwd: installDir, silent: !core.isDebug() };
|
|
168
|
+
await exec.exec("npm", [
|
|
169
|
+
"install",
|
|
170
|
+
"--ignore-scripts",
|
|
171
|
+
"--no-fund",
|
|
172
|
+
"--no-audit",
|
|
173
|
+
"--save-exact",
|
|
174
|
+
spec,
|
|
175
|
+
], npmOptions);
|
|
176
|
+
core.info("Verifying registry signatures and provenance attestations...");
|
|
177
|
+
await exec.exec("npm", ["audit", "signatures"], npmOptions);
|
|
178
|
+
const binPath = path.join(installDir, "node_modules", ".bin", "neurolink");
|
|
179
|
+
if (!fs.existsSync(binPath)) {
|
|
180
|
+
throw new Error(`${spec} installed but exposes no neurolink executable at ${binPath}`);
|
|
181
|
+
}
|
|
182
|
+
core.info(`NeuroLink CLI ${version} installed at ${installDir}`);
|
|
183
|
+
return binPath;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The exact `npm install` spec for a `neurolink_version` input. Anything that
|
|
187
|
+
* is neither `latest` nor an exact `x.y.z` is refused: a range or a dist-tag
|
|
188
|
+
* would quietly reintroduce the moving target the pin exists to remove.
|
|
189
|
+
*/
|
|
190
|
+
export function resolveInstallSpec(version) {
|
|
191
|
+
if (version === "latest") {
|
|
192
|
+
core.warning(`neurolink_version is "latest": the action will install whatever ${CLI_PACKAGE} version the registry serves at run time. Pin an exact version (the default) for a reproducible, review-gated install.`);
|
|
193
|
+
return `${CLI_PACKAGE}@latest`;
|
|
194
|
+
}
|
|
195
|
+
if (!EXACT_VERSION_PATTERN.test(version)) {
|
|
196
|
+
throw new Error(`neurolink_version must be an exact version such as 13.8.16, or the literal "latest"; received "${version}"`);
|
|
197
|
+
}
|
|
198
|
+
return `${CLI_PACKAGE}@${version}`;
|
|
144
199
|
}
|
|
145
200
|
/**
|
|
146
201
|
* Execute NeuroLink CLI command
|
|
@@ -148,8 +203,9 @@ export async function installNeurolink(version) {
|
|
|
148
203
|
* @param env - Environment variables
|
|
149
204
|
* @param workingDirectory - Working directory for execution
|
|
150
205
|
* @param timeout - Timeout in milliseconds (defaults to 5 minutes)
|
|
206
|
+
* @param cliPath - The executable to run; defaults to `neurolink` on PATH
|
|
151
207
|
*/
|
|
152
|
-
export async function executeNeurolink(args, env, workingDirectory, timeout = DEFAULT_EXECUTION_TIMEOUT_MS) {
|
|
208
|
+
export async function executeNeurolink(args, env, workingDirectory, timeout = DEFAULT_EXECUTION_TIMEOUT_MS, cliPath = "neurolink") {
|
|
153
209
|
const startTime = Date.now();
|
|
154
210
|
try {
|
|
155
211
|
// Filter out undefined values from process.env
|
|
@@ -159,7 +215,7 @@ export async function executeNeurolink(args, env, workingDirectory, timeout = DE
|
|
|
159
215
|
processEnv[key] = value;
|
|
160
216
|
}
|
|
161
217
|
}
|
|
162
|
-
const execPromise = exec.getExecOutput(
|
|
218
|
+
const execPromise = exec.getExecOutput(cliPath, args, {
|
|
163
219
|
cwd: workingDirectory,
|
|
164
220
|
env: { ...processEnv, ...env },
|
|
165
221
|
silent: false,
|
|
@@ -251,8 +307,8 @@ export async function executeNeurolink(args, env, workingDirectory, timeout = DE
|
|
|
251
307
|
export async function runNeurolink(inputs) {
|
|
252
308
|
let credPath;
|
|
253
309
|
try {
|
|
254
|
-
// Install CLI
|
|
255
|
-
await installNeurolink(inputs.neurolinkVersion);
|
|
310
|
+
// Install CLI (exact version unless `latest` was requested)
|
|
311
|
+
const cliPath = await installNeurolink(inputs.neurolinkVersion);
|
|
256
312
|
// Build environment
|
|
257
313
|
const env = buildEnvironmentVariables(inputs);
|
|
258
314
|
// Handle Google Cloud credentials (base64 decode)
|
|
@@ -273,7 +329,7 @@ export async function runNeurolink(inputs) {
|
|
|
273
329
|
}
|
|
274
330
|
// Execute with timeout (use input timeout or default)
|
|
275
331
|
const executionTimeout = inputs.timeout || DEFAULT_EXECUTION_TIMEOUT_MS;
|
|
276
|
-
return await executeNeurolink(args, env, inputs.workingDirectory, executionTimeout);
|
|
332
|
+
return await executeNeurolink(args, env, inputs.workingDirectory, executionTimeout, cliPath);
|
|
277
333
|
}
|
|
278
334
|
finally {
|
|
279
335
|
// Clean up credential file
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import * as core from "@actions/core";
|
|
7
7
|
import { AIProviderName } from "../constants/enums.js";
|
|
8
8
|
import { ErrorFactory } from "../utils/errorHandling.js";
|
|
9
|
+
import { DEFAULT_NEUROLINK_CLI_VERSION } from "./cliVersion.js";
|
|
9
10
|
const PROVIDER_KEY_MAP = {
|
|
10
11
|
[AIProviderName.OPENAI]: ["openaiApiKey"],
|
|
11
12
|
[AIProviderName.ANTHROPIC]: ["anthropicApiKey"],
|
|
@@ -183,7 +184,9 @@ export function parseActionInputs() {
|
|
|
183
184
|
// Advanced
|
|
184
185
|
timeout: parseNumericInput("timeout", core.getInput("timeout"), 300, parseInt, 10),
|
|
185
186
|
debug: core.getBooleanInput("debug"),
|
|
186
|
-
|
|
187
|
+
// Empty means the pinned version this action was released with; `latest`
|
|
188
|
+
// is an explicit opt-in to a moving target (see ./cliVersion.ts).
|
|
189
|
+
neurolinkVersion: core.getInput("neurolink_version") || DEFAULT_NEUROLINK_CLI_VERSION,
|
|
187
190
|
workingDirectory: core.getInput("working_directory") || ".",
|
|
188
191
|
};
|
|
189
192
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The NeuroLink CLI version the GitHub Action installs when the
|
|
3
|
+
* `neurolink_version` input is left empty.
|
|
4
|
+
*
|
|
5
|
+
* This is a deliberate, reviewed pin and not `latest`. The action runs with the
|
|
6
|
+
* consumer's provider keys and `github.token` in its environment, so an
|
|
7
|
+
* unpinned install would hand a single bad npm publish of `@juspay/neurolink`
|
|
8
|
+
* to every consumer's workflow within minutes — and pinning the ACTION by SHA
|
|
9
|
+
* would buy them nothing, because the code that actually runs is whatever the
|
|
10
|
+
* install fetched. pnpm's `minimumReleaseAge` cooldown does not apply here
|
|
11
|
+
* either: the action installs with npm.
|
|
12
|
+
*
|
|
13
|
+
* Why a committed constant and not the version in package.json: semantic-release
|
|
14
|
+
* derives the published version from git tags and no longer commits the bump,
|
|
15
|
+
* so `package.json#version` in this repository lags the registry by major
|
|
16
|
+
* versions and would pin consumers to an ancient CLI.
|
|
17
|
+
*
|
|
18
|
+
* Bump it in a reviewed change (`pnpm run build:action` afterwards — the
|
|
19
|
+
* committed bundle is diffed against a fresh build in CI). The supply-chain
|
|
20
|
+
* suite checks that it is an exact `x.y.z` and that the version exists on the
|
|
21
|
+
* registry with a provenance attestation.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_NEUROLINK_CLI_VERSION = "13.8.16";
|
|
24
|
+
/** An exact `x.y.z` (optionally pre-release) — the only shape the installer accepts besides `latest`. */
|
|
25
|
+
export declare const EXACT_VERSION_PATTERN: RegExp;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/lib/action/cliVersion.ts
|
|
2
|
+
/**
|
|
3
|
+
* The NeuroLink CLI version the GitHub Action installs when the
|
|
4
|
+
* `neurolink_version` input is left empty.
|
|
5
|
+
*
|
|
6
|
+
* This is a deliberate, reviewed pin and not `latest`. The action runs with the
|
|
7
|
+
* consumer's provider keys and `github.token` in its environment, so an
|
|
8
|
+
* unpinned install would hand a single bad npm publish of `@juspay/neurolink`
|
|
9
|
+
* to every consumer's workflow within minutes — and pinning the ACTION by SHA
|
|
10
|
+
* would buy them nothing, because the code that actually runs is whatever the
|
|
11
|
+
* install fetched. pnpm's `minimumReleaseAge` cooldown does not apply here
|
|
12
|
+
* either: the action installs with npm.
|
|
13
|
+
*
|
|
14
|
+
* Why a committed constant and not the version in package.json: semantic-release
|
|
15
|
+
* derives the published version from git tags and no longer commits the bump,
|
|
16
|
+
* so `package.json#version` in this repository lags the registry by major
|
|
17
|
+
* versions and would pin consumers to an ancient CLI.
|
|
18
|
+
*
|
|
19
|
+
* Bump it in a reviewed change (`pnpm run build:action` afterwards — the
|
|
20
|
+
* committed bundle is diffed against a fresh build in CI). The supply-chain
|
|
21
|
+
* suite checks that it is an exact `x.y.z` and that the version exists on the
|
|
22
|
+
* registry with a provenance attestation.
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_NEUROLINK_CLI_VERSION = "13.8.16";
|
|
25
|
+
/** An exact `x.y.z` (optionally pre-release) — the only shape the installer accepts besides `latest`. */
|
|
26
|
+
export const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
package/dist/action/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
* @module action
|
|
4
4
|
*/
|
|
5
5
|
export { parseActionInputs, validateProviderKey, buildEnvironmentVariables, validateActionInputs, maskSecrets, } from "./actionInputs.js";
|
|
6
|
-
export { buildCliArgs, installNeurolink, executeNeurolink, runNeurolink, transformCliResponse, } from "./actionExecutor.js";
|
|
6
|
+
export { buildCliArgs, installNeurolink, resolveInstallSpec, executeNeurolink, runNeurolink, transformCliResponse, } from "./actionExecutor.js";
|
|
7
|
+
export { DEFAULT_NEUROLINK_CLI_VERSION, EXACT_VERSION_PATTERN, } from "./cliVersion.js";
|
|
7
8
|
export { postResultComment, writeJobSummary, setActionOutputs, getActionOutputs, } from "./githubIntegration.js";
|
package/dist/action/index.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// Input handling
|
|
7
7
|
export { parseActionInputs, validateProviderKey, buildEnvironmentVariables, validateActionInputs, maskSecrets, } from "./actionInputs.js";
|
|
8
8
|
// Execution
|
|
9
|
-
export { buildCliArgs, installNeurolink, executeNeurolink, runNeurolink, transformCliResponse, } from "./actionExecutor.js";
|
|
9
|
+
export { buildCliArgs, installNeurolink, resolveInstallSpec, executeNeurolink, runNeurolink, transformCliResponse, } from "./actionExecutor.js";
|
|
10
|
+
export { DEFAULT_NEUROLINK_CLI_VERSION, EXACT_VERSION_PATTERN, } from "./cliVersion.js";
|
|
10
11
|
// GitHub integration
|
|
11
12
|
export { postResultComment, writeJobSummary, setActionOutputs, getActionOutputs, } from "./githubIntegration.js";
|
|
12
13
|
// Re-export types for convenience
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import pLimit from "p-limit";
|
|
15
15
|
import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
|
|
16
16
|
import { logger } from "../../utils/logger.js";
|
|
17
|
+
import { downloadAttachment, getAttachmentPolicy, readAttachmentFile, } from "../../utils/attachmentPolicy.js";
|
|
18
|
+
import { MAX_IMAGE_BYTES } from "../../utils/sizeGuard.js";
|
|
17
19
|
import { validateImageForVideo } from "../../utils/parameterValidation.js";
|
|
18
20
|
import { VIDEO_ERROR_CODES } from "../../constants/videoErrors.js";
|
|
19
21
|
import { extractFirstFrame, extractLastFrame } from "./frameExtractor.js";
|
|
@@ -44,14 +46,14 @@ const DEFAULT_TRANSITION_DURATION = 4;
|
|
|
44
46
|
*
|
|
45
47
|
* @throws {VideoError} If the image cannot be resolved
|
|
46
48
|
*/
|
|
47
|
-
async function resolveImageToBuffer(image, segmentIndex) {
|
|
49
|
+
async function resolveImageToBuffer(image, segmentIndex, policy) {
|
|
48
50
|
if (Buffer.isBuffer(image)) {
|
|
49
51
|
return image;
|
|
50
52
|
}
|
|
51
53
|
if (typeof image === "string") {
|
|
52
54
|
return image.startsWith("http://") || image.startsWith("https://")
|
|
53
|
-
? fetchImageFromUrl(image, segmentIndex)
|
|
54
|
-
: readImageFromDisk(image, segmentIndex);
|
|
55
|
+
? fetchImageFromUrl(image, segmentIndex, policy)
|
|
56
|
+
: readImageFromDisk(image, segmentIndex, policy);
|
|
55
57
|
}
|
|
56
58
|
// ImageWithAltText
|
|
57
59
|
if (typeof image === "object" && "data" in image) {
|
|
@@ -62,7 +64,7 @@ async function resolveImageToBuffer(image, segmentIndex) {
|
|
|
62
64
|
if (typeof imgData === "string") {
|
|
63
65
|
// Handle HTTP(S) URLs
|
|
64
66
|
if (imgData.startsWith("http://") || imgData.startsWith("https://")) {
|
|
65
|
-
return fetchImageFromUrl(imgData, segmentIndex);
|
|
67
|
+
return fetchImageFromUrl(imgData, segmentIndex, policy);
|
|
66
68
|
}
|
|
67
69
|
// Handle data URIs (e.g., "data:image/png;base64,iVBORw0KG...")
|
|
68
70
|
if (imgData.startsWith("data:")) {
|
|
@@ -82,7 +84,7 @@ async function resolveImageToBuffer(image, segmentIndex) {
|
|
|
82
84
|
}
|
|
83
85
|
// Try as file path
|
|
84
86
|
try {
|
|
85
|
-
return await readImageFromDisk(imgData, segmentIndex);
|
|
87
|
+
return await readImageFromDisk(imgData, segmentIndex, policy);
|
|
86
88
|
}
|
|
87
89
|
catch (fileError) {
|
|
88
90
|
// Not a valid file path - throw clear error instead of silently treating as base64
|
|
@@ -114,47 +116,52 @@ async function resolveImageToBuffer(image, segmentIndex) {
|
|
|
114
116
|
context: { segmentIndex },
|
|
115
117
|
});
|
|
116
118
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Fetch an image from an HTTP(S) URL under the attachment policy: HTTPS to a
|
|
121
|
+
* public host unless the policy says otherwise, DNS-pinned, redirects
|
|
122
|
+
* re-validated per hop, body bounded while streaming.
|
|
123
|
+
*/
|
|
124
|
+
async function fetchImageFromUrl(url, segmentIndex, policy) {
|
|
121
125
|
try {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
retriable: response.status >= 500,
|
|
130
|
-
context: { segmentIndex, url: url.substring(0, 100) },
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return Buffer.from(await response.arrayBuffer());
|
|
126
|
+
const downloaded = await downloadAttachment(url, {
|
|
127
|
+
policy,
|
|
128
|
+
maxBytes: MAX_IMAGE_BYTES,
|
|
129
|
+
label: `segment ${segmentIndex} image URL`,
|
|
130
|
+
timeoutMs: IMAGE_IO_TIMEOUT_MS,
|
|
131
|
+
});
|
|
132
|
+
return downloaded.buffer;
|
|
134
133
|
}
|
|
135
134
|
catch (error) {
|
|
136
135
|
if (error instanceof VideoError) {
|
|
137
136
|
throw error;
|
|
138
137
|
}
|
|
138
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
139
|
+
const status = /HTTP (\d{3})/.exec(message)?.[1];
|
|
140
|
+
const refused = error?.code === "ATTACHMENT_URL_REFUSED";
|
|
139
141
|
throw new VideoError({
|
|
140
142
|
code: VIDEO_ERROR_CODES.INVALID_INPUT,
|
|
141
|
-
message: `Failed to fetch image for segment ${segmentIndex}: ${
|
|
143
|
+
message: `Failed to fetch image for segment ${segmentIndex}: ${message}`,
|
|
142
144
|
category: ErrorCategory.EXECUTION,
|
|
143
145
|
severity: ErrorSeverity.HIGH,
|
|
144
|
-
retriable:
|
|
145
|
-
context: { segmentIndex },
|
|
146
|
+
retriable: !refused && (status === undefined || Number(status) >= 500),
|
|
147
|
+
context: { segmentIndex, url: url.substring(0, 100) },
|
|
146
148
|
originalError: error instanceof Error ? error : undefined,
|
|
147
149
|
});
|
|
148
150
|
}
|
|
149
|
-
finally {
|
|
150
|
-
clearTimeout(timeout);
|
|
151
|
-
}
|
|
152
151
|
}
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Read an image from the local filesystem under the attachment policy: roots
|
|
154
|
+
* containment when configured, O_NOFOLLOW open of the real path, regular
|
|
155
|
+
* files only, bounded at the image cap.
|
|
156
|
+
*/
|
|
157
|
+
async function readImageFromDisk(filePath, segmentIndex, policy) {
|
|
156
158
|
try {
|
|
157
|
-
|
|
159
|
+
const read = await readAttachmentFile(filePath, {
|
|
160
|
+
policy,
|
|
161
|
+
maxBytes: MAX_IMAGE_BYTES,
|
|
162
|
+
label: `segment ${segmentIndex} image file`,
|
|
163
|
+
});
|
|
164
|
+
return read.buffer;
|
|
158
165
|
}
|
|
159
166
|
catch (error) {
|
|
160
167
|
throw new VideoError({
|
|
@@ -206,7 +213,7 @@ function processOrderedCompletions(state) {
|
|
|
206
213
|
*
|
|
207
214
|
* @throws {VideoError} On generation failure or circuit breaker trip
|
|
208
215
|
*/
|
|
209
|
-
async function generateSingleClip(segment, index, videoOptions, region, state) {
|
|
216
|
+
async function generateSingleClip(segment, index, videoOptions, region, state, policy) {
|
|
210
217
|
if (state.circuitOpen) {
|
|
211
218
|
throw new VideoError({
|
|
212
219
|
code: VIDEO_ERROR_CODES.DIRECTOR_CLIP_FAILED,
|
|
@@ -219,7 +226,7 @@ async function generateSingleClip(segment, index, videoOptions, region, state) {
|
|
|
219
226
|
}
|
|
220
227
|
const clipStart = Date.now();
|
|
221
228
|
try {
|
|
222
|
-
const imageBuffer = await resolveImageToBuffer(segment.image, index);
|
|
229
|
+
const imageBuffer = await resolveImageToBuffer(segment.image, index, policy);
|
|
223
230
|
// Validate image buffer (type, dimensions, size limits) before generation
|
|
224
231
|
const imageValidation = validateImageForVideo(imageBuffer);
|
|
225
232
|
if (imageValidation) {
|
|
@@ -273,7 +280,7 @@ async function generateSingleClip(segment, index, videoOptions, region, state) {
|
|
|
273
280
|
* @returns Ordered array of clip results (indexed by segment number)
|
|
274
281
|
* @throws {VideoError} On any clip failure (all clips are mandatory)
|
|
275
282
|
*/
|
|
276
|
-
async function generateClips(segments, videoOptions, region) {
|
|
283
|
+
async function generateClips(segments, videoOptions, region, policy) {
|
|
277
284
|
const limit = pLimit(CLIP_CONCURRENCY);
|
|
278
285
|
const state = {
|
|
279
286
|
consecutiveFailures: 0,
|
|
@@ -282,7 +289,7 @@ async function generateClips(segments, videoOptions, region) {
|
|
|
282
289
|
completions: new Array(segments.length).fill({ status: "pending" }),
|
|
283
290
|
nextExpectedIndex: 0,
|
|
284
291
|
};
|
|
285
|
-
const clipPromises = segments.map((segment, i) => limit(() => generateSingleClip(segment, i, videoOptions, region, state)));
|
|
292
|
+
const clipPromises = segments.map((segment, i) => limit(() => generateSingleClip(segment, i, videoOptions, region, state, policy)));
|
|
286
293
|
// Collect results — any failure is fatal
|
|
287
294
|
const settled = await Promise.allSettled(clipPromises);
|
|
288
295
|
const failures = settled.filter((r) => r.status === "rejected");
|
|
@@ -449,8 +456,11 @@ export async function executeDirectorPipeline(segments, videoOptions = {}, direc
|
|
|
449
456
|
clipDuration,
|
|
450
457
|
resolution: videoOptions.resolution ?? "720p",
|
|
451
458
|
});
|
|
459
|
+
// The instance's attachment policy for segment images given as URLs or
|
|
460
|
+
// paths; the environment-derived default when the caller stamped none.
|
|
461
|
+
const attachmentPolicy = getAttachmentPolicy(directorOptions);
|
|
452
462
|
// Phase 1: Generate all clips
|
|
453
|
-
const clipResults = await generateClips(segments, videoOptions, region);
|
|
463
|
+
const clipResults = await generateClips(segments, videoOptions, region, attachmentPolicy);
|
|
454
464
|
logger.info("Phase 1 complete — all clips generated", {
|
|
455
465
|
clipCount: clipResults.length,
|
|
456
466
|
elapsedMs: Date.now() - pipelineStart,
|
|
@@ -25,6 +25,23 @@ export declare const JPEG_QUALITY = "2";
|
|
|
25
25
|
export declare const LAST_FRAME_SEEK_OFFSET = "0.5";
|
|
26
26
|
/** Minimum valid MP4 buffer size in bytes (ftyp header = 8 bytes minimum) */
|
|
27
27
|
export declare const MIN_VIDEO_BUFFER_SIZE = 12;
|
|
28
|
+
/**
|
|
29
|
+
* Input-side options every ffmpeg run here gets, placed before the first
|
|
30
|
+
* `-i` so they apply to the inputs.
|
|
31
|
+
*
|
|
32
|
+
* `-protocol_whitelist file,crypto,data` pins the demuxer to local files: a
|
|
33
|
+
* playlist or container that references `http://`, `concat:` or `subfile:`
|
|
34
|
+
* cannot make ffmpeg fetch or read anything else, whatever the installed
|
|
35
|
+
* build's defaults are. `-nostdin` keeps a child from ever waiting on a
|
|
36
|
+
* terminal. Passed explicitly rather than relied on, because the whitelist a
|
|
37
|
+
* build ships with is the build's choice, not ours.
|
|
38
|
+
*/
|
|
39
|
+
export declare const FFMPEG_INPUT_HARDENING: readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* Prepend {@link FFMPEG_INPUT_HARDENING} to an argv that names an input, once.
|
|
42
|
+
* An argv without `-i` (a version probe) is returned as given.
|
|
43
|
+
*/
|
|
44
|
+
export declare function withInputHardening(args: readonly string[]): string[];
|
|
28
45
|
/**
|
|
29
46
|
* Create a tracked temporary directory for FFmpeg operations.
|
|
30
47
|
*
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module adapters/video/ffmpegAdapter
|
|
11
11
|
*/
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
13
12
|
import { readdirSync, rmdirSync, unlinkSync } from "node:fs";
|
|
14
|
-
import {
|
|
13
|
+
import { mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
15
14
|
import { tmpdir } from "node:os";
|
|
16
15
|
import { join } from "node:path";
|
|
17
16
|
import { logger } from "../../utils/logger.js";
|
|
@@ -34,6 +33,32 @@ export const LAST_FRAME_SEEK_OFFSET = "0.5";
|
|
|
34
33
|
export const MIN_VIDEO_BUFFER_SIZE = 12;
|
|
35
34
|
/** MP4 ftyp box magic bytes at offset 4: "ftyp" */
|
|
36
35
|
const FTYP_MAGIC = Buffer.from([0x66, 0x74, 0x79, 0x70]);
|
|
36
|
+
/**
|
|
37
|
+
* Input-side options every ffmpeg run here gets, placed before the first
|
|
38
|
+
* `-i` so they apply to the inputs.
|
|
39
|
+
*
|
|
40
|
+
* `-protocol_whitelist file,crypto,data` pins the demuxer to local files: a
|
|
41
|
+
* playlist or container that references `http://`, `concat:` or `subfile:`
|
|
42
|
+
* cannot make ffmpeg fetch or read anything else, whatever the installed
|
|
43
|
+
* build's defaults are. `-nostdin` keeps a child from ever waiting on a
|
|
44
|
+
* terminal. Passed explicitly rather than relied on, because the whitelist a
|
|
45
|
+
* build ships with is the build's choice, not ours.
|
|
46
|
+
*/
|
|
47
|
+
export const FFMPEG_INPUT_HARDENING = [
|
|
48
|
+
"-nostdin",
|
|
49
|
+
"-protocol_whitelist",
|
|
50
|
+
"file,crypto,data",
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* Prepend {@link FFMPEG_INPUT_HARDENING} to an argv that names an input, once.
|
|
54
|
+
* An argv without `-i` (a version probe) is returned as given.
|
|
55
|
+
*/
|
|
56
|
+
export function withInputHardening(args) {
|
|
57
|
+
if (!args.includes("-i") || args.includes("-protocol_whitelist")) {
|
|
58
|
+
return [...args];
|
|
59
|
+
}
|
|
60
|
+
return [...FFMPEG_INPUT_HARDENING, ...args];
|
|
61
|
+
}
|
|
37
62
|
// ============================================================================
|
|
38
63
|
// TEMP DIRECTORY MANAGEMENT
|
|
39
64
|
// ============================================================================
|
|
@@ -80,8 +105,9 @@ function ensureCleanupRegistered() {
|
|
|
80
105
|
*/
|
|
81
106
|
export async function createTrackedTempDir(prefix) {
|
|
82
107
|
ensureCleanupRegistered();
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
// mkdtemp: an unguessable name AND 0o700, atomically — nothing else on the
|
|
109
|
+
// host can read the frames or clips that land here.
|
|
110
|
+
const dir = await mkdtemp(join(tmpdir(), `neurolink-${prefix}-`));
|
|
85
111
|
activeTempDirs.add(dir);
|
|
86
112
|
return dir;
|
|
87
113
|
}
|
|
@@ -172,8 +198,9 @@ export async function runFfmpeg(args, options = {}) {
|
|
|
172
198
|
const ffmpegPath = options.binaryPath ?? (await getFfmpegPath());
|
|
173
199
|
const timeoutMs = options.timeoutMs ?? FFMPEG_FRAME_TIMEOUT_MS;
|
|
174
200
|
const maxBuffer = options.maxBuffer ?? FFMPEG_FRAME_MAX_BUFFER;
|
|
201
|
+
const hardenedArgs = withInputHardening(args);
|
|
175
202
|
return new Promise((resolve, reject) => {
|
|
176
|
-
const proc = execFile(ffmpegPath,
|
|
203
|
+
const proc = execFile(ffmpegPath, hardenedArgs, { timeout: timeoutMs, maxBuffer }, (error, stdout, stderr) => {
|
|
177
204
|
if (error) {
|
|
178
205
|
// execFile's callback error carries no output, but ffmpeg prints a
|
|
179
206
|
// stream's input report before it exits non-zero on a stream it
|
|
@@ -59,7 +59,7 @@ async function extractFrame(videoBuffer, position) {
|
|
|
59
59
|
const inputPath = join(tempDir, "input.mp4");
|
|
60
60
|
const outputPath = join(tempDir, `${position}_frame.jpg`);
|
|
61
61
|
try {
|
|
62
|
-
await writeFile(inputPath, videoBuffer);
|
|
62
|
+
await writeFile(inputPath, videoBuffer, { mode: 0o600, flag: "wx" });
|
|
63
63
|
const args = position === "first"
|
|
64
64
|
? [
|
|
65
65
|
"-y",
|
|
@@ -27,13 +27,16 @@ async function writeClipsAndBuildConcatList(videoBuffers, tempDir, concatListPat
|
|
|
27
27
|
const concatLines = [];
|
|
28
28
|
for (let i = 0; i < videoBuffers.length; i++) {
|
|
29
29
|
const inputPath = join(tempDir, `clip_${i}.mp4`);
|
|
30
|
-
await writeFile(inputPath, videoBuffers[i]);
|
|
30
|
+
await writeFile(inputPath, videoBuffers[i], { mode: 0o600, flag: "wx" });
|
|
31
31
|
inputPaths.push(inputPath);
|
|
32
32
|
// Normalize backslashes to forward slashes for FFmpeg, then escape single quotes
|
|
33
33
|
const safePath = inputPath.replace(/\\/g, "/").replace(/'/g, "'\\''");
|
|
34
34
|
concatLines.push(`file '${safePath}'`);
|
|
35
35
|
}
|
|
36
|
-
await writeFile(concatListPath, concatLines.join("\n")
|
|
36
|
+
await writeFile(concatListPath, concatLines.join("\n"), {
|
|
37
|
+
mode: 0o600,
|
|
38
|
+
flag: "wx",
|
|
39
|
+
});
|
|
37
40
|
return inputPaths;
|
|
38
41
|
}
|
|
39
42
|
/**
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arithmetic evaluator for the built-in `calculateMath` tool.
|
|
3
|
+
*
|
|
4
|
+
* The expression is MODEL output. It is parsed by a small recursive-descent
|
|
5
|
+
* parser and evaluated over numbers only: there is no `Function`, no `eval`,
|
|
6
|
+
* no property access and no identifier that is not on the fixed allow-list
|
|
7
|
+
* below. Anything the grammar does not cover is reported as an error.
|
|
8
|
+
*
|
|
9
|
+
* Grammar (precedence low → high):
|
|
10
|
+
*
|
|
11
|
+
* expression := term (("+" | "-") term)*
|
|
12
|
+
* term := unary (("*" | "/" | "%") unary)*
|
|
13
|
+
* unary := ("-" | "+") unary | power
|
|
14
|
+
* power := primary ("**" unary)? right-associative
|
|
15
|
+
* primary := number | constant | function "(" args ")" | "(" expression ")"
|
|
16
|
+
*
|
|
17
|
+
* Numbers are decimal literals (`12`, `3.5`, `.5`, `1e-3`). Functions and
|
|
18
|
+
* constants are the listed `Math` members, accepted with or without the
|
|
19
|
+
* `Math.` prefix. Unary minus binds looser than `**`, so `-2 ** 2` is `-4`,
|
|
20
|
+
* as in mathematics (JavaScript rejects that spelling outright).
|
|
21
|
+
*/
|
|
22
|
+
import type { ArithmeticEvaluation } from "../types/index.js";
|
|
23
|
+
/** The names the model may use, for the error text and the tool description. */
|
|
24
|
+
export declare const ARITHMETIC_FUNCTION_NAMES: readonly string[];
|
|
25
|
+
export declare const ARITHMETIC_CONSTANT_NAMES: readonly string[];
|
|
26
|
+
/**
|
|
27
|
+
* Evaluate an arithmetic expression without executing it as code.
|
|
28
|
+
*
|
|
29
|
+
* Total: every failure — unsupported syntax, an unknown name, a result that is
|
|
30
|
+
* not a finite number — comes back as `{ ok: false, error }`, never a throw.
|
|
31
|
+
*/
|
|
32
|
+
export declare function evaluateArithmetic(expression: string): ArithmeticEvaluation;
|