ocpipe 0.3.0 → 0.3.1
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/DESIGN.md +257 -0
- package/GETTING_STARTED.md +384 -0
- package/README.md +21 -22
- package/example/correction.ts +2 -2
- package/example/index.ts +1 -1
- package/llms.txt +200 -0
- package/package.json +13 -2
- package/src/agent.ts +1 -1
- package/src/index.ts +2 -2
- package/src/module.ts +17 -12
- package/src/parsing.ts +149 -42
- package/src/pipeline.ts +1 -1
- package/src/predict.ts +8 -3
- package/src/signature.ts +1 -1
- package/src/state.ts +1 -1
- package/src/testing.ts +4 -3
- package/src/types.ts +15 -7
package/src/parsing.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ocpipe response parsing.
|
|
3
3
|
*
|
|
4
4
|
* Extracts and validates LLM responses using JSON or field marker formats.
|
|
5
5
|
*/
|
|
@@ -507,13 +507,13 @@ export function applyJqPatch(
|
|
|
507
507
|
/\binput\b/,
|
|
508
508
|
/\binputs\b/,
|
|
509
509
|
/\bsystem\b/,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
510
|
+
/@base64d/,
|
|
511
|
+
/@uri/,
|
|
512
|
+
/@csv/,
|
|
513
|
+
/@tsv/,
|
|
514
|
+
/@json/,
|
|
515
|
+
/@text/,
|
|
516
|
+
/@sh/,
|
|
517
517
|
/`[^`]*`/, // Backtick string interpolation
|
|
518
518
|
/\bimport\b/,
|
|
519
519
|
/\binclude\b/,
|
|
@@ -532,7 +532,7 @@ export function applyJqPatch(
|
|
|
532
532
|
|
|
533
533
|
// Only allow patches that look like field operations
|
|
534
534
|
// Valid: .foo = "bar", .items[0].name = .items[0].title, del(.foo) | .bar = 1
|
|
535
|
-
const safePattern = /^[\s\w
|
|
535
|
+
const safePattern = /^[\s\w[\]."'=|,:\-{}]*$/
|
|
536
536
|
if (!safePattern.test(patch)) {
|
|
537
537
|
console.error(` Invalid characters in patch, skipping: ${patch}`)
|
|
538
538
|
return obj
|
|
@@ -648,44 +648,63 @@ export function buildBatchJsonPatchPrompt(
|
|
|
648
648
|
return lines.join('\n')
|
|
649
649
|
}
|
|
650
650
|
|
|
651
|
-
/**
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
)
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
651
|
+
/**
|
|
652
|
+
* extractBalancedArray extracts a balanced JSON array from a string starting at startIdx.
|
|
653
|
+
* Returns the array substring or null if not found/unbalanced.
|
|
654
|
+
*/
|
|
655
|
+
function extractBalancedArray(text: string, startIdx: number): string | null {
|
|
656
|
+
if (startIdx === -1 || startIdx >= text.length) return null
|
|
657
|
+
|
|
658
|
+
let bracketCount = 0
|
|
659
|
+
let endIdx = startIdx
|
|
660
|
+
for (let i = startIdx; i < text.length; i++) {
|
|
661
|
+
if (text[i] === '[') bracketCount++
|
|
662
|
+
else if (text[i] === ']') {
|
|
663
|
+
bracketCount--
|
|
664
|
+
if (bracketCount === 0) {
|
|
665
|
+
endIdx = i + 1
|
|
666
|
+
break
|
|
667
|
+
}
|
|
662
668
|
}
|
|
663
669
|
}
|
|
664
670
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
671
|
+
if (endIdx > startIdx && bracketCount === 0) {
|
|
672
|
+
return text.slice(startIdx, endIdx)
|
|
673
|
+
}
|
|
674
|
+
return null
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** extractJsonPatch extracts a JSON Patch array from an LLM response. */
|
|
678
|
+
export function extractJsonPatch(response: string): JsonPatchOperation[] {
|
|
679
|
+
// Try to find JSON array in code blocks first
|
|
680
|
+
// Use indexOf to find code block boundaries to avoid ReDoS vulnerabilities
|
|
681
|
+
const codeBlockStart = response.indexOf('```')
|
|
682
|
+
if (codeBlockStart !== -1) {
|
|
683
|
+
const codeBlockEnd = response.indexOf('```', codeBlockStart + 3)
|
|
684
|
+
if (codeBlockEnd !== -1) {
|
|
685
|
+
const codeBlockContent = response.slice(codeBlockStart + 3, codeBlockEnd)
|
|
686
|
+
// Skip optional "json" language identifier and whitespace
|
|
687
|
+
const arrayStart = codeBlockContent.indexOf('[')
|
|
688
|
+
if (arrayStart !== -1) {
|
|
689
|
+
const arrayJson = extractBalancedArray(codeBlockContent, arrayStart)
|
|
690
|
+
if (arrayJson) {
|
|
691
|
+
try {
|
|
692
|
+
return JSON.parse(arrayJson) as JsonPatchOperation[]
|
|
693
|
+
} catch {
|
|
694
|
+
// Continue to try other methods
|
|
695
|
+
}
|
|
677
696
|
}
|
|
678
697
|
}
|
|
679
698
|
}
|
|
699
|
+
}
|
|
680
700
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
701
|
+
// Try to find raw JSON array by counting brackets
|
|
702
|
+
const arrayJson = extractBalancedArray(response, response.indexOf('['))
|
|
703
|
+
if (arrayJson) {
|
|
704
|
+
try {
|
|
705
|
+
return JSON.parse(arrayJson) as JsonPatchOperation[]
|
|
706
|
+
} catch {
|
|
707
|
+
// Fall through to empty array
|
|
689
708
|
}
|
|
690
709
|
}
|
|
691
710
|
|
|
@@ -775,6 +794,37 @@ export function applyJsonPatch(
|
|
|
775
794
|
return result
|
|
776
795
|
}
|
|
777
796
|
|
|
797
|
+
/** Keys that could be used for prototype pollution attacks. */
|
|
798
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
799
|
+
|
|
800
|
+
function isUnsafeKey(key: string): boolean {
|
|
801
|
+
return UNSAFE_KEYS.has(key)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Unescape a single JSON Pointer path segment according to RFC 6901.
|
|
806
|
+
* This ensures that checks for dangerous keys are applied to the
|
|
807
|
+
* effective property name, not the escaped form.
|
|
808
|
+
*/
|
|
809
|
+
function unescapeJsonPointerSegment(segment: string): string {
|
|
810
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~')
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* isSafePathSegment determines whether a JSON Pointer path segment is safe to use
|
|
815
|
+
* as a property key on an object. It rejects keys that are known to enable
|
|
816
|
+
* prototype pollution or that contain characters commonly used in special
|
|
817
|
+
* property notations.
|
|
818
|
+
*/
|
|
819
|
+
function isSafePathSegment(segment: string): boolean {
|
|
820
|
+
// Normalize the segment as it will appear as a property key.
|
|
821
|
+
const normalized = unescapeJsonPointerSegment(String(segment))
|
|
822
|
+
if (isUnsafeKey(normalized)) return false
|
|
823
|
+
// Disallow bracket notation-style segments to avoid unexpected coercions.
|
|
824
|
+
if (normalized.includes('[') || normalized.includes(']')) return false
|
|
825
|
+
return true
|
|
826
|
+
}
|
|
827
|
+
|
|
778
828
|
/** getValueAtPath retrieves a value at a JSON Pointer path. */
|
|
779
829
|
function getValueAtPath(
|
|
780
830
|
obj: Record<string, unknown>,
|
|
@@ -782,6 +832,15 @@ function getValueAtPath(
|
|
|
782
832
|
): unknown {
|
|
783
833
|
let current: unknown = obj
|
|
784
834
|
for (const part of parts) {
|
|
835
|
+
// Block prototype-pollution: reject __proto__, constructor, prototype
|
|
836
|
+
if (
|
|
837
|
+
part === '__proto__' ||
|
|
838
|
+
part === 'constructor' ||
|
|
839
|
+
part === 'prototype'
|
|
840
|
+
) {
|
|
841
|
+
return undefined
|
|
842
|
+
}
|
|
843
|
+
if (!isSafePathSegment(part)) return undefined
|
|
785
844
|
if (current === null || current === undefined) return undefined
|
|
786
845
|
if (Array.isArray(current)) {
|
|
787
846
|
const idx = parseInt(part, 10)
|
|
@@ -806,25 +865,49 @@ function setValueAtPath(
|
|
|
806
865
|
let current: unknown = obj
|
|
807
866
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
808
867
|
const part = parts[i]!
|
|
868
|
+
// Block prototype-pollution: reject __proto__, constructor, prototype
|
|
869
|
+
if (
|
|
870
|
+
part === '__proto__' ||
|
|
871
|
+
part === 'constructor' ||
|
|
872
|
+
part === 'prototype'
|
|
873
|
+
) {
|
|
874
|
+
return
|
|
875
|
+
}
|
|
876
|
+
if (!isSafePathSegment(part)) {
|
|
877
|
+
// Avoid writing to dangerous or malformed prototype-related properties
|
|
878
|
+
return
|
|
879
|
+
}
|
|
809
880
|
if (Array.isArray(current)) {
|
|
810
881
|
const idx = parseInt(part, 10)
|
|
811
882
|
if (current[idx] === undefined) {
|
|
812
883
|
// Create intermediate object or array
|
|
813
884
|
const nextPart = parts[i + 1]!
|
|
814
|
-
current[idx] = /^\d+$/.test(nextPart) ? [] :
|
|
885
|
+
current[idx] = /^\d+$/.test(nextPart) ? [] : Object.create(null)
|
|
815
886
|
}
|
|
816
887
|
current = current[idx]
|
|
817
888
|
} else if (typeof current === 'object' && current !== null) {
|
|
818
889
|
const rec = current as Record<string, unknown>
|
|
819
890
|
if (rec[part] === undefined) {
|
|
820
891
|
const nextPart = parts[i + 1]!
|
|
821
|
-
rec[part] = /^\d+$/.test(nextPart) ? [] :
|
|
892
|
+
rec[part] = /^\d+$/.test(nextPart) ? [] : Object.create(null)
|
|
822
893
|
}
|
|
823
894
|
current = rec[part]
|
|
824
895
|
}
|
|
825
896
|
}
|
|
826
897
|
|
|
827
898
|
const lastPart = parts[parts.length - 1]!
|
|
899
|
+
// Block prototype-pollution: reject __proto__, constructor, prototype
|
|
900
|
+
if (
|
|
901
|
+
lastPart === '__proto__' ||
|
|
902
|
+
lastPart === 'constructor' ||
|
|
903
|
+
lastPart === 'prototype'
|
|
904
|
+
) {
|
|
905
|
+
return
|
|
906
|
+
}
|
|
907
|
+
if (!isSafePathSegment(lastPart)) {
|
|
908
|
+
// Avoid writing to dangerous or malformed prototype-related properties
|
|
909
|
+
return
|
|
910
|
+
}
|
|
828
911
|
if (Array.isArray(current)) {
|
|
829
912
|
const idx = parseInt(lastPart, 10)
|
|
830
913
|
current[idx] = value
|
|
@@ -843,6 +926,18 @@ function removeValueAtPath(
|
|
|
843
926
|
let current: unknown = obj
|
|
844
927
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
845
928
|
const part = parts[i]!
|
|
929
|
+
// Block prototype-pollution: reject __proto__, constructor, prototype
|
|
930
|
+
if (
|
|
931
|
+
part === '__proto__' ||
|
|
932
|
+
part === 'constructor' ||
|
|
933
|
+
part === 'prototype'
|
|
934
|
+
) {
|
|
935
|
+
return
|
|
936
|
+
}
|
|
937
|
+
if (!isSafePathSegment(part)) {
|
|
938
|
+
// Avoid accessing dangerous prototype-related properties
|
|
939
|
+
return
|
|
940
|
+
}
|
|
846
941
|
if (Array.isArray(current)) {
|
|
847
942
|
current = current[parseInt(part, 10)]
|
|
848
943
|
} else if (typeof current === 'object' && current !== null) {
|
|
@@ -853,6 +948,18 @@ function removeValueAtPath(
|
|
|
853
948
|
}
|
|
854
949
|
|
|
855
950
|
const lastPart = parts[parts.length - 1]!
|
|
951
|
+
// Block prototype-pollution: reject __proto__, constructor, prototype
|
|
952
|
+
if (
|
|
953
|
+
lastPart === '__proto__' ||
|
|
954
|
+
lastPart === 'constructor' ||
|
|
955
|
+
lastPart === 'prototype'
|
|
956
|
+
) {
|
|
957
|
+
return
|
|
958
|
+
}
|
|
959
|
+
if (!isSafePathSegment(lastPart)) {
|
|
960
|
+
// Avoid deleting dangerous or malformed properties
|
|
961
|
+
return
|
|
962
|
+
}
|
|
856
963
|
if (Array.isArray(current)) {
|
|
857
964
|
const idx = parseInt(lastPart, 10)
|
|
858
965
|
current.splice(idx, 1)
|
package/src/pipeline.ts
CHANGED
package/src/predict.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ocpipe Predict class.
|
|
3
3
|
*
|
|
4
4
|
* Executes a signature by generating a prompt, calling OpenCode, and parsing the response.
|
|
5
5
|
*/
|
|
@@ -47,8 +47,13 @@ export interface PredictConfig {
|
|
|
47
47
|
correction?: CorrectionConfig | false
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
type AnySignature = SignatureDef<
|
|
51
|
+
Record<string, FieldConfig>,
|
|
52
|
+
Record<string, FieldConfig>
|
|
53
|
+
>
|
|
54
|
+
|
|
50
55
|
/** Predict executes a signature by calling an LLM and parsing the response. */
|
|
51
|
-
export class Predict<S extends
|
|
56
|
+
export class Predict<S extends AnySignature> {
|
|
52
57
|
constructor(
|
|
53
58
|
public readonly sig: S,
|
|
54
59
|
public readonly config: PredictConfig = {},
|
|
@@ -239,7 +244,7 @@ export class Predict<S extends SignatureDef<any, any>> {
|
|
|
239
244
|
|
|
240
245
|
// Input fields as JSON
|
|
241
246
|
const inputsWithDescriptions: Record<string, unknown> = {}
|
|
242
|
-
for (const [name
|
|
247
|
+
for (const [name] of Object.entries(this.sig.inputs) as [
|
|
243
248
|
string,
|
|
244
249
|
FieldConfig,
|
|
245
250
|
][]) {
|
package/src/signature.ts
CHANGED
package/src/state.ts
CHANGED
package/src/testing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ocpipe testing utilities.
|
|
3
3
|
*
|
|
4
|
-
* Provides mock backends and test helpers for unit testing
|
|
4
|
+
* Provides mock backends and test helpers for unit testing ocpipe components.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { RunAgentOptions, RunAgentResult, FieldConfig } from './types.js'
|
|
@@ -175,11 +175,12 @@ export function generateMockOutputs(
|
|
|
175
175
|
case 'ZodObject':
|
|
176
176
|
result[name] = {}
|
|
177
177
|
break
|
|
178
|
-
case 'ZodEnum':
|
|
178
|
+
case 'ZodEnum': {
|
|
179
179
|
// Get first enum value via options property
|
|
180
180
|
const enumType = config.type as { options?: readonly string[] }
|
|
181
181
|
result[name] = enumType.options?.[0] ?? 'unknown'
|
|
182
182
|
break
|
|
183
|
+
}
|
|
183
184
|
default:
|
|
184
185
|
result[name] = null
|
|
185
186
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Core type definitions for the Declarative Self-Improving TypeScript SDK.
|
|
2
|
+
* ocpipe shared types.
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
import type { z } from 'zod/v4'
|
|
@@ -131,14 +129,24 @@ export interface SignatureDef<
|
|
|
131
129
|
}
|
|
132
130
|
|
|
133
131
|
/** Infer the input type from a signature definition. */
|
|
134
|
-
export type InferInputs<
|
|
135
|
-
S extends SignatureDef<
|
|
132
|
+
export type InferInputs<
|
|
133
|
+
S extends SignatureDef<
|
|
134
|
+
Record<string, FieldConfig>,
|
|
135
|
+
Record<string, FieldConfig>
|
|
136
|
+
>,
|
|
137
|
+
> =
|
|
138
|
+
S extends SignatureDef<infer I, Record<string, FieldConfig>> ?
|
|
136
139
|
{ [K in keyof I]: z.infer<I[K]['type']> }
|
|
137
140
|
: never
|
|
138
141
|
|
|
139
142
|
/** Infer the output type from a signature definition. */
|
|
140
|
-
export type InferOutputs<
|
|
141
|
-
S extends SignatureDef<
|
|
143
|
+
export type InferOutputs<
|
|
144
|
+
S extends SignatureDef<
|
|
145
|
+
Record<string, FieldConfig>,
|
|
146
|
+
Record<string, FieldConfig>
|
|
147
|
+
>,
|
|
148
|
+
> =
|
|
149
|
+
S extends SignatureDef<Record<string, FieldConfig>, infer O> ?
|
|
142
150
|
{ [K in keyof O]: z.infer<O[K]['type']> }
|
|
143
151
|
: never
|
|
144
152
|
|