Repository navigation
Expand file tree
/
Copy pathgenerate.go
More file actions
233 lines (208 loc) · 7.92 KB
/
Copy pathgenerate.go
File metadata and controls
233 lines (208 loc) · 7.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Package generate scaffolds a Python runnable from its declaration: the
// author's handler, the typed bindings of its contract, and the generated
// directory holding the harness and the contract it enforces.
package generate
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
"github.com/codefly-dev/core/resources"
"github.com/codefly-dev/runnable-python/pkg/contract"
"github.com/codefly-dev/runnable-python/pkg/harness"
)
// Layout of what the agent owns inside a runnable directory.
const (
// GeneratedDirectory holds everything the agent regenerates; nothing an
// author edits lives here.
GeneratedDirectory = ".codefly"
ContractFile = "contract.json"
ProjectFile = "pyproject.toml"
LockFile = "uv.lock"
// PythonRequirement is the interpreter floor of generated code: the typed
// bindings use typing.NotRequired.
PythonRequirement = ">=3.11"
)
var identifier = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
// Scaffold writes the author-owned files of a new runnable. It never
// overwrites: a handler is the author's, and regenerating a runnable must not
// silently discard an implementation.
func Scaffold(runnable *resources.Runnable, dir string) error {
handler, err := Confine(dir, runnable.Entrypoint.Handler)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(handler), 0o755); err != nil {
return err
}
if err := writeIfAbsent(handler, handlerTemplate(runnable)); err != nil {
return err
}
return writeIfAbsent(filepath.Join(dir, ProjectFile), projectTemplate(runnable))
}
// Generate writes the agent-owned files: the typed bindings, the contract the
// harness enforces and the harness itself. Regenerating an unchanged
// declaration produces the same bytes.
func Generate(runnable *resources.Runnable, dir string) error {
return GenerateForRelease(runnable, dir, nil)
}
// GenerateForRelease binds the harness to the workspace-resolved release.
// Standalone generation checks the declaration's name and version; the Builder
// always supplies the complete identity resolved during Load.
func GenerateForRelease(runnable *resources.Runnable, dir string, identity *basev0.RunnableIdentity) error {
if err := contract.Supported(runnable); err != nil {
return err
}
module, err := HandlerModule(runnable.Entrypoint.Handler)
if err != nil {
return err
}
generated := filepath.Join(dir, GeneratedDirectory)
if err := os.RemoveAll(generated); err != nil {
return err
}
if err := os.MkdirAll(generated, 0o755); err != nil {
return err
}
if err := harness.Write(generated); err != nil {
return fmt.Errorf("write harness: %w", err)
}
generatedContract := contract.Generate(runnable, module)
if identity != nil {
generatedContract.Runnable = map[string]string{"name": identity.GetName(), "module": identity.GetModule(), "workspace": identity.GetWorkspace(), "version": identity.GetVersion()}
}
document, err := generatedContract.Encode()
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(generated, ContractFile), document, 0o644); err != nil {
return err
}
return os.WriteFile(filepath.Join(dir, TypesFile), Types(runnable), 0o644)
}
// HandlerModule is the importable name of the declared entrypoint. The
// declaration allows any runnable-relative path; only an importable Python
// module can be a handler.
func HandlerModule(handler string) (string, error) {
trimmed := strings.TrimSuffix(handler, ".py")
if trimmed == handler {
return "", fmt.Errorf("handler %q is not a Python module: expected a .py file", handler)
}
segments := strings.Split(filepath.ToSlash(trimmed), "/")
for _, segment := range segments {
if !identifier.MatchString(segment) {
return "", fmt.Errorf("handler %q is not importable: %q is not an identifier", handler, segment)
}
}
return strings.Join(segments, "."), nil
}
// Resolve returns where target really is: symlinks resolved through the part
// of the path that exists, with the components that do not yet exist appended
// to it. Every containment check runs on this form, because the spelling a
// caller supplies says nothing about where a symlink in it leads.
//
// A dangling symlink is refused rather than treated as a missing directory:
// creating through one writes wherever it points.
func Resolve(target string) (string, error) {
ancestor := filepath.Clean(target)
var missing []string
for {
resolved, err := filepath.EvalSymlinks(ancestor)
if err == nil {
for i := len(missing) - 1; i >= 0; i-- {
resolved = filepath.Join(resolved, missing[i])
}
return resolved, nil
}
if !os.IsNotExist(err) {
return "", err
}
if _, statErr := os.Lstat(ancestor); statErr == nil {
return "", fmt.Errorf("path %q contains a dangling symlink", target)
}
missing = append(missing, filepath.Base(ancestor))
parent := filepath.Dir(ancestor)
if parent == ancestor {
return "", err
}
ancestor = parent
}
}
// Within says whether path is strictly inside directory. Both must already be
// resolved: a prefix test on unresolved paths proves nothing.
func Within(path string, directory string) bool {
separator := string(filepath.Separator)
return strings.HasPrefix(path, strings.TrimSuffix(directory, separator)+separator)
}
// Confine resolves a declared runnable-relative path inside dir and refuses a
// target outside it, symlinks included: core validates the declared spelling,
// but whoever opens the file is what decides what content is trusted.
func Confine(dir string, relative string) (string, error) {
root, err := filepath.EvalSymlinks(dir)
if err != nil {
return "", fmt.Errorf("resolve runnable directory: %w", err)
}
resolved, err := Resolve(filepath.Join(root, relative))
if err != nil {
return "", err
}
if resolved != root && !Within(resolved, root) {
return "", fmt.Errorf("path %q resolves outside the runnable directory", relative)
}
return resolved, nil
}
func writeIfAbsent(path string, content []byte) error {
if _, err := os.Stat(path); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
return os.WriteFile(path, content, 0o644)
}
func handlerTemplate(runnable *resources.Runnable) []byte {
description := runnable.Description
if description == "" {
description = fmt.Sprintf("the %s operation", runnable.Name)
}
scaffold := fmt.Sprintf(`%q
from codefly_runnable import Context
from codefly_types import Input, Output
def handle(context: Context, input: Input) -> %s:
"""Answer one call.
The input has already been validated against the contract, and the output
is validated before the answer is sent.
"""
raise NotImplementedError("implement %s")
`, description, OutputType, runnable.Name)
// A receipt-recovery operation serves the receipt route, and the harness
// refuses to start without the function behind it. Scaffolding it here is
// what keeps that refusal from being the author's first sight of it.
if runnable.Execution.Recovery == resources.RunnableRecoveryReceipt {
scaffold += fmt.Sprintf(`
def %s(context: Context, input: Input) -> %s | None:
"""Answer what this effect committed, or None when there is no receipt.
None is not an error and never means the effect did not happen: it is the
answer a caller reads as inconclusive, which is what sends it looking again
rather than invoking a second time.
Authorize the lookup here: answer only for an effect the caller's
context.invocation.work_context may read. The harness requires a Work
Context but neither verifies it nor binds the effect id to it.
"""
raise NotImplementedError("implement the receipt lookup of %s")
`, contract.ReceiptAttribute, OutputType, runnable.Name)
}
return []byte(scaffold)
}
func projectTemplate(runnable *resources.Runnable) []byte {
return []byte(fmt.Sprintf(`[project]
name = %q
version = %q
description = %q
requires-python = %q
dependencies = []
[tool.uv]
package = false
`, runnable.Name, runnable.Version, runnable.Description, PythonRequirement))
}